Adapter and Anti-Corruption Layer Patterns for External Supplier Integrations
Why this matters in travel
A flight-search domain should not become:
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
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
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:
ERR_4912Supplier B:
NO_AVAILMy domain wants:
AVAILABILITY_CHANGEDAdapter:
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.
legacy ORGANIZATION_CODE
-> BuyerId
legacy STATUS=7
-> TenderStatus.CLOSED
legacy DATE string DDMMYYYY
-> ZonedDateTimeProvider capability differences
I do not create an enormous lowest-common-denominator model.
If one supplier supports seat maps and another does not:
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.
Related architecture guides
- [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)
-
Architect's final take
The adapter boundary lets the business model remain stable while suppliers, SDKs and legacy systems change around it.
Your feedback helps prioritize deeper technical content.






