Skip to content
beginner Phase 3 · Design Patterns at Scale

Structural Patterns: Adapter and Facade

Shape foreign interfaces into the contracts your domain expects.

50m
0 problems
Topic Progress 0%

Wrapping the Vendor SDK So Its Churn Stops at One File

The payments vendor ships version 4 of their SDK. Their ChargeRequest builder loses two methods, their exception hierarchy is renamed, and their idempotency key moves from a header into the request body. If the vendor's types are spread through your services, this upgrade touches forty files across four teams. If they are not, it touches one adapter. Adapters translate one interface into another so mismatched components interoperate — and in distributed systems, the mismatch being adapted is almost always between your domain's language and a vendor's.

The design has two halves: the port your code already speaks, and the adapter that translates to and from the vendor:

// The contract your domain invented (and loves):
public interface PaymentProcessor {
    ChargeReceipt charge(Money amount, CardToken card, IdempotencyKey key)
        throws PaymentDeclined, PaymentUnavailable;
}

// The adapter: the ONLY file importing the vendor SDK:
public final class StripeV3Processor implements PaymentProcessor {
    private final StripeClient vendor;   // com.stripe:stripe-java

    public ChargeReceipt charge(Money m, CardToken t, IdempotencyKey k) {
        try {
            var resp = vendor.charges()                       // vendor shape
                .create(toVendorAmount(m), toVendorToken(t))
                .withIdempotencyKey(k.value())
                .execute();
            return new ChargeReceipt(resp.getId(), resp.getPaid());
        } catch (StripeException e) {                         // vendor shape
            throw mapToDomain(e);   // Declined / Unavailable / Unexpected
        }
    }
}

Three properties are doing the work. Translation, not extension: the adapter converts types and exceptions both directions and adds no behavior of its own — the moment it starts caching charges or retrying internally, it becomes a different component with different responsibilities. The import boundary: grep com.stripe in your source tree and the result should be a list of adapter files, nothing else; that single grep is the enforceable version of the anti-corruption boundary, and it belongs in CI as an architecture rule. The domain owns the vocabulary: your contract says PaymentDeclined; the vendor says CardException type=card_declined. Every concept crossing the boundary is re-named into your language, so vendor vocabulary never leaks into business logic, error handling, or the logs your support team reads at 3 a.m.

The economic argument for the extra layer is the part leadership hears: without adapters, a vendor upgrade is a cross-team migration with a coordination tax and a release coupling; with adapters, it is one module's test suite. The adapter also buys honest testability — unit tests exercise a FakePaymentProcessor implementing your port, with the vendor absent entirely — and it gives you somewhere to put the operational concerns that are yours, not the vendor's: timeout configuration, retry policy with budgets, metrics labeling. Interviewers ask this shape directly — 'how do you handle a dependency on an external service?' — and the strong answer names the port, the adapter, the grep rule, and the migration story. The companion pattern operates at a different scale: where the adapter wraps one foreign thing, the facade wraps many familiar things — Facade over subsystems. The adapter also becomes the natural home for concerns that are yours rather than the vendor's, which is a detail reviewers appreciate. Timeout configuration, retry budgets with jitter, circuit-breaker hooks, and metric labels describing outcomes in your vocabulary all live in the adapter, where they apply uniformly to every caller of the port. If those policies were left to each call site, you would get the usual drift: one team retries three times with no backoff, another has no timeout at all, and the vendor sees thundering herds assembled by your own inconsistency.

One Entry Point Over Nine Subsystems: Facades at Application Scale

Checkout needs to do five things in order: reserve inventory, authorize the card, create the order, emit a OrderPlaced event, and start fraud scoring. Without a unifying entry point, each caller — the REST controller, the mobile BFF, the customer-service tool — reimplements the five-step dance, and the sixtieth call site is where the step order drifts. Facades offer a simplified entry point over a cluster of collaborating subsystems, and at application scale the facade is usually a use case orchestrating services that stay independently useful.

// The facade: one method, one transaction boundary, one vocabulary:
public final class Checkout {
    private final Inventory inventory;
    private final PaymentProcessor payments;
    private final Orders orders;
    private final OrderEventPublisher events;
    private final FraudScoring fraud;

    public CheckoutReceipt place(PlaceCheckout cmd) {
        Reservation r = inventory.reserve(cmd.lines());      // may throw
        ChargeReceipt c = payments.charge(cmd.total(), cmd.card(), cmd.idem());
        Order order = orders.save(Order.from(cmd, r, c));
        events.orderPlaced(order);
        fraud.enqueue(order);
        return new CheckoutReceipt(order.id(), c.transactionId());
    }
}

