skip to content

A facade covers the common cases, but a caller now needs a lower-level capability the facade does not expose. What are your options, and what does each cost?

level: seniorimportance: must knowfreq 58%

answer

  1. Transparent vs opaque vs escape hatch
  2. Opaque → bottleneck + flag/overload accretion
  3. Hatch is greppable demand data
  4. Blast radius decides: intra-team vs published API
  5. Prefer several narrow facades over one wide one

basics

~20 s

Either leave the subsystem reachable so that caller uses it directly (flexible, weaker boundary), or add the capability to the facade (keeps the boundary, but the facade grows). A middle option is a deliberate escape hatch that exposes the underlying object.

solid answer

~50 s

This is Facade's central trade-off: simplicity for the common case versus continued access to full capability. Three strategies. **Transparent facade** — subsystem stays public; the advanced caller drops down. GoF explicitly sanction this; cost is a weak boundary, callers coupled to internals, and refactoring freedom lost for whoever took the shortcut. **Opaque facade** — subsystem is hidden (package-private, `internal`, module-private); every new need must go through the facade owner. Strongest contract, but the facade becomes a queue: parameters, overloads and flags accrete until it is as complex as what it hid. **Escape hatch** — mostly opaque, but with a documented lower-level accessor or an options/config object (e.g. an HTTP client facade exposing `rawRequest()`). Pragmatic, and honest that the abstraction is leaky; the risk is that the hatch becomes the main road. The choice depends on blast radius: cross-team or published APIs favour opaque with a versioned hatch; intra-team code favours transparent. Also consider multiple narrow facades per client group instead of widening one.

code

typescript · 13 lines
typescript
// Escape hatch: 95% path is simple, 5% path stays reachable and greppable.
class HttpFacade {
  constructor(private readonly raw: LowLevelClient) {}

  get(url: string): Promise<Response> {           // common case, opinionated defaults
    return this.raw.send({ method: "GET", url, timeoutMs: 5000, retries: 2 });
  }

  /** ADVANCED / UNSTABLE: shape follows the underlying client and may change. */
  underlying(): LowLevelClient {                  // deliberate, documented hatch
    return this.raw;
  }
}

go deeper

for a junior

Say that the subsystem can usually still be called directly, and that the facade is a convenience for the common case.

for a middle

Name the three options and give one concrete pro and con for each.

for a senior

Decide by blast radius and ownership, mention enforcement tooling, and describe the accretion failure mode of opaque facades.

for a principal

Discuss it as API strategy: versioning and compatibility of the hatch, two-layer SDK design, organisational bottlenecks, and using hatch telemetry to drive the roadmap for the high-level API.

