Cruxly › Low-level design › More Design Patterns
Facade
hiding a wide subsystem behind the slice callers actually need
Open the interactive lessonForcing problem: a subsystem (a payment SDK, a media library, a set of OS calls) exposes a wide, detailed interface, but any given caller only needs a small, specific sequence of it — and every caller re-deriving that sequence is both repetitive and fragile to the subsystem's internal changes.
The violation: the subsystem's shape leaking everywhere
// Every call site that wants to charge a card repeats this entire dance.
void checkoutA(Cart cart, Card card) {
PaymentGatewayClient client = new PaymentGatewayClient(apiKey);
client.authenticate();
TokenResponse token = client.tokenizeCard(card.number, card.expiry, card.cvv);
RiskScore risk = client.assessRisk(token.id, cart.total);
if (risk.score > THRESHOLD) throw new PaymentDeclinedException();
ChargeResponse charge = client.charge(token.id, cart.total, cart.currency);
client.logTransaction(charge.id);
}
Four other call sites in the codebase repeat this same five-step sequence, in the same order, because that order is a fact about the payment SDK, not about any individual checkout flow. When the SDK adds a mandatory sixth step, or changes an argument order, every one of those call sites needs the identical edit — this is the shotgun-surgery smell from Open/Closed elsewhere in this track, just caused by an external library's shape instead of an internal type switch.
The fix: one class owns the sequence
class PaymentFacade {
private final PaymentGatewayClient client;
PaymentFacade(String apiKey) {
this.client = new PaymentGatewayClient(apiKey);
client.authenticate();
}
/** Charges a card for a cart total. Throws PaymentDeclinedException on risk failure. */
ChargeReceipt charge(Card card, Cart cart) {
TokenResponse token = client.tokenizeCard(card.number, card.expiry, card.cvv);
RiskScore risk = client.assessRisk(token.id, cart.total);
if (risk.score > THRESHOLD) throw new PaymentDeclinedException();
ChargeResponse charge = client.charge(token.id, cart.total, cart.currency);
client.logTransaction(charge.id);
return new ChargeReceipt(charge.id, cart.total);
}
}
// every call site now does this, and only this
ChargeReceipt receipt = paymentFacade.charge(card, cart);
Every call site now depends on one method with one clear contract, and the SDK's five-step internal shape is knowledge that lives in exactly one place. When the gateway changes its API, one class changes.
The Facade does not hide capability, it hides sequencing. Advanced callers who genuinely need the SDK's raw client for something the facade doesn't cover can still reach it directly — PaymentFacade is a convenience layer over the subsystem, not a wall around it. This is worth saying explicitly, because the alternative — a facade that tries to be the only way to touch the subsystem — tends to grow an ever-expanding pile of pass-through methods as new needs arise.
The mechanism: delegation, not reimplementation
When Facade becomes the problem it was meant to solve
A Facade that accumulates business logic, not just sequencing, has become a god-object. If PaymentFacade.charge() starts making pricing decisions, applying discounts, or deciding which of three payment providers to use based on business rules, it has grown past "convenience wrapper over a subsystem" into "a class doing several unrelated jobs" — the exact SRP violation covered elsewhere in this track, now wearing a Facade's name.
A Facade over a Facade over a Facade is a sign the abstraction boundary was drawn in the wrong place, not a sign more layering is needed. If calling code needs to reach through two facades to get anything done, one of them is not actually simplifying anything.
The test for whether a class is still a legitimate Facade: could you regenerate this class purely by looking at the subsystem's own documentation and asking "what sequence does a typical caller need"? If the answer requires knowing business rules that have nothing to do with the subsystem itself, that logic belongs in a different class that uses the Facade, not inside it.
:::key Facade versus Adapter, since they are easy to conflate Adapter (covered elsewhere in this track) exists because an interface doesn't match what your code expects — it translates shape. Facade exists because an interface is too detailed for what most callers need — it simplifies sequencing. A Facade can sit in front of an Adapter, and often does, but they solve different problems. :::
How to say it in the interview
"The tell is the same five or six calls into a third-party library, in the same order, copy-pasted across a dozen call sites — that repetition is the actual problem, and Facade is just the name for the class that owns the sequence once."
Checklist
- Recognised repeated multi-step subsystem calls across sites as the forcing problem
- Made call sites depend on one Facade method with a clear contract
- Kept the underlying subsystem reachable for callers who need more than the Facade covers
- Recognised business logic accumulating inside a Facade as an SRP violation wearing a pattern's name
- Distinguished Facade (simplifies sequencing) from Adapter (translates a mismatched shape)