Skip to content

9.4.9 — Facade

What the original Gang of Four book says: Provide one unified interface to a set of interfaces in a subsystem. A Facade gives the subsystem a higher-level interface that makes it easier to use.

What that means when you are actually writing code: When getting one job done means driving five libraries in the right order with the right error handling, put that whole dance behind a single method — so callers state the goal, not the steps.

Facade is the most quietly common pattern in application code. Almost every "service" class you have ever written is a facade. Its value is not cleverness — it is removing cleverness from where it does not belong, so that ninety percent of callers can say what they want in one line, and the ten percent who need the raw subsystem can still reach it.

1. The story: the eight-step ritual copied into every controller

You build a feature: upload a video, transcode it, generate a thumbnail, store both, and record it in the database. The honest sequence touches five subsystems:

typescript
// inside the upload controller — the raw ritual
const raw = await s3.putObject({ Bucket: "in", Key: id, Body: buf }).promise();
const job = await ffmpeg.transcode({ input: `s3://in/${id}`, presets: ["720p", "1080p"] });
await waitForJob(job.id);                                    // poll until done
const thumb = await thumbnailer.grab(`s3://in/${id}`, { at: "00:00:03" });
const outUrl = await s3.putObject({ Bucket: "out", Key: `${id}.mp4`, Body: job.output }).promise();
const thumbUrl = await s3.putObject({ Bucket: "out", Key: `${id}.jpg`, Body: thumb }).promise();
await db.videos.insert({ id, outUrl, thumbUrl, status: "ready", owner });
await cdn.invalidate([`/videos/${id}`]);

Then the same eight steps get copied — with subtle variations and drift — into the mobile upload path, the admin re-encode tool, the bulk importer, and three tests. Now here is what goes wrong.

The order and the error handling are duplicated across five places, and they diverge: one path forgets the CDN invalidation, another polls with a different timeout, a third does not clean up the input object on failure. Each divergence is a bug.

Every caller is coupled to five subsystemss3, ffmpeg, the thumbnailer, db, cdn. A change to any one of them (a new ffmpeg option, an S3 SDK upgrade) ripples into every controller.

The controllers are unreadable. A reader who wants to know "what does upload do?" has to parse a transcoding pipeline to find the answer, which is "it makes a video available."

Testing a controller requires standing up five mocks in the right sequence — so nobody does, and the paths are under-tested.

The knowledge that should be one thing — "publish a video" — is scattered as an eight-step ritual everywhere it is needed. Facade collects it into one place with one honest method.

typescript
const video = await videoService.publish({ file: buf, owner, id });   // ← the whole ritual, one call

2. How you arrive at the pattern

Step 1 — Start naive. Call each subsystem directly at the point of use. This is correct when the task is genuinely one call, or when the caller legitimately needs fine control over each step.

Step 2 — Wait for the force. Accomplishing one conceptual operation requires driving several components in a specific order, with shared error handling, cleanup, and sequencing — and that orchestration is needed in more than one place, or is complex enough that callers should not have to know it.

Step 3 — Draw the line between what varies and what stays fixed.

What variesthe internal steps — which subsystems, in what order, how errors and cleanup are handled
What stays fixedthe goal the caller cares about — "publish a video", stated once as a method

The fixed goal becomes the facade's interface — an interface you invent for the caller's convenience. The varying steps hide behind it. Note that this "invented interface" is the key difference from Adapter, which must match a pre-existing required shape (9.4.7).

Step 4 — Decide when the choice is made. Composition at construction: the facade holds references to the subsystems it orchestrates (ideally injected, 9.4.6), and callers hold the facade.

