What signals tell you a facade has stopped helping and become a liability, and what would you do about each?
answer
- 1:1 pass-through = no simplification
- God facade → split per client group
- Leaky signatures leak internal types/exceptions
- Unused facade = wrong level or dead code
- Subsystem referencing facade = cycle, not Facade
basics
~20 sWarning signs: it mirrors the subsystem method-for-method, it keeps growing into a god-object, it leaks internal types in its signatures, or nobody uses it because callers still import internals. Fix by resizing, splitting, or deleting it.
solid answer
~50 sFour smells. **Pass-through / 1:1 mirror** — every facade method delegates to exactly one subsystem call with the same parameters; it adds a maintenance surface and no simplification, so delete it or raise its granularity to real tasks. **God facade** — one class fronts unrelated concerns, dozens of methods, ballooning constructor dependencies; split into several narrow facades per client group (Interface Segregation) or return focused capability objects. **Leaky signatures** — the facade's parameters and return types are internal entity/DTO types, so callers still compile against internals and refactoring is still blocked; introduce boundary types. **Unused facade** — telemetry or grep shows most callers bypass it; either the abstraction is at the wrong level (re-derive it from real usage) or it should go. Also watch for facades that grow business logic (now a service layer, name it honestly) and for cycles where subsystem code calls back into the facade — that breaks the pattern outright.
go deeper
Name one or two smells — a facade that just forwards calls, or one nobody uses — and say it can be deleted.
Give several smells with a concrete fix for each, and mention splitting by client group.
Add leaky signatures and exception translation, the cycle rule, and an evaluation checklist based on measurable ratios.
Frame it as API lifecycle governance: deprecation and migration strategy, ownership and bottleneck effects, and how architecture tests keep boundaries honest as the org grows.
### Why facades decay A facade is a *judgement about what callers need*. Requirements move; the judgement ages. Unlike most patterns, a stale facade is not merely useless — it is actively expensive, because it is a second API that must be maintained, documented, tested and versioned. Recognising decay early is a senior skill. ### Smell 1 — the pass-through facade (1:1 mirror) ``` class OrderFacade { find(id) { return repo.find(id) } save(o) { return repo.save(o) } delete(id) { return repo.delete(id) } } ``` Every method forwards one call with identical parameters. Nothing was simplified; nothing was reordered; no default was chosen. The caller must still understand the subsystem exactly as before, and now there is a second file to update on every change. - **Why it happens:** cargo-culted layering ('every module must have a facade'), or a facade written before the subsystem was complex enough to need one. - **Diagnosis:** compare the number of subsystem types a caller needed *before* and *after*. If it is unchanged, it is not simplifying. - **Fix:** raise granularity — replace CRUD mirrors with intent methods (`placeOrder`, `cancelWithRefund`) that encapsulate multi-step sequences — or delete it. A thin pass-through is sometimes still justified *purely as a visibility boundary* (so internals can be hidden), and that is a legitimate reason; just be explicit that that, not simplification, is the value. ### Smell 2 — the god facade Symptoms: 30+ methods; a constructor taking 12 collaborators; unrelated concerns (billing + notifications + reporting) behind one type; the file everyone edits, so it is a merge-conflict hotspot; changes to it force recompiles/redeploys everywhere. - **Why it happens:** an opaque facade under pressure — every new need must go through it — combined with reluctance to create a second facade. - **Fix:** segregate by *client*, not by data. Ask 'who calls this, and for what job?' and give each answer its own narrow facade over the same subsystem. Multiple facades over one subsystem is normal and healthy. Alternatively, have one entry point return capability objects (`api.orders()`, `api.refunds()`), keeping discoverability without one flat surface. ### Smell 3 — leaky signatures The facade compiles, but its methods take and return internal types — JPA entities, ORM models, internal enums, library-specific exception types. Callers must import internals to call the facade, so the coupling reduction was illusory and the subsystem still cannot be refactored. - **Fix:** define boundary types (commands, DTOs, records) owned by the facade's own module, and map at the edge. Cost is real — mapping code and duplicated shapes — so apply it where the boundary matters (cross-team, published, or likely-to-change subsystems) rather than everywhere. - Related: leaking *exceptions*. If a facade over an HTTP library throws that library's exception type, swapping the library is a breaking change. Translate to your own error type. ### Smell 4 — the ignored facade Grep shows 6 uses of the facade and 90 direct uses of the subsystem. Either the facade solves the wrong problem (its methods do not match real tasks), or discovery failed (nobody knows it exists), or it is too restrictive and everyone routed around it. - **Fix:** derive the API from actual call sites — collect the top 10 sequences callers write and make those the methods. If after that it is still unused, delete it; an unused abstraction is pure carrying cost. ### Smell 5 — facade grows behaviour It starts computing prices, applying domain rules, or holding transactional state. That may be a perfectly good *application service*, but calling it a facade sets the wrong expectations (reviewers assume it is deletable and behaviourless). Rename it and treat it with the testing and design rigour a service deserves. ### Smell 6 — the cycle Subsystem classes import the facade or call back into it. This is definitionally not Facade any more (it is closer to Mediator), it breaks the one-way dependency rule, and it means the subsystem cannot be reused or tested without its own wrapper. Fix by inverting: pass what the subsystem needs as parameters or callbacks, or move the coordinating logic entirely into the facade. ### A pragmatic evaluation checklist 1. How many subsystem types does a typical caller name with the facade vs without? (Must drop.) 2. What is the ratio of facade methods to subsystem methods? (Near 1:1 → suspicious.) 3. Do facade signatures mention internal types? (Should not, at real boundaries.) 4. What fraction of call sites use it? (Low → wrong level or dead.) 5. Does anything in the subsystem reference the facade? (Must be zero.) 6. Is it growing flags/overloads faster than callers? (Split it.) ### The uncomfortable conclusion Sometimes the right action is deletion. A facade that is not reducing caller coupling or encapsulating a real sequence is negative-value code, and 'we might need the indirection later' is not a justification — the indirection can be reintroduced when a second caller or a real variation appears.
- Is a thin pass-through facade ever justified?Yes — when its value is the visibility boundary rather than simplification. Making one type public so the rest of a module can be package-private/internal is a real benefit, and architecture tests can enforce it. Just say that is the reason, because it changes how you would evaluate it.
- How would you decide where to split a god facade?By client and job, not by data shape. List the callers, group them by the task they are trying to accomplish, and give each group a narrow facade over the same subsystem. That follows Interface Segregation and keeps each facade cohesive; splitting by entity tends to reproduce the same flat surface.
- What is wrong with a facade that throws the underlying library's exception types?It leaks the implementation into the contract. Callers write catch blocks against that library, so replacing it becomes a breaking change even though the facade's method signatures look stable. Translate into error types owned by the facade's own module.
saying these in an interview costs you the question
- Treating 'every module must have a facade' as a rule; a facade over a trivial or single-collaborator subsystem is pure indirection.
- Responding to a growing facade by adding parameters and overloads rather than splitting it.
- Claiming coupling is reduced while the facade's signatures expose internal entities or library exception types.
- Assuming a facade is harmless when unused — it is a second API to maintain, test, document and version.
- Allowing subsystem classes to call back into the facade and still calling the result a facade.