Definition
A facade provides one unified interface to a set of interfaces in a subsystem: a higher-level interface that makes the subsystem easier to use.
After Gamma et al., Design Patterns (1994), Facade
01Notice the recipe
A direct sequence is a fair start.
A single counter handler can validate a request, record the work, and print a slip. For one stable caller, those calls are easy to follow. This shop takes alterations in three places, and at first each one runs the recipe itself. In outline:
// The counter, the phone line and the partner shop each run the recipe.
function counterIntake(request: AlterationRequest) {
validator.check(request);
const order = { id: nextOrderId(), request };
book.record(order);
printer.print(order);
}
function phoneIntake(request: AlterationRequest) {
validator.check(request);
const order = { id: nextOrderId(), request };
book.record(order);
printer.print(order); // the slip waits at the counter for pickup
}
function partnerIntake(request: AlterationRequest) {
validator.check(request);
const order = { id: nextOrderId(), request };
book.record(order);
printer.print(order); // the slip goes back with the courier
} That works while the recipe stays put. Then the tailors get overbooked, and the shop adds a queue: every order must reserve a slot before it is recorded, and the order number now comes from the queue. Each of the three callers has to add the step, in the same place, and decide what to tell a customer when the queue is full. The next change means three edits again, and the caller that is missed drifts.
A facade gives callers one simpler interface for a useful task across a subsystem. Here, the desk’s acceptAlteration(request) owns the order of four collaborators. Validation, queue policy, record storage, and printing
remain with their specialists.
The caller chooses the request and presents the outcome. It can stop knowing the internal recipe; it still needs to know whether the alteration was accepted and whether a claim slip printed.
02Give the workflow one way in
Validate, reserve, record, print.
The desk takes one request and calls its four services in order: check it, reserve a queue number, record the work order, print the claim slip. It returns the order’s number and whether its slip printed. This is the whole program the lab runs: the services, the desk, and three callers, one of them with an incomplete request.
type AlterationRequest = { customer: string; garment: string; work: string };
type WorkOrder = { id: string; request: AlterationRequest };
type IntakeResult = { orderId: string; slipPrinted: boolean };
// The subsystem: four services, each with one job.
class Validator {
check(request: AlterationRequest): void {
const { customer, garment, work } = request;
if (!customer || !garment || !work) throw new Error('incomplete request');
}
}
class Queue {
private next = 1;
reserve(): string {
return `ALT-${this.next++}`;
}
}
class OrderBook {
orders: WorkOrder[] = [];
record(order: WorkOrder): void {
this.orders.push(order);
}
}
class Printer {
jammed = true; // the first slip jams; the next ones print
print(order: WorkOrder): void {
if (this.jammed) {
this.jammed = false;
throw new Error(`printer jammed on ${order.request.customer}'s slip`);
}
}
}
// The facade: one operation for the whole intake.
class IntakeDesk {
constructor(
private validator: Validator,
private queue: Queue,
private book: OrderBook,
private printer: Printer,
) {}
acceptAlteration(request: AlterationRequest): IntakeResult {
this.validator.check(request);
const order = { id: this.queue.reserve(), request };
this.book.record(order);
try {
this.printer.print(order);
return { orderId: order.id, slipPrinted: true };
} catch {
return { orderId: order.id, slipPrinted: false };
}
}
}
const book = new OrderBook();
const desk = new IntakeDesk(new Validator(), new Queue(), book, new Printer());
// Each caller makes one call and tells its customer what happened.
function take(caller: string, request: AlterationRequest): void {
try {
const result = desk.acceptAlteration(request);
const slip = result.slipPrinted ? 'slip printed' : 'no slip';
console.log(`${caller}: accepted ${result.orderId}, ${slip}`);
} catch (error) {
console.log(`${caller}: not accepted (${(error as Error).message})`);
}
}
take('counter', { customer: 'Mina', garment: 'blue coat', work: 'reline' });
take('phone', { customer: 'Omar', garment: 'grey suit', work: '' });
take('partner', { customer: 'Lena', garment: 'linen dress', work: 'hem' });
const recorded = book.orders.length;
console.log(`work orders recorded: ${recorded}`); | Role | Responsibility | What it handles |
|---|---|---|
| Caller | Knows the customer | Chooses the request and tells the customer what happened. |
| Facade (the desk) | Knows the recipe | Calls the services in order and returns the task’s outcome. |
| Subsystem (the services) | Knows its own step | What a complete request is, how numbers are handed out, where orders are kept, how a slip prints. |
03Predict what is left
The printer jams. What does the shop have?
Experiment
Three callers, one intake desk
// Lines 47–52, the try/catch around the printer, become two lines:
this.printer.print(order);
return { orderId: order.id, slipPrinted: true }; What a production facade addsReal services, named failures, and a slip’s reason
The same desk, ready for real services. Each step before the record can stop the intake with an error, and nothing is recorded; a slip that does not print comes back in the result with its reason, so the person at the counter knows to reprint it. Validation names the field that is missing or blank.
The services arrive as interfaces, so the real queue and a test’s fake both fit. The
recorded order is a frozen copy of the request, so a caller that edits its request object
afterwards cannot change the work order. Only the printer’s error is caught; every other
error leaves acceptAlteration before anything is recorded.
export type AlterationRequest = Readonly<{
customer: string;
garment: string;
work: string;
}>;
export type WorkOrder = Readonly<{ id: string; request: AlterationRequest }>;
export type IntakeResult = Readonly<{
orderId: string;
slipPrinted: boolean;
/** Why the slip did not print, for the person at the desk. */
slipError?: string;
}>;
export interface Queue {
reserve(): string; // throws when the tailors are fully booked
}
export interface OrderBook {
record(order: WorkOrder): void; // throws when the order cannot be stored
}
export interface Printer {
print(order: WorkOrder): void; // throws when the slip does not print
}
export class Validator {
check(request: AlterationRequest): void {
for (const field of ['customer', 'garment', 'work'] as const) {
if (!request[field].trim()) throw new Error(`incomplete request: ${field}`);
}
}
}
export class IntakeDesk {
private validator = new Validator();
constructor(
private queue: Queue,
private book: OrderBook,
private printer: Printer
) {}
/**
* Takes one alteration. Throws, with nothing recorded, when the request is
* incomplete, the queue is full or the order cannot be stored. Once the
* order is recorded it is accepted: a slip that fails to print is reported
* in the result, not thrown.
*/
acceptAlteration(request: AlterationRequest): IntakeResult {
this.validator.check(request);
const order: WorkOrder = Object.freeze({
id: this.queue.reserve(),
request: Object.freeze({ ...request })
});
this.book.record(order);
try {
this.printer.print(order);
return { orderId: order.id, slipPrinted: true };
} catch (error) {
const slipError = error instanceof Error ? error.message : String(error);
return { orderId: order.id, slipPrinted: false, slipError };
}
}
} Reading the TypeScriptParameter properties, catch, and what a throw skips
constructor(private validator: Validator, …) {} declares a private field
for each parameter and assigns it: the desk keeps its four services. A catch without a name (ES2019) catches the error without binding it; the desk
only needs to know that printing failed.
A throw leaves the function at once and skips the lines after it, but nothing
that already ran is undone: the order book’s array keeps what was pushed. Under strict, a caught error is unknown (TypeScript 4.4’s useUnknownInCatchVariables), so take asserts it is an Error; the production desk checks with instanceof.
04Meet it in your language
You already call facades.
Node’s stream.pipeline() connects a readable source, transforms, and a
destination, then reports completion or failure. On failure, it destroys pipeline streams,
with documented exceptions for streams already ended or closed. Its promise form, from node:stream/promises, arrived in Node 15. The steps it coordinates are the ones
a hand-written .pipe() chain leaves out: .pipe() forwards data,
not errors.
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
// Before: the streams wired by hand. If the source cannot be read, .pipe()
// passes nothing on: the write stream is never closed, and the read error,
// with no listener, is thrown as an uncaught exception.
export function compressByHand(source: string, target: string): void {
createReadStream(source).pipe(createGzip()).pipe(createWriteStream(target));
}
// After: pipeline is the facade over that wiring. It connects the streams,
// destroys every one of them when one fails, and settles once.
export async function compressLog(source: string, target: string) {
await pipeline(
createReadStream(source),
createGzip(),
createWriteStream(target),
);
} The useful similarity is one entry point coordinating a multi-part task. The boundary
differs at failure: pipeline’s cleanup acts on streams, not on what they wrote. When the
source is missing, compressLog rejects, and the empty target file the write
stream created stays on disk; delete it in your catch if a partial file
matters. A facade coordinates the steps; it does not undo the ones that ran.
05Decide where it earns its place
Centralize a repeated task, not every service.
Keep a direct function when one caller owns a short, stable sequence. A facade becomes useful when several entry points need the same workflow and the same partial-result rule. Its cost is another boundary to maintain as the task changes.
| The change | Each caller runs the recipe | One desk |
|---|---|---|
| A deposit is taken before an order is recorded | Each caller adds the step, in the same place, with the same failure rule. | The desk gains a step. Callers keep making the same call. |
| Claim slips are emailed instead of printed | Every caller that prints changes, and decides again what a failed send means. | The desk’s last step changes; its result still says whether the slip went out. |
| One caller needs a different promise | That caller writes its own sequence. | Give the desk a second operation for that task, not a flag on the first. |
If a queue stage is added, the shared operation can put it in one place. If callers need different promises, split around real tasks instead of adding flags to a kitchen-sink interface.
What belongs in the facade is the order of the steps and what their outcome means for the caller. What stays in the services is each step’s own rule: what makes a request complete, how full the queue may get, how a slip is printed. A desk that starts deciding those itself has become one more place that has to change with them.
The facade coordinates; it does not make four operations atomic. The work order can persist while printing fails. Production code must define persistence, duplicate submissions, printer retries, and how staff provide a replacement slip.
Distinguish nearby responsibilitiesAdapter, Mediator, Proxy, dependency injection
Adapter translates an interface; this facade gives callers a task over collaborators. Mediator also sits among several objects, but they talk to each other through it; the desk’s services never call the desk. Proxy controls access to a represented object. Dependency injection supplies the services; it does not define the intake workflow.
06Make the call
Did the intake accept this request?
The queue cannot reserve space for a fresh alteration. Pick the report that matches the stages that ran and the work that exists.
Before you open the answer below, say it in your own words: what does a caller get to stop knowing, and what must a facade’s result still tell it?
One way to say itThe recipe, the outcome, and where a facade stops
The caller knows the customer and the request; the facade knows the recipe, the order of the steps across the subsystem, so every caller runs it the same way and a new step is added once. The facade does not make the steps atomic. Each step that ran stays done, so the result has to say how far the task got: before the order was recorded, nothing was accepted; after it, the work is accepted even if the slip failed. Reporting that partial truth is the facade’s job; a caller told only “failed” will repeat work that already happened. A facade earns its place when several callers share a task; give each different task its own operation, and let code whose task is not the facade’s call the subsystem directly.
Practice in the lab
The tailor from the lesson hands finished alterations back three ways: over the counter, from a locker kiosk, and through a courier app. Each pickup finds the order, takes the balance, releases the garment from the rack and texts a receipt. Each level starts from the previous level’s finished code, so you can begin at any of them.
- FACADE-101 (easy) Three pickup screens, three different recipes
- FACADE-102 (medium) The till goes; the card terminal holds, then takes Pro
- FACADE-103 (hard) Review: the courier retries the whole pickup Pro