Step 5 — Name the pattern and say what it costs. The name is Facade. The costs: it can become a god object if it collects every operation in the system (section 10); it hides power, so a caller with a legitimate advanced need may find the simple interface insufficient (mitigation: don't seal off the subsystem — section 7); and it adds one layer. The benefit dominates for the common case: callers state intent, the orchestration lives once, and the subsystem can be restructured behind the facade without touching callers.

3. The mental model

In one sentence: a facade is the front desk of a hotel — you say "I'd like to check in," and one person handles housekeeping, billing, and keycards behind the counter; you never speak to those departments directly, but they are still there if you truly need them.

The analogy that makes it stick — the car ignition. Turning the key (or pressing Start) triggers a precise sequence: engage the starter motor, fuel the injectors, fire the ignition coils, monitor the sensors. The driver expresses one intent — "start" — and a facade drives a dozen subsystems in order. A mechanic can still open the hood and touch each subsystem directly; the facade simplifies the common interaction without removing the expert path.

When to reach for it. The signals: "this one operation touches five different services or libraries" · "we keep copying this sequence of calls" · "callers shouldn't need to know about all these internal components" · "give me a simple front for this complicated subsystem" · "I want a PaymentService so controllers don't talk to Stripe, the ledger, and the fraud checker directly." At architecture scale: "one API gateway in front of twelve microservices."

The distinction that matters most — Facade versus Adapter (again, because it is the most confused pair). Adapter changes an interface to match one the client already requires — the target shape pre-exists and is dictated by the client's needs. Facade invents a new, simpler interface for a subsystem where no such target existed — you designed it for convenience. The test: did the interface exist before I wrote the wrapper? If yes (I'm making X fit that shape) → Adapter. If no (I'm inventing a simpler front) → Facade. A secondary tell: Adapter usually wraps one adaptee; Facade usually orchestrates several subsystems.

4. Structure

controllerbulk importeradmin toolVideoService (facade)publish()one intent, orchestratedS3 storageffmpeg transcoderthumbnailerdatabaseCDNexpert path stays open (Facade doesn't seal the subsystem)
Figure 9 — One front, many components. Every client (blue) depends only on the facade (green), which drives the subsystems (grey) in the right order with shared error handling. The dashed purple arc is the honest detail most tutorials omit: a facade simplifies access without forbidding it — an advanced caller can still reach a subsystem directly when it genuinely needs to.

The participants: the Facade (the unified interface, does the orchestration), the Subsystem classes (the components doing the real work — they do not know the facade exists), and the Client (uses the facade instead of the subsystems).

5. The implementation, line by line

typescript
export class VideoService {                              // the facade
  constructor(                                           // (1) subsystems INJECTED, not new-ed
    private readonly storage: BlobStore,
    private readonly transcoder: Transcoder,             //     each is itself a port ([9.4.7])
    private readonly thumbs: Thumbnailer,
    private readonly repo: VideoRepository,
    private readonly cdn: Cdn,
    private readonly log: Logger,
  ) {}

  async publish(input: PublishInput): Promise<Video> {   // (2) ONE intent, honest name
    const { id, owner, file } = input;
    await this.storage.put(`in/${id}`, file);            // (3) the orchestration lives HERE, once
    try {
      const outputs = await this.transcoder.run(`in/${id}`, ["720p", "1080p"]);   // (4) sequencing
      const [thumb] = await this.thumbs.grab(`in/${id}`, { at: "00:00:03" });
      const outUrl   = await this.storage.put(`out/${id}.mp4`, outputs["1080p"]);
      const thumbUrl = await this.storage.put(`out/${id}.jpg`, thumb);
      const video = await this.repo.insert({ id, owner, outUrl, thumbUrl, status: "ready" });
      await this.cdn.invalidate([`/videos/${id}`]);
      this.log.info({ id, owner }, "video published");
      return video;
    } catch (e) {
      await this.storage.delete(`in/${id}`).catch(() => {});  // (5) cleanup on failure, in ONE place
      this.log.error({ id, err: e }, "publish failed");
      throw new PublishError(id, { cause: e });          // (6) subsystem errors → one domain error
    }
  }
}

Now the numbered lines.

(1) Subsystems are injected, not constructed inside. This is what keeps the facade testable and swappable — and note that each subsystem is ideally itself a port/adapter (9.4.7), so the facade orchestrates roles, not vendor SDKs. A facade over raw SDKs works, but a facade over adapters is the composition professionals reach for.

(2) The method names the goal, not the mechanism. publish, not putTranscodeThumbnailStoreRecordInvalidate. The name is the abstraction — if you cannot name the operation cleanly, the facade may be doing too many unrelated things (section 10, the god-object smell).

(3) Orchestration knowledge lives once. The order — store input → transcode → thumbnail → store outputs → record → invalidate — exists in exactly one place. Change the order or add a step (say, virus-scan the input), and one method changes, not five controllers.

(4) Sequencing and dependencies are the facade's real job. The thumbnail needs the input stored; the DB record needs the output URLs; the CDN invalidation needs the record. Getting these dependencies right, once, is exactly the value — every copy-pasted version was a chance to get them wrong.

(5) Cleanup and compensation live in one place. On failure, the input object is deleted so a failed publish does not leak storage. Doing this once is the difference between a system that self-heals and one that accumulates orphaned objects — which was the reality of the five scattered copies.

(6) Subsystem errors are translated to a domain error. Callers catch PublishError, not S3ServiceException or FfmpegError. This is the same error-translation discipline as Adapter — the facade shields callers from the subsystems' error vocabularies too.

What this does when you run it: videoService.publish({ file, owner, id }) runs the whole pipeline, returns a Video, cleans up on failure, logs once, and throws exactly one error type. Every caller — the controller, the importer, the admin tool — is now one line and depends on one thing.

5.1 The expert path stays open

A facade simplifies without imprisoning. If the admin re-encode tool needs to transcode with unusual presets that publish() does not expose, it should still be able to use the transcoder directly — the subsystems stay public. The mistake is making a facade the only door and then bloating it with every advanced option to compensate (section 10). The rule: facade for the common ninety percent, direct subsystem access for the expert ten percent.

typescript
// common path: the facade
await videoService.publish({ file, owner, id });
// expert path: reach past it deliberately, for a genuine one-off need
const custom = await transcoder.run(`in/${id}`, ["4k", "hdr"], { crf: 18, twoPass: true });

5.2 The functional facade

A facade need not be a class — for a stateless orchestration, a function that closes over its dependencies is the lighter spelling:

typescript
export const makeVideoService = (deps: Deps) => ({
  publish: (input: PublishInput) => publishImpl(deps, input),
  unpublish: (id: VideoId) => unpublishImpl(deps, id),
});

This is the composition-root-friendly form (9.4.6) and often what "service" means in a functional-core codebase.

6. Five domains, the same shape

(a) The service layer — the everyday facade. OrderService, PaymentService, AuthService — the classes between your controllers and your infrastructure are facades. PaymentService.charge() drives the gateway, the ledger, the fraud checker, and the receipt emailer; controllers call one method. This is the reason "service" is the most common class suffix in backend code, and recognising services as facades tells you how to keep them healthy: one cohesive area per service, orchestration inside, no god object.

(b) The API gateway — Facade at architecture scale. One public API in front of a dozen microservices (10.8.3): the client makes one call, and the gateway fans out, aggregates, and shields the client from the internal topology. It is literally "a Facade with an IP address," and the Backend-for-Frontend is the same pattern specialised per client type.

(c) An SDK's top-level object. new Stripe(key) presents charges, customers, webhooks — a facade over dozens of internal HTTP endpoints, retry logic, and pagination. Good libraries are facades; their whole job is to make a complex remote subsystem feel like a few method calls.

(d) A library wrapper for your domain. MediaConverter.toMp4(input) hiding ffmpeg's hundreds of flags; Mailer.send(msg) hiding MIME construction, SMTP, and retries; Search.query(q) hiding OpenSearch's DSL. Each turns an intimidating subsystem into one intent-shaped method for your team.

(e) Simplifying your own legacy subsystem. Even code you own can become subsystem-shaped after years of growth. A facade over a tangled legacy module gives new code a clean entry point and a seam behind which you can refactor the legacy internals incrementally — a facade is often the first move in a strangler-fig migration.

7. Variants

VariantShapeNotes
Class facadea service class holding subsystemsthe default; the "service layer"
Functional facadea function or object closing over depsfunctional-core codebases
Module facadea module's public exports over private internalsindex.ts re-exporting a curated surface
API gateway / BFFa network service fronting othersFacade at architecture scale
Opaque vs transparentsubsystem hidden vs still reachableprefer transparent (expert path open)
Session facadeone facade per use case or workflowkeeps facades cohesive, avoids the god object

Module-as-facade is worth its own mention, because you use it constantly: a package's index.ts that re-exports a small, curated public surface while keeping the internal files private is a facade over the module's structure — callers import from one place and the internals can be reorganised freely (3.6.5). This is the cheapest facade there is, and the one most teams under-use.

8. Where you already use it

What you have usedWhat it stands in front of
fetch(url)DNS, TCP, TLS, connection reuse, redirects, chunked decoding
Any OrderService class you have writtenrepositories, the payment client, the email sender
A package's index.ts with three exportsthirty private files behind it
JSON.parse(text)a full tokenizer and parser (3.1)

fetch is the clearest one, and it is worth counting what it saves you. One line — await fetch(url) — and behind it the browser looks up the hostname, opens a connection, negotiates encryption, possibly reuses a connection it already had, follows redirects, and reassembles a response that arrived in pieces. Every one of those is a real subsystem with its own failure modes, and none of them appear in your code. That is a deep facade: a tiny front door, an enormous amount of work behind it.

9. Ways to get it wrong

  1. The god object. A facade that collects every operation in the app (AppService with 80 methods touching everything) has as many reasons to change as the whole system (9.3.5, Single Responsibility).

    The fix: one facade per cohesive area or use case (session facades); split when the name stops being honest.

  2. Leaking subsystem types. publish() returning an S3.PutObjectOutput or throwing FfmpegError re-couples callers to the internals.

    The fix: translate to domain types and one domain error, like Adapter.

  3. Sealing off the subsystem. Making the facade the only access and then bloating it with every advanced flag to serve the ten-percent expert cases.

    The fix: keep the subsystems reachable; facade the common path only (section 5.1).

  4. A facade that only forwards. One method that just calls one subsystem method adds a layer with no orchestration. That is a pass-through, not a facade — delete it unless it exists to translate types (and then it's an Adapter).

  5. Business logic hidden in the facade. A facade orchestrates; when it starts making domain decisions (pricing rules, eligibility), those belong in domain objects the facade calls, not in the facade itself.

    The fix: facade coordinates, domain decides (9.2.3).

  6. The facade constructing its own subsystems. new S3Client() inside the facade makes it untestable and hard-codes lifetimes.

    The fix: inject them (9.4.6).

  7. Too many facades over facades. Layers of facades each forwarding to the next.

    The fix: a facade should collapse complexity, not add a floor to the tower.

10. Facade compared with its neighbours

Compared withThe differenceChoose Facade when
AdapterAdapter matches a required interface (one adaptee); Facade invents a simpler one (many subsystems)you're simplifying, not fitting a target
DecoratorDecorator keeps the same interface and adds behaviour; Facade presents a new, simpler interfaceyou want a simpler front, not a wrapped-same-shape
ProxyProxy keeps the same interface and controls access to one object; Facade fronts manythe goal is simplification over a group
Mediator (9.4.1 section 2)sits between components that talk to each othercallers come in from outside, one way
Abstract Factorya Factory creates a family; a Facade uses and orchestrates a subsystemthe concern is calling, not constructing

The last row is the one people muddle, so it is worth spelling out. Both a facade and a mediator cut down the number of things that have to know about each other, and both do it by putting one object in the middle. What differs is who is talking to whom.

A facade faces outward. Callers who live outside the group come in through it to get one job done, and the traffic only ever flows in that one direction: caller, then facade, then the parts inside.

A mediator faces inward. The objects it serves are peers who all live inside the same group, and the traffic flows both ways — a peer tells the mediator something happened, and the mediator turns round and tells other peers what to do about it. The peers never hold a reference to each other, only to the mediator.

So a facade makes a group easier to use from outside, and a mediator makes a group easier to wire up on the inside.

11. Interview calibration

The 45-second answer, in the order you would say it:

Facade puts a simple, intent-named interface in front of a complex subsystem — the several libraries or services you must orchestrate to get one job done. Instead of every caller copying the eight-step ritual and coupling to five components, they call videoService.publish() and the orchestration, sequencing, error handling, and cleanup live once behind it.

The interface is one you invent for the caller's convenience — that's the difference from Adapter, which has to match a pre-existing required shape. Most service classes are facades. The two things to keep right: don't let it become a god object — one facade per cohesive area — and don't seal off the subsystem, so the rare expert caller can still go direct.

Follow-up questions, with the seed of each answer:

  • "Facade versus Adapter?" — Facade invents a simpler interface over many components; Adapter matches a required interface over one. Simplify versus fit.
  • "Is a service class a Facade?" — Usually yes — it orchestrates infra and domain behind intent-named methods. Keep it cohesive.
  • "Facade versus Mediator?" — Facade is a one-way front for clients; Mediator coordinates peers talking to each other.
  • "How do you keep it from becoming a god object?" — Session facades: one per use case or area; split when the class name stops describing it.
  • "Does the facade hide the subsystem completely?" — No — it simplifies the common path; the subsystem stays reachable for expert needs.

Recall

  • Facade = one intent-named interface over a subsystem you must orchestrate. It collects a multi-step ritual (order, sequencing, error handling, cleanup) into one place so callers state the goal, not the steps — killing the duplication-and-drift of the same sequence copied across controllers.
  • Versus Adapter (the key distinction): Facade invents a simpler interface where none pre-existed (and usually fronts several subsystems); Adapter matches a required interface (usually one adaptee). Test: did the interface exist before the wrapper? Yes → Adapter; no → Facade.
  • Do it right: inject the subsystems (testable, swappable — and ideally they're ports/adapters so the facade orchestrates roles); name the method for the goal; put sequencing, cleanup/compensation, and error translation (subsystem errors → one domain error) in the one place; keep the expert path open (simplify without sealing off).
  • Everyday forms: the service layer (OrderService, PaymentService — most services are facades), the API gateway / BFF (Facade at architecture scale — "a Facade with an IP address"), an SDK's top-level object, a package's index.ts public surface (the cheapest, most under-used facade).
  • Misuse: the god object (collects everything — split into cohesive session facades) · leaking subsystem types or errors · sealing the subsystem then bloating the facade · a pure pass-through (no orchestration) · business logic in the facade (it coordinates; domain objects decide) · constructing its own subsystems.

Self-test: What is the one-test difference between Facade and Adapter? Name the four things that belong in the one place a facade centralises. Why keep the subsystem reachable? What makes a service class a facade, and what makes it a god object? Facade versus Mediator — which direction does each simplify?

Quiz Bank

FoundationalDerive Facade from scattered orchestration code and name everything it centralizes.

Naive: call each subsystem directly where needed — fine when the task is one call or the caller legitimately needs step-level control.

The force: one conceptual operation ("publish a video") requires driving several components (storage, transcoder, thumbnailer, database, CDN) in a specific order with shared error handling and cleanup — and that orchestration is needed in more than one place.

What breaks without the pattern: the multi-step sequence gets copied into every caller and drifts — one path forgets CDN invalidation, another uses a different timeout, a third leaks the input object on failure; every caller couples to five subsystems so any subsystem change ripples everywhere; controllers become unreadable because the intent ("make a video available") is buried in a transcoding pipeline; and testing a caller requires five sequenced mocks, so the paths go under-tested.

The varies/fixed line: the internal steps vary (which subsystems, order, error and cleanup handling); the goal is fixed ("publish").

The pattern: a facade exposing the goal as one method, with the orchestration behind it.

What it centralises, all in one place: first, the sequence and the inter-step dependencies (the thumbnail needs the stored input; the DB record needs the output URLs); second, error handling — subsystem exceptions translated to one domain error; third, cleanup and compensation on failure (delete the input object so a failed publish leaks nothing); fourth, logging and observability for the operation.

The cost: the risk of becoming a god object, one added layer, and hidden power (mitigated by keeping the subsystem reachable). Every caller becomes one line depending on one thing.

FoundationalGive the precise distinction between Facade and Adapter and a test to tell them apart.

Both wrap something and present an interface, and both keep callers from touching the wrapped thing directly — but they solve opposite-facing problems.

Adapter changes an interface to match one the client already requires. The target shape pre-exists and is dictated by the client's needs (your domain already expects a PaymentGateway, so StripeGateway is written to fit it); Adapter usually wraps a single adaptee; its purpose is compatibility.

Facade invents a new, simpler interface for a subsystem where no such target existed. You design videoService.publish() for convenience, choosing its shape freely; a facade usually orchestrates several subsystems; its purpose is simplification.

The test: ask did the interface exist before I wrote the wrapper? If a required shape already existed and you are making the foreign thing conform to it → Adapter. If you are inventing a simpler front because the raw subsystem is unpleasant to use → Facade.

A secondary tell: count the wrapped things — one foreign object suggests Adapter, several coordinated subsystems suggests Facade. A tertiary tell: an adapter's method roughly mirrors an adaptee method (translated), while a facade's method typically triggers many subsystem calls in sequence.

They also combine happily: a facade often orchestrates several adapters (the VideoService facade calling a BlobStore adapter, a Transcoder adapter, and so on), which is the professional arrangement — Adapter isolates each external dependency, and Facade orchestrates the isolated set.

AppliedWhy is 'keep the expert path open' a defining property of a good facade, and what goes wrong if you seal the subsystem?

A facade's value proposition is simplifying the common case — the ninety percent of callers who want "publish a video" with sensible defaults. But there is always a minority of legitimate advanced needs: an admin tool that must transcode with unusual presets, a debugging path that needs a single subsystem, a performance-critical caller that wants to skip a step. A good facade serves the common case without removing the ability to reach the subsystems directly — the subsystem classes stay public, and the expert caller uses them deliberately.

What goes wrong if you seal the subsystem (make the facade the only door): the facade must grow to accommodate every advanced need, because there is nowhere else to go. So publish() sprouts a dozen optional parameters — presets, crf, twoPass, skipThumbnail, customCdnPaths — each serving one rare caller, and the facade drifts toward a leaky, over-parameterised god method that is neither simple (the common case now navigates a huge signature) nor complete (it still cannot express everything the raw subsystem can). You have destroyed the facade's one job — simplification — in the name of totality.

The correct model is the car: the ignition simplifies starting, but a mechanic can still open the hood; the facade and the subsystem coexist. Concretely: keep publish() lean for the ninety percent, and let the ten percent call transcoder.run(...) directly for their one-off. The discipline is that reaching past the facade should be a deliberate, visible choice (so reviewers notice it and can ask "should this be a new facade method?"), not the default — but it must be possible, or the abstraction becomes a cage.

InterviewMost backend 'service' classes are facades. What does that tell you about how to design and maintain them, and when does a service stop being a good facade?

Recognising services as facades gives you a design checklist and a decay detector.

Design implications: first, a service should be named for a cohesive area or capability (PaymentService, OrderService), because a facade's method names are its abstraction — if you cannot name the service's responsibility in a few words, it is fronting an incoherent subsystem. Second, it should orchestrate, holding injected subsystems (repositories, adapters, other services) and coordinating them, while the actual domain decisions live in domain objects the service calls — a facade coordinates, domain objects decide (9.2.3). Third, it should translate infrastructure errors to domain errors and infrastructure types to domain types, shielding controllers exactly as a facade shields callers. Fourth, its subsystems should be injected so the service is testable by faking them, and the service's tests should assert orchestration (sequence, cleanup, error paths) rather than re-testing the subsystems.

When it stops being a good facade — the god-object decay: the tells are a service with dozens of methods spanning unrelated areas (a UserService that also does billing, notifications, and reporting), a constructor with fifteen injected dependencies, a class that changes for many unrelated reasons (9.3.9, Single Responsibility), and methods that no longer share a coherent theme. The fix is session facades — split by use case or bounded context so each facade fronts one cohesive subsystem, and let a workflow that spans areas be an explicit orchestration over several focused services rather than one mega-service.

The meta-point for an interview: "service layer" is not a magic word — a service is a facade, and it is healthy exactly as long as it stays a cohesive, orchestrating front, and unhealthy the moment it becomes a dumping ground.

StaffYour monolith has controllers calling repositories, third-party SDKs, and each other directly — 200 endpoints, no service layer, and every cross-cutting change (add audit logging to all writes, swap the email vendor) requires touching dozens of controllers. Design a facade-based service layer and a migration that delivers value incrementally.

The diagnosis is missing facades: business operations have no single home, so they are re-expressed in every controller, and cross-cutting changes have no seam to attach to. The target is a service layer where each cohesive capability is a facade orchestrating repositories, adapters, and domain objects, and controllers become thin (parse request → call one service method → format response).

Design. First, identify capabilities, not entities — group the 200 endpoints by business operation into cohesive services (OrderService, PaymentService, CatalogService, NotificationService), sized so each has a nameable responsibility and a modest set of dependencies; resist one giant AppService (a god object) and resist one facade per endpoint (no consolidation). Second, each service is a facade holding injected dependencies that are themselves ports/adapters (9.4.7) — so the email vendor is behind a Mailer port, and the SDKs behind adapters — which means "swap the email vendor" becomes one adapter, and "add audit logging to all writes" becomes a 9.4.8 decorator on the repository port applied once at the composition root. This is the payoff: cross-cutting changes attach to the seams the facade layer creates, instead of to dozens of controllers. Third, domain logic moves into domain objects the services call, so services orchestrate and domain objects decide — otherwise you have merely relocated the mess into fat services.

Migration — incremental, value-first, never a rewrite. First, start with the capability that has the most duplication or the most pending cross-cutting pain (if audit logging is the driver, start with the write-heavy operations). Second, extract one service behind a facade, move the orchestration out of the controllers that duplicate it, and point those controllers at the service — a bounded PR per capability, each independently shippable and independently valuable (the first extraction that removes a five-way duplication is a visible win that funds the rest). Third, as each service lands, its dependencies become ports — the first time PaymentService is extracted, Stripe goes behind an adapter, immediately enabling the vendor-swap and testability benefits for that capability. Fourth, cross-cutting concerns become decorators or middleware at the composition root once the ports exist — audit logging wraps the repository port, so every service that uses it gets auditing without change.

Fifth, ratchet it: a lint rule forbidding controllers from importing repositories or SDKs directly, with a shrinking allowlist of not-yet-migrated controllers, so the architecture can only improve.

What each step buys, measured: controllers touched per cross-cutting change drops toward one (the decorator) or one (the adapter); test setup per capability drops from N mocks to one faked service; and endpoints sharing an operation stop drifting.

The sentence for the plan: the monolith's pain is not that it is a monolith — it is that it has no service layer, so every business operation is scattered; we introduce facades one capability at a time, put their dependencies behind ports as we go, and cross-cutting changes stop being shotgun surgery because there is finally a seam to change them at.

Flashcards

FlashFacade in one line

One intent-named interface over a subsystem you must orchestrate. Callers state the goal; sequencing, error handling and cleanup live once behind it.

FlashFacade vs Adapter

Adapter matches a pre-existing required interface (one adaptee); Facade invents a simpler one (many subsystems). Test: did the interface exist before the wrapper?

FlashService classes

Most *Service classes are facades: orchestrate injected repos/adapters/domain, name methods for the goal, translate errors to domain types. Keep cohesive.

FlashKeep the expert path open

Facade simplifies the common 90% without sealing the subsystem. Sealing it forces the facade to bloat with every advanced option and become a god method.

FlashFacade at scale

API gateway / BFF = a Facade with an IP address, fronting many microservices. A package's index.ts is a facade over its private files.

FlashFacade misuse

God object (split into session facades) · leaking subsystem types/errors · pure pass-through · business logic in the facade · constructing its own subsystems.

Scenario Drill

DrillYou're building a checkout operation that must: validate the cart, reserve inventory, calculate tax, charge payment, create an order record, decrement inventory, send a confirmation email, and emit an analytics event — with correct rollback if any step fails after payment. Design this as a facade, decide what belongs in the facade versus elsewhere, and handle the partial-failure problem honestly.

Checkout is the textbook facade: one business intent orchestrating many subsystems in a strict order with real failure semantics — and the honest hard part is not the happy path but what happens when step 6 fails after step 4 took the customer's money.

The facade. CheckoutService.checkout(cart, payment, customer): Order holds injected ports — InventoryService, TaxCalculator, PaymentGateway (an adapter), OrderRepository, Mailer, EventBus — and orchestrates: validate cart → reserve inventory → calculate tax → charge payment → persist order → confirm inventory decrement → send email → emit analytics.

What belongs in the facade versus elsewhere — the design judgment. The sequence and the failure/compensation policy belong in the facade; that is its job. The decisions do not: cart validation rules live in the Cart domain object, tax logic lives in TaxCalculator, inventory-reservation rules live in InventoryService — the facade calls them and reacts, it does not embody them (or it becomes a god object with all the business logic, section 10). Email and analytics are side effects the facade triggers but must not let fail the operation — a confirmation email bouncing must not undo a paid order.

The partial-failure problem, handled honestly — this is where junior and senior answers diverge. The steps split into two phases at the payment boundary. Before payment (validate, reserve, tax), failures are cheap: release the inventory reservation and return an error; nothing irreversible happened.

After payment (persist order, decrement inventory, notify), a failure cannot be handled by naive rollback, because you cannot un-charge a card with a local try/catch — the money moved in an external system. So the facade uses compensation, not rollback (10.8.4's saga thinking at LLD scale): if order persistence fails after a successful charge, the compensating action is a refund (or, better, a retry of persistence first, since the charge succeeding while the DB write fails is a transient-looking condition worth one retry before refunding). Critically, the charge and the order record should be tied by an idempotency key so a retry of the whole checkout does not double-charge (9.6.3) — the facade generates it once per checkout attempt and passes it to the gateway.

The genuinely unrecoverable middle states (charged, refund also failing) must be made durable and visible: write a record to a reconciliation or dead-letter store and alert, because a payment system that silently loses track of a charged-but-unfulfilled order is a financial incident, not a bug.

What must be transactional versus eventual: the order record and the inventory decrement should be in one database transaction if they share a database (so they commit or fail together); the email and analytics are fire-after-commit side effects (emit them after the transaction commits, ideally via an outbox so they are not lost if the process dies between commit and send — 10.8.4).

Why a facade is exactly right here: this policy — the ordering, the payment boundary, the compensation, the idempotency, the outbox — is intricate and must be identical for every checkout entry point (web, mobile, one-click, admin), so it must live in exactly one place; scattered across controllers, the compensation logic would drift and some path would double-charge or leak reservations.

The sentence for the design review: checkout is a facade whose real content is its failure policy — the happy path is eight calls in order, but the value is that the payment boundary, compensation, idempotency, and post-commit side effects are decided once, correctly, where every checkout path shares them, because the alternative is a double-charge bug waiting in whichever controller someone copies next.