← Design patterns
Composition Simplifying a subsystem

Facade

Give a workflow one clear way in.

A neighborhood tailor accepts alterations through a counter, a phone order, and a partner shop. Each intake needs to validate the request, reserve queue space, record a work order, and print a claim slip. Give callers one operation for that workflow, and keep the result honest if the paper fails after the work is accepted.

TypeScriptGoRust The same intake desk in each language.

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:

intake.ts
// 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.

intake.ts
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}`);
Who owns what
RoleResponsibilityWhat it handles
CallerKnows the customerChooses the request and tells the customer what happened.
Facade (the desk)Knows the recipeCalls the services in order and returns the task’s outcome.
Subsystem (the services)Knows its own stepWhat 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

intake.ts
// Lines 47–52, the try/catch around the printer, become two lines:
this.printer.print(order);
return { orderId: order.id, slipPrinted: true };
Suppose the desk let the printer’s error escape, as above: the try/catch becomes a plain call. The printer jams on Mina’s slip, as it does in the program. What does the counter tell Mina, and how many work orders does the last line report?
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.

intake.ts
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.

compress.ts
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.

What would you have to change tomorrow?
The changeEach caller runs the recipeOne desk
A deposit is taken before an order is recordedEach 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 printedEvery 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 promiseThat 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.

Validation passed, but the queue is full, so reservation failed. What should the intake report to the customer?
A one-off script imports last year’s paper ledger: finished work, already paid for, that only needs to appear in the order book. What should it use?
Somewhere else: an online shop’s placeOrder charges the card, reserves the stock, then sends a confirmation email. The email service is down after the charge went through. What should placeOrder give its caller?

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