### Restating the tension A facade's value comes from *removing choices*. It picks defaults, an ordering, and a level of granularity. Every removed choice is a caller who someday needs it back. The pattern therefore has a built-in pressure valve question: **when the facade is not enough, what happens?** GoF's original text already answers it: clients that need more customisation can *pass through* the facade to the subsystem. That is a deliberate design property, not an oversight. But in modern modular codebases (enforced module boundaries, published SDKs, service APIs) 'just call the internals' is often impossible or undesirable. So there are really three positions on a spectrum. ### 1. Transparent facade (subsystem public) The facade is pure convenience; the subsystem's classes remain importable. - **Pros:** zero ceiling on capability; no bottleneck on the facade owner; the facade can stay genuinely small because it never has to cover the tail; adding a facade is a non-breaking, opt-in change. - **Cons:** the boundary is advisory. Callers that drop down re-acquire the coupling the facade was meant to remove, and now the subsystem cannot be refactored freely. Over time you get 'boundary erosion': the shortcut spreads by copy-paste. There is no signal in the type system telling you which callers are on the supported path. - **Mitigation:** architecture tests (ArchUnit, Spring Modulith `allowedDependencies`, dependency-cruiser, ESLint `no-restricted-imports`) that *allow* direct access only from an explicit allow-list; deprecation annotations; lint warnings rather than hard bans. ### 2. Opaque facade (subsystem hidden) Internals are package-private / `internal` / not exported / behind a module descriptor. The facade is the only door. - **Pros:** a real contract. You can refactor, replace, or re-implement everything behind it. Callers cannot depend on accidents. This is what makes modular monoliths and published libraries evolvable. - **Cons — and this is the important part:** the facade becomes a **bottleneck and an accretion point**. Every unanticipated need turns into a change request against the facade's owner. The typical decay path is: add a boolean flag → add an overload → add an options object → add 'advanced' variants of half the methods. Eventually the facade has the complexity of the subsystem plus its own, and you have paid twice. It also creates organisational coupling: a team blocked on another team's facade PR. - **Mitigation:** version the facade properly; prefer *adding a second narrow facade* for a new client group over widening the existing one; keep an explicit deprecation policy. ### 3. Escape hatch (the pragmatic middle) Mostly opaque, plus one deliberate, documented way down: `client.underlying()`, `executeRaw(...)`, an interceptor/plugin hook, or a fully-typed options object that the facade forwards. Examples of this in practice: an HTTP-client facade exposing the raw request builder; an ORM exposing native-SQL execution; a cloud SDK exposing a low-level operation client under the high-level one; a build tool exposing a raw task hook. - **Pros:** the 95% path is clean and the 5% path is unblocked without a PR to the platform team. Usage of the hatch is *greppable*, so you can measure demand and later promote a real method for the popular cases. - **Cons:** it is an admitted leak. Anything reachable through the hatch is now, de facto, part of your compatibility surface — you cannot silently swap the underlying implementation any more. And if the facade is too thin, the hatch becomes the default and the facade becomes decoration. - **Discipline:** mark the hatch as unstable/advanced, document that its return type may change with the underlying library, and periodically review hatch usage as a backlog of missing facade features. ### Choosing Decide by **blast radius and ownership**, not by taste: - *Same team, same repo, easy to change all callers* → transparent is fine; the cost of erosion is low and refactors are cheap. - *Cross-team internal module* → opaque with architecture tests, plus an escape hatch to avoid becoming a bottleneck. - *Published library / external SDK / public service API* → opaque, semver'd, deliberate and documented low-level layer (many SDKs literally ship two layers: high-level convenience and a generated low-level client). ### Two additional moves people forget - **Multiple facades, not one wider one.** Different client groups (checkout, reporting, admin) get their own narrow facade over the same subsystem. Keeps each Interface-Segregation-clean and stops god-object growth. - **Facade returns capability objects.** Instead of 40 flat methods, the facade returns small, focused sub-objects (`api.orders()`, `api.refunds()`), preserving one entry point while distributing the surface. ### Smells that you chose wrong - Facade methods with long parameter lists and boolean flags → opaque facade under pressure; consider hatch or sub-facades. - Most callers import internals anyway → transparent facade with no real value; either strengthen or delete it. - The hatch appears in more call sites than the facade methods → the abstraction is at the wrong level; re-derive it from actual usage.

  • How do you stop an escape hatch from becoming the default path?
    Measure it. Because the hatch is a single named method, you can grep or instrument call sites; recurring uses are a backlog of missing facade features. Combine that with naming/annotating it as advanced or unstable, and with a documented no-compatibility-guarantee on what it returns.
  • If the facade is the only door and it keeps growing flags and overloads, what would you do?
    Split it. Add a second narrow facade aimed at the client group driving the growth, or have the facade return focused capability objects rather than accumulating flat methods. Widening one facade destroys the cohesion that justified it.
  • Is making subsystem classes package-private enough to enforce an opaque facade?
    Within one package/module, yes for compile-time access, but real systems leak via reflection, serialization, DI containers and multi-package subsystems. Practical enforcement adds architecture tests (ArchUnit, Spring Modulith allowedDependencies, dependency-cruiser/ESLint import rules) that fail the build on forbidden imports.

A hotel concierge who can book anything standard. Transparent = the guest may also phone the restaurant directly. Opaque = the phone lines are cut, so every unusual request queues at the desk. Escape hatch = the desk hands you the restaurant's direct number when you ask, and notes how often that happens.

saying these in an interview costs you the question

  • Insisting a facade must fully hide the subsystem — GoF explicitly permit clients to bypass it; hiding is a separate, deliberate boundary decision.
  • Treating 'add another parameter to the facade' as the default response to every new requirement; that is how facades become god-objects.
  • Assuming an escape hatch is always bad practice — many mature SDKs ship an explicit low-level layer precisely to avoid becoming a bottleneck.
  • Believing a transparent facade still gives you refactoring freedom over the subsystem; once callers import internals, it does not.
  • Forgetting that whatever the hatch returns becomes part of your compatibility surface.

context