Learning outcome
After this lesson, you can distinguish Adapter from Facade by intent rather than class shape. You will be able to choose a migration boundary based on ownership, dependency direction, and the needs of callers; explain how the patterns can coexist; and recognize when a simple function is sufficient.
Intuition
An Adapter makes one interface usable as another. The consumer already has a preferred contract, but a legacy service speaks a different language. The adapter translates operations, data, errors, and sometimes interaction style so the legacy dependency fits the consumer’s model.
A Facade gives callers a simpler entry point to a complicated subsystem. The underlying services may already be usable, but callers should not need to understand their ordering rules, coordination, or internal structure.
A useful question is: Whose problem is the boundary solving?
- If it protects a consumer from an incompatible dependency, it is probably an Adapter.
- If it protects callers from subsystem complexity, it is probably a Facade.
- If translation and orchestration are both required, the patterns can coexist at different boundaries.
Deep dive
| Dimension | Adapter | Facade |
|---|---|---|
| Primary intent | Convert one contract into another | Simplify access to a subsystem |
| Starting point | A consumer contract and an incompatible provider | Several operations or components that are cumbersome to use directly |
| Typical ownership | Owned near the side that requires the preferred contract | Owned by the team presenting the subsystem capability |
| Dependency direction | Consumer logic depends on its preferred abstraction; the adapter depends on the legacy API | Callers depend on the facade; the facade coordinates subsystem components |
| Translation | Usually central: names, types, errors, units, or protocols | Optional; orchestration and hiding complexity are central |
| Number of wrapped components | Often one, but not necessarily | Often several, but one complex subsystem can justify a facade |
| Effect on callers | Makes a foreign service look compatible | Makes a complex workflow look simple |
Worked scenario 1: adapting a legacy customer service
A new billing component defines customer eligibility in business terms meaningful to billing. The legacy customer service exposes account status codes, nullable date fields, and transport-specific failures.
Placing those legacy concepts directly in billing would reverse the desired dependency direction: billing policy would become coupled to an external contract. Instead, the billing-owned Adapter implements billing’s preferred customer-information boundary. It translates status codes into billing concepts, normalizes missing values according to an explicit policy, and maps transport failures into errors the billing application can handle.
This is an Adapter because compatibility is the main concern. It should not quietly coordinate invoicing, payment, and notifications; that would add unrelated workflow responsibility.
Worked scenario 2: simplifying account closure
Closing an account requires verifying outstanding balances, revoking credentials, scheduling data retention, recording an audit event, and notifying the customer. Each participating component has an acceptable interface, so incompatibility is not the main problem. The problem is that every caller would otherwise need to know the sequence and failure policy.
An account-closure Facade presents one application-level capability and owns that coordination. Callers do not depend on the subsystem’s internal topology. Individual legacy services behind the facade may still require Adapters. For example, an audit Adapter can translate the facade’s audit request into the old audit service’s contract.
The Facade therefore simplifies the workflow, while nested Adapters handle incompatible integrations. Their responsibilities remain distinct even when they appear in the same request path.
When a simple function is enough
Do not introduce a named pattern merely because a call needs minor reshaping. A small, stateless conversion used in one local context may be clearer as a function. Escalate to an Adapter when there is a meaningful contract boundary, substitution is useful, or translation policy deserves isolated ownership. Use a Facade when callers would otherwise duplicate coordination or become coupled to subsystem structure.
Failure modes
- Calling every wrapper a Facade: A one-to-one contract translator is still an Adapter, even if its interface is smaller.
- Leaking legacy vocabulary: If consumer code handles old status codes or transport exceptions, the Adapter has not completed the boundary.
- Creating a god Facade: Combining unrelated workflows produces a broad service with weak cohesion and difficult ownership.
- Hiding business policy as translation: An Adapter should translate at the boundary, not become an unreviewed home for pricing or eligibility rules.
- Allowing direct bypasses: If callers freely access components behind a Facade, orchestration rules become optional and duplicated.
- Abstracting too early: A named interface and wrapper add navigation cost when a local function would express the entire transformation.
Interview drill
Decision exercises
- A new order service expects money in minor units, while a legacy pricing endpoint returns decimal amounts and currency codes. Which pattern fits?
Answer: Use an Adapter owned near the order service’s integration boundary. The essential task is translating an incompatible provider contract into the consumer’s chosen representation, including an explicit rounding policy.
- Three valid services must be called in order to provision a tenant, and several API endpoints need that workflow. Which pattern fits?
Answer: Use a Facade for tenant provisioning. The contracts are usable, but exposing ordering and partial-failure rules would duplicate subsystem knowledge across callers. Add Adapters only where an individual service contract is incompatible.
- One endpoint needs to trim and normalize a label before passing it to an otherwise compatible service. Which pattern fits?
Answer: Start with a simple function. There is no substantial integration contract or subsystem complexity. Promote it only if normalization becomes shared policy or the external boundary develops meaningful translation concerns.
Five interview questions
- What difference in intent separates Adapter from Facade even when both wrap another object?
- How does consumer ownership of an interface help keep dependencies pointed away from a legacy service?
- When can an Adapter legitimately sit behind a Facade?
- What evidence suggests that a Facade has become an incoherent god service?
- How would you decide whether a transformation deserves an Adapter rather than a local function?
Revision checklist
- Identify whether the real problem is incompatibility or complexity.
- Keep consumer business logic independent of legacy contracts.
- Give orchestration and translation separate, explicit responsibilities.
- Place ownership where the preferred contract or presented capability is governed.
- Permit Adapters behind a Facade when both concerns exist.
- Prefer a simple function when the transformation is local and policy-light.
- Prevent callers from bypassing a boundary whose rules must remain consistent.