Read what the facade gives each audience. Callers get one method and one result object — no knowledge of the five services, no ordering mistakes. The services keep their own contracts and stay independently testable and reusable (customer service still calls payments directly for manual refunds; the facade did not absorb their logic). Operations get a named seam for the cross-cutting concerns that belong to the flow rather than to any service: a timeout budget split across the five calls, compensation logic if the charge succeeds after the reservation fails, and a single trace span named checkout.place that makes the flow legible in dashboards.

Two failure modes keep the pattern honest. The god facade: as more flows get added, the single CheckoutService grows to hold twenty orchestration methods and its own data access, becoming exactly the multi-responsibility hub the smell catalog warns about — the cure is one facade per flow or cohesive flow family, small enough that each method reads as a paragraph. The leaky facade: callers start needing the subsystem objects after all (give me the raw inventory service, I need the low-level API), which means the abstraction is missing something real; the cure is to widen the facade's contract with the new operation, not to export the internals. The design question that settles most arguments about granularity: facade methods should correspond to business operations a stakeholder can name — place, refund, cancel — not to technical steps like stepTwoAuthorize. When a new caller appears (the kiosk app, the partner API), the facade is where you point it; when a new operation appears, it is a new facade method backed by the same services. The next topic extends the same wrapper instinct to algorithms that vary at runtime — Strategy and Observer. Granularity questions also arise around transactionality. The facade is the natural owner of the flow-level transactional story: either the whole place() participates in one database transaction, or the facade implements compensation (release the reservation if the charge fails after reservation succeeded) with the same rigor. Distributing transaction decisions across the five services instead leaves each one guessing about the others' rollback behavior, and the resulting saga logic, if needed at all, belongs at the orchestration seam — which is precisely the entry point the facade already is.

Adapter, Facade, Decorator, Proxy: Choosing the Right Wrapper

Structural patterns are all wrappers, and interviews distinguish candidates by which question the wrapper answers. The four you must be able to separate on sight:

Pattern The question it answers Shape Tell-tale
Adapter How do I make this foreign thing fit my interface? translates types/exceptions into your vocabulary implements YOUR port, wraps THEIR SDK
Facade What is the simple way into this cluster of services? one entry point over many collaborators one method per business operation
Decorator How do I add behavior around one object without editing it? same interface, layered (Logger(Sms(rateLimited(real)))) nesting at construction, behavior stacked
Proxy How do I stand in for an object to control access? same interface, one delegate lazy loading, authz check, remote call

The differences are visible in three tests. Who owns the interface — an adapter implements your port and is the only party who knows the foreign API; a facade exposes the same domain's simplified surface; decorator and proxy preserve the wrapped object's interface exactly, which is what makes stacking (decorator) or substitution (proxy) possible. Where nesting appears — decorators compose at construction with visible nesting (withLogging(withRetry(service))), while facades never nest from the caller's perspective; they present one flat call. What may change — adapter and facade change the contract seen by callers (deliberately); decorators and proxies must not, or they break the open-closed promise they exist to provide.

A single concrete decision to anchor the table: you are wrapping a rate-limited SMS vendor. If the vendor's API shape differs from your NotificationChannel port — different send signature, different exception types — you need an adapter. If the vendor's library requires configuring three objects (client, template engine, meter) before sending, and you want callers to see one send() — that is a facade built on top of the adapter. If you need per-tenant rate limiting around whatever channel is configured — decorator, stacked around any channel implementation. If you need to check the caller's permission tier before delegating — proxy. Real systems use several of these in one stack, in that order of concerns, and being able to narrate the stack is a strong interview answer.

The final discipline is knowing when to write none. If the foreign API already matches your port's shape, the adapter would be a pure pass-through — one more hop for the reader with no translation to justify it; wire the SDK directly behind the port until friction appears. If the facade wraps exactly one service with one method, it is a rename, and a rename is fine without a class. Wrapper patterns earn their indirection when they stop real churn (vendor upgrades), real complexity (multi-object setup), or real variation (stacked behaviors) — name which one you are stopping, and the pattern defends itself. With objects composed, the remaining axis of design is what objects do when things happen: Behavioral Patterns: Strategy and Observer.

Quiz

1. A vendor SDK upgrade renames its exception hierarchy. With a proper adapter in place, what is the expected blast radius?

Question 1 options

2. A `Checkout` facade orchestrates inventory reservation, payment, order persistence, event publishing, and fraud scoring. What is the correct sign that it is becoming a god facade?

Question 2 options

3. You need per-tenant rate limiting wrapped around any configured notification channel without editing channel implementations. Which pattern fits, and what is its structural tell?

Question 3 options

4. Your port `NotificationChannel.send(Message)` has a signature identical to the vendor SDK's client method. What is the correct call?

Question 4 options

Flashcards

Question

State the adapter pattern and the three properties that make a vendor adapter effective.

Answer

