Facade
A small interface over a subsystem whose full surface most callers do not need. Useful when the subsystem is genuinely complex and dangerous when the facade becomes the only way in.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Six callers use four of my subsystem's twenty types to do the same three-step thing. Should there be one entry point?
Publishing a document means validating it, rendering three formats, uploading to storage, invalidating a CDN path and writing an index entry. Five callers do all five steps, in the same order, and two of them forget the index entry.
Let callers use the subsystem. Everything is public, the steps are documented, and a wrapper would just hide what is going on.
The sequence is knowledge, and it is duplicated five times. Two callers already have it wrong, which is exactly what duplicated knowledge produces (Duplicate Knowledge).
- The sequence is knowledge, and it is duplicated five times. Two callers already have it wrong, which is exactly what duplicated knowledge produces (Duplicate Knowledge).
- Every caller now depends on five subsystem types, so any change inside the subsystem is a change to five modules in three teams (Change Amplification).
- Adding a step — a search-index warm-up — means finding and editing five call sites, with no compiler help and no list (Shotgun Surgery).
- The subsystem cannot be refactored: its internal types are load-bearing across the codebase, so what was meant to be an implementation detail is now a contract (Stable Boundaries).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The subsystem's parts are used independently elsewhere — the renderer is called by the preview endpoint, storage by the import job (Module Granularity).
- The order of steps matters and the fourth can fail without the first three needing to be undone (Partial Failure).
- Three of the five callers are in other teams' modules (Code Ownership).
- A document that is publicly reachable is always in the index. There is no published-but-unindexed state (Consistency Boundaries).
- Publishing twice with the same document version produces the same result and no duplicate index entries (Idempotency by Design).
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- The facade owns the *sequence* and the invariant it protects — nothing else. It does not own validation, rendering or storage; those already have owners.
- The subsystem parts keep their own responsibilities and stay individually usable, because other callers legitimately need them.
- Callers own deciding to publish, and nothing about how (Information Hiding).
- The facade is a boundary for the common path only. It deliberately does not close the subsystem, because closing it would break the preview endpoint and the import job.
- What sits behind the facade should be the parts whose *order and combination* is the knowledge; parts with independent value stay reachable (Exposing Too Much).
- The facade's signature is the contract other teams depend on, so it must be expressed in publishing vocabulary — a document and a version in, a result out — and never in subsystem types (Interface Versus Implementation).
What the facade actually owns
The temptation is to describe a facade as "a simpler interface". That is what it looks like, not what it is for. What it holds is a specific piece of knowledge — the order of the steps and the invariant that must hold at the end — which was previously copied into five places and was wrong in two.
Reading its responsibility profile is the check on whether it stays healthy. One reason to change is a facade; five reasons to change means every caller's special case has moved inside it (Divergent Change).
- — The order of the five steps
- — That a published document must be indexed
- — What partial failure at step four means
- — Runs the sequence
- — Enforces the index invariant
- — Reports which step failed
- — Validator
- — Renderer
- — Storage
- — CdnClient
- — SearchIndex
- — A step is added or reordered
- — The meaning of partial failure changes
Two reasons to change, and both are genuinely about the sequence rather than about any one step. That is a healthy facade. The failure to watch for is the third and fourth reason arriving as parameters — publish(doc, version, skipCdn, indexAsync) — at which point it has become a caller-shaped switchboard and each caller's needs should get their own named entry point instead (Boolean Parameters).
The line between a facade and a god object
They are structurally identical from the outside: one type that many callers use to do many things. The difference is entirely in what is inside — whether it holds a sequence and delegates, or whether it holds the logic itself.
The practical test is deletion: could you delete the facade and have callers do the steps themselves, painfully but possible? If the answer is no because the logic only exists in there, it is not a facade any more.
class DocumentService {
publish(doc) {
if (!doc.title || doc.title.length > 200) throw ... // validation lives here now
const html = this.renderHtml(doc) // and rendering
const pdf = this.renderPdf(doc) // and this
const key = `docs/${doc.id}/${doc.version}.pdf` // and the storage layout
...
}
renderHtml(doc) { /* 80 lines */ }
renderPdf(doc) { /* 120 lines */ }
// + 18 more methods, added one per caller request
}async function publish(doc: Document, v: Version): Promise<PublishResult> {
const valid = validator.check(doc)
if (!valid.ok) return PublishResult.invalid(valid.errors)
const rendered = await renderer.renderAll(doc) // owns rendering
const stored = await storage.put(rendered, v) // owns layout
const cdn = await cdn.invalidate(stored.paths) // may fail on its own
await index.upsert(doc, v, stored) // the invariant
return PublishResult.published(stored, cdn)
}The second version can be deleted and the five callers can still do the work — painfully, by calling the parts — because the knowledge inside it is only the order and the invariant. The first version cannot: rendering exists nowhere else, so every caller is now permanently coupled to DocumentService, and the preview endpoint that only wants HTML has to go through publishing to get it. The distinction is not size and it is not method count; it is whether the facade delegates or owns (Cohesion).
How facades decay
Every row below starts as a reasonable request from a real caller. That is what makes the decay hard to stop in review — no single addition is wrong, and the twentieth one is a different design from the first.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| "Can publish skip the CDN step for internal docs?" | A boolean parameter appears | Two different operations sharing one name | A second named function — publishInternal — sharing the same private sequence helper (Boolean Parameters). |
| "I need the rendered HTML from publish" | The result type grows a subsystem type | A caller needs a step's output, not the sequence | Let that caller call the renderer directly. The subsystem is public on purpose (Leaky Abstractions). |
| "Publishing is slow; can it be async?" | The facade grows a queue and a job | An operational concern landing on a structural boundary | Enqueue at the caller, or a separate schedulePublish. A facade that owns a queue owns a lifecycle (Background Jobs and Workers). |
| "Just add the validation here, it is two lines" | Logic starts living in the facade | The facade is the convenient place, so it becomes the default place | Push it to the part that owns it, even at two lines, because the tenth two-line addition is the god object (God Object). |
| "Everything should go through the service" | The subsystem is made private | Facade confused with encapsulation | Close a module when its internals genuinely have no independent consumers, which is a separate decision with separate evidence (Designing a Module Interface). |
How to build it
Most important first.
- Write the facade as the sequence, in one function, with the invariant enforced at the end: index entry written or the whole thing reports failure.
- Keep the subsystem public. A facade that forces every access through it is not a facade, it is a new god object with a friendly name (God Object).
- Return a result that says what happened at each step, because a five-step operation that returns
voidis undebuggable in production (Debuggability by Design). - Handle partial failure explicitly: decide, in the facade, what "uploaded but CDN invalidation failed" means and encode it, rather than letting each caller invent an answer (Partial Failure).
- Resist adding parameters for each caller's variation. Two callers wanting different behaviour is the signal for two facade functions, not one with a flag (Boolean Parameters).
- Delete the facade if it ends up with one caller. A sequence used once is a function in that caller (Over-Decomposition).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Named change — "add a search-index warm-up step": with the facade, one function, and all five callers get it; without, five edits and the two that were already wrong stay wrong. This is the change the facade is for.
- Named change — "the storage client's API changes": with the facade, one file; without, five modules across three teams, coordinated across sprints.
- Named change — "one caller must publish without CDN invalidation": the facade makes this *more* expensive, because the natural move is a boolean parameter and that is a permanent worsening. The right answer is a second entry point, which costs more than editing one caller would have (Boolean Parameters).
- A facade that wraps a subsystem which is not actually complex — three calls in any order, no invariant — makes nothing cheaper. That is the test: name the sequence knowledge it holds, or do not build it.
- You now have two ways to use the subsystem, and a reader has to know which one is intended.
- The facade hides the steps, so a caller debugging a publish must open one more file — and if the facade swallows step-level detail, several (Swallowed Errors).
- Keeping the subsystem public means the facade's benefit is voluntary; someone under deadline will call past it, and the invariant it protects is only as strong as review (What to Automate Out of Review).
What can go wrong
- Facade rot: it grows a method per caller, each a slight variation, until it is a twenty-method class that is harder to understand than the subsystem it covers (God Object).
- Leaky facade: a method returns a subsystem type, so callers depend on the internals anyway and the facade only added a hop (Leaky Abstractions).
- Pass-through facade: every method forwards one call unchanged. There is no sequence, no invariant and no knowledge in it — just a file (Pattern Overuse).
- The mitigation fails too: making the subsystem private to force facade use breaks the two legitimate independent callers, who then copy the code they can no longer call (The Common Module).
- The facade depends on every subsystem part — high, deliberate fan-out concentrated in one place that is easy to find and easy to test (Fan-in and Fan-out).
- Callers depend on the facade and, ideally, on nothing else in the subsystem. That reduction from five dependencies to one is the measurable benefit.
- The subsystem must not depend on the facade. If a renderer calls back into publishing, the direction has inverted and a cycle is next (Dependency Cycles).
- "A facade simplifies the subsystem." It hides it for one use case. The subsystem is exactly as complex as before, and someone still maintains it (Essential and Accidental Complexity).
- "Every module should have a facade." A module whose interface is already small has nothing to hide behind one, and adding a layer per module is how a codebase gets four hops between the request and the work (Pattern Overuse).
- "The facade should be the only way in." That is a different decision — closing the module — and it breaks legitimate independent callers. Facade and encapsulation are related and not the same (Encapsulation).
- "It is the same as an adapter." An adapter translates a vocabulary you do not control; a facade simplifies access to one you do. Different problems, and the adapter is much more often the right one (Adapter).
- god-object
- shotgun-surgery
Testing it, and how it ages
- Test the facade as a unit with fakes for the subsystem parts, asserting the *sequence-level invariant*: a successful publish is always indexed.
- Test partial failure explicitly — storage succeeds, CDN fails — because that is the case the five callers each invented an answer to (Failure-Aware Feature Design).
- Do not duplicate subsystem tests through the facade. Rendering is tested where rendering lives (What a Unit Is).
- One integration test of the real sequence, because ordering bugs do not appear against fakes (Where a Test Must Be Real).
- Facades tend to become the module's public interface over time, which is fine if the module's boundary genuinely is the facade and bad if the subsystem still has independent consumers (Designing a Module Interface).
- The healthy growth path is more facade functions with distinct names, not more parameters on one.
- A facade over a subsystem you own often reveals that the subsystem was decomposed wrongly — if the sequence is always the same, the parts might want to be one part (Over-Decomposition).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat a repeated multi-step sequence with an invariant deserves one home is language-independent; the facade is just a function with a good name in most codebases, and the class formulation is a Java-era habit.
- SCALE-SPECIFICWith two callers in one module, the sequence is a private function and a facade is over-structure. It starts paying at the point where callers live in modules owned by other people, because the cost being avoided is coordination rather than typing (Code Ownership).
- CONTESTEDA defensible opposing view holds that facades are usually a symptom rather than a fix: if five callers all run the same five steps, the subsystem was decomposed along the wrong lines and should be one module with one operation, not five modules plus a wrapper. That argument is right when the parts have no independent consumers — and wrong here, because the renderer and the storage client genuinely do (Module Granularity).
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — the same shape as an API gateway or backend-for-frontend one level up, with the same decay mode: it starts as a route table and ends holding business logic nobody meant to put there.