Adapter and Anti-Corruption Layer Patterns for External Supplier Integrations

How I isolate airline, hotel, payment and legacy-system differences behind adapters and anti-corruption layers so external provider models do not leak into core business logic.

Romharshan Singh
Romharshan SinghSenior Solution Architect • AI & Cloud Mentor
1 September 20263 min read0 viewsUpdated 1 Sept 2026
Adapter and Anti-Corruption Layer Patterns for External Supplier Integrations

Adapter and Anti-Corruption Layer Patterns for External Supplier Integrations

Why this matters in travel

A flight-search domain should not become:

typescript
if (supplier === 'A') ...
else if (supplier === 'B') ...
else if (supplier === 'C') ...

through every booking class.

External suppliers use different:

  • field names;
  • pricing structures;
  • error codes;
  • availability semantics;
  • cancellation rules;
  • authentication;
  • timeouts.

I hide that behind an internal contract.

Internal interface

typescript
export interface FlightProvider {
  search(
    request: FlightSearch,
  ): Promise<FlightOption[]>;

  reserve(
    request: FlightReserve,
  ): Promise<Reservation>;

  cancel(
    reservationId: string,
  ): Promise<void>;
}

Business code depends on FlightProvider.

Supplier adapter

typescript
export class SupplierAAdapter
  implements FlightProvider {

  constructor(
    private readonly client: SupplierAClient,
  ) {}

  async search(
    req: FlightSearch,
  ): Promise<FlightOption[]> {
    const raw =
      await this.client.search(
        toSupplierARequest(req),
      );

    return raw.items.map((x) => ({
      id: x.offerRef,
      total: Number(x.amount.total),
      currency: x.amount.currency,
      segments: mapSegments(x.legs),
    }));
  }

  async reserve(
    req: FlightReserve,
  ) {
    const raw = await this.client.reserve(
      toSupplierAReserve(req),
    );

    return mapReservation(raw);
  }

  async cancel(id: string) {
    await this.client.cancel(
      toSupplierAReservationId(id),
    );
  }
}

Error normalization

Supplier A:

plaintext
ERR_4912

Supplier B:

plaintext
NO_AVAIL

My domain wants:

plaintext
AVAILABILITY_CHANGED

Adapter:

typescript
function mapSupplierError(
  error: SupplierError,
): DomainError {
  switch (error.code) {
    case 'ERR_4912':
      return new AvailabilityChangedError();

    case 'AUTH_EXPIRED':
      return new ProviderAuthenticationError();

    default:
      return new ProviderUnavailableError();
  }
}

The rest of the booking system does not need provider error vocabulary.

Anti-Corruption Layer for legacy systems

A legacy procurement/project system can expose a very different domain model.

The ACL translates concepts, not only JSON.

plaintext
legacy ORGANIZATION_CODE
   -> BuyerId

legacy STATUS=7
   -> TenderStatus.CLOSED

legacy DATE string DDMMYYYY
   -> ZonedDateTime

Provider capability differences

I do not create an enormous lowest-common-denominator model.

If one supplier supports seat maps and another does not:

typescript
interface SeatMapCapableProvider {
  getSeatMap(
    offerId: string,
  ): Promise<SeatMap>;
}

Optional capabilities remain explicit.

Resilience belongs around the adapter

The adapter/client is a good boundary for:

  • timeout;
  • retry;
  • circuit breaker;
  • provider metrics;
  • correlation ID;
  • rate limit.

The domain service should see a predictable contract.

Production checklist

  • Define internal contract first.
  • Keep provider SDK types out of domain.
  • Normalize errors.
  • Add provider-specific timeout.
  • Make retry safe.
  • Trace provider latency separately.
  • Contract-test mappings.
  • Version supplier integrations.
  • Model optional capabilities explicitly.
  • Do not leak legacy semantics into new services.

FAQ

Adapter vs anti-corruption layer?

Adapter translates interfaces. ACL protects the domain from an external/legacy conceptual model.

Should every provider have a separate adapter?

Yes when contracts/behavior differ meaningfully.

Should retries live in the domain?

Usually no. I keep transport/provider resilience in the integration boundary.

- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects) - [Strangler Pattern](/articles/strangler-pattern-monolith-to-microservices-modernization)
Was this article useful?

Your feedback helps prioritize deeper technical content.

Romharshan Singh
ABOUT THE AUTHOR

Romharshan Singh

Senior Solution Architect and Full Stack Technology Leader with 20+ years of enterprise engineering experience across AI, cloud, distributed systems, Java, Node.js, React, Angular, Kafka and Kubernetes.