It translates one interface into another so a foreign component fits the contract your code speaks. Effective adapters: pure translation (no added behavior — caching or retries make it a different component), an import boundary (grep for the vendor package returns only adapter files, enforced in CI), and domain-owned vocabulary (vendor exceptions renamed into PaymentDeclined/Unavailable so vendor language never reaches business logic or logs).

Question

What is a facade's contract with callers and with the subsystems behind it?

Answer

Callers get one entry point per business operation (place, refund, cancel) with one vocabulary and one transaction seam; subsystems keep their own contracts and remain independently reusable (customer service still calls payments directly for manual refunds). The facade orchestrates — it must not absorb subsystem logic or data access, or it becomes the god facade and splits per flow family.

Question

Distinguish adapter, facade, decorator, and proxy using the three diagnostic tests.

Answer

Who owns the interface: adapter implements YOUR port over their SDK; facade presents a simplified same-domain surface; decorator/proxy preserve the wrapped interface exactly. Nesting: only decorators nest visibly at construction. What may change: adapters and facades deliberately change the caller-visible contract; decorators and proxies must not, or they break the open-closed promise they exist to provide.

Question

You wrap a rate-limited SMS vendor needing client + template engine + meter configuration, with per-tenant rate limits and permission checks required. Name the pattern stack in order.

Answer

Facade over adapter first — the multi-object setup is flattened behind one send() (facade), which translates the vendor's shapes into the NotificationChannel port (adapter). Then per-tenant limiting as a decorator stacked around any channel, and the permission-tier check as a proxy at the front. Real systems compose several structural wrappers in one stack; narrating the stack is the interview answer.

Question

When should you deliberately NOT write a wrapper?

Answer

When the foreign API already matches your port (a pass-through adapter is a hop with no translation to justify), when a facade would wrap one service with one method (that is a rename, which is fine without a class), or when no real churn, setup complexity, or behavior variation exists to stop. Wrappers earn indirection by stopping something concrete — name it or skip it.

Revision Notes

Key Takeaways

  • 1. The adapter's enforceable boundary is a grep: vendor package imports return only adapter files, so a vendor upgrade is one module's test suite instead of a four-team migration
  • 2. Adapters translate both types and failure vocabulary into domain language, keeping vendor terms out of business logic, support logs, and incident channels
  • 3. Facades orchestrate without absorbing: business-operation methods over independently reusable services; the god-facade and leaky-facade failure modes have named cures
  • 4. All structural patterns are wrappers, separated by three tests: who owns the interface, whether nesting is visible, and whether the caller-visible contract changes
  • 5. The discipline of writing no wrapper — pass-through adapters and one-method facades are indirection without a buyer — is what marks pattern selection as architecture rather than habit

Interview Tips

  • • 'How do you isolate a third-party dependency?' — the port plus adapter, the CI grep rule, domain-owned exception mapping, and the migration story (vendor v4 upgrade touches one file)
  • • 'Adapter vs facade?' — adapter changes one foreign interface to yours; facade flattens many of your own services into business-operation entry points; offer the checkout example with its transaction seam
  • • 'Where do decorators and proxies fit in your stacks?' — rate limiting and retry as stacked decorators, permission checks as proxies; emphasize that they must preserve the interface or they break open-closed
  • • 'When is a wrapper over-engineering?' — identical shapes (pass-through), single-service single-method facade, no churn to stop; the ability to decline patterns is what separates architects from pattern collectors

Cheat Sheet

Structural Patterns: Adapter and Facade

Vendor adapter shape:

public interface PaymentProcessor {           // YOUR port
  ChargeReceipt charge(Money m, CardToken t, IdempotencyKey k)
      throws PaymentDeclined, PaymentUnavailable;
}                                             // only file importing SDK:
final class StripeV3Processor implements PaymentProcessor { ... mapToDomain(e) }

Rules: pure translation (no hidden behavior) · CI grep for vendor imports returns only adapters · domain owns vocabulary (map exceptions both ways).

Facade: one method per business operation (place/refund/cancel), one transaction + timeout seam, services stay independently reusable. God-facade signal: 20 unrelated flows + own data access → one facade per flow family. Leaky-facade signal: callers demanding internals → widen the contract, do not export services.

Wrapper chooser:

Pattern Question Tell
Adapter fit this foreign API to my port implements your port, wraps their SDK
Facade simple entry into this cluster flat call, no nesting
Decorator add behavior, keep interface visible nesting: logger(retry(x))
Proxy control access to the same interface lazy/authz/remote standing in

Stack example: facade (flatten setup) → adapter (translate vendor) → decorator (per-tenant rate limit) → proxy (permission check).

Write none when: shapes already match (pass-through), one-service-one-method (rename suffices), or nothing concrete (churn, setup, variation) is being stopped.

Continue Learning