A facade: one task-shaped call over a subsystem
One method per task, the order and the wiring inside it; undo what a failed task started; keep the rules in the parts.
Predict first
Before you read: predict
ExamHall's candidate app starts a proctored attempt with one call, ProctoringFacade.start. Today the alert channel is down: its subscribe throws ChannelDown. The files are below.
What does main.ts print?
// proctoring.tsexport class ChannelDown extends Error {}export class FaceCheck { verify(attemptId: string): void {} }export class VideoService { open = new Set<string>(); start(attemptId: string): string { this.open.add(attemptId); return `vid-${attemptId}`; } stop(attemptId: string): void { this.open.delete(attemptId); }}export class AlertChannel { subscribe(videoId: string): void { throw new ChannelDown(); } }export class AuditLog { entries: string[] = []; write(line: string): void { this.entries.push(line); } } // proctoring-facade.tsimport { FaceCheck, VideoService, AlertChannel, AuditLog } from './proctoring';export class ProctoringFacade { constructor(private face: FaceCheck, private video: VideoService, private alerts: AlertChannel, private audit: AuditLog) {} start(attemptId: string): string | null { this.face.verify(attemptId); const videoId = this.video.start(attemptId); this.alerts.subscribe(videoId); this.audit.write(`started ${attemptId}`); return videoId; }} // main.tsimport { FaceCheck, VideoService, AlertChannel, AuditLog } from './proctoring';import { ProctoringFacade } from './proctoring-facade';const video = new VideoService();const audit = new AuditLog();const facade = new ProctoringFacade(new FaceCheck(), video, new AlertChannel(), audit);let seen: string;try { seen = String(facade.start('A-17')); } catch (e) { seen = (e as Error).constructor.name; }console.log(`open video sessions: ${video.open.size} · audit entries: ${audit.entries.length} · the app sees: ${seen}`);The 30-second version
A facade gives its callers one method per task and makes the subsystem's calls inside it, in the order the parts need, wiring the parts up itself. Each part's change is real once its call returns, so a failure partway is the facade's to undo and to report. It adds no rules of its own.
One method per task, the order and the wiring inside it; undo what a failed task started; keep the rules in the parts.
How it works
Step 1 of 13
Analogy: A funeral director
A family arranges a funeral service through one director.
| With the funeral director | In code |
|---|---|
| the family makes one call, "a service on Friday", and the director arranges the rest | one method per task (step 3) |
| the registrar's certificate before the crematorium booking, the booking before the notice in the paper | the order comes from the parts (step 4) |
| when the crematorium cancels after the notice is placed, the director withdraws the notice and rings the family | undo, then report (step 6) |
| a family that wants its own florist rings the florist itself | the parts stay open (step 8) |
| a director who also runs the florist, prints the orders of service and books the caterers, so every supplier goes through him | the god object (step 11) |
In real terms: name the facade's methods after tasks; inside each, call the parts in the order their contracts need, and undo what a failure leaves behind.
Breaks down when: a director can ring the family halfway and ask; a facade's method runs to its end or throws.
Contrasting case: friends holding a small garden memorial need no director: one caller, one sequence.
Examples, run for real
4.1 ExamHall: three copies of one start sequence
EXH-4830: ExamHall's candidate app, practice mode and mock-exam runner each started an attempt by calling the four parts themselves. The mock-exam runner wrote its audit entry first, so attempts that the face check then refused were logged as started.
MR !5130 adds proctoring-facade.ts, whose proctoring.start makes the four calls once, and points the three callers at it:
// mock-exam.ts, MR !5130
export const begin = (attemptId: string) => proctoring.start(attemptId);
4.1 EXH-4830 as shipped: 3 caller files call proctoring parts (12 part calls) · 5 starts each, face check refusing 2 · audit entries for refused starts: candidate app 0, practice mode 0, mock-exam runner 2 · files a change to the order edits: 3
4.1 MR !5130: 1 file calls proctoring parts (4 part calls, proctoring-facade.ts) · 5 starts each · audit entries for refused starts: candidate app 0, practice mode 0, mock-exam runner 0 · files a change to the order edits: 1
Before the MR, each caller held its own copy of the order, and one copy was wrong. After it, the order sits in one method; the callers name the task (steps 2–3).
4.2 What the MR does not change
The lock after three alerts stays in AttemptStore, the part both the facade and the invigilator console call; MR !5130 copies no rule into the facade (step 9).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 7
A team chat starts a group call for Ana through startCall; media.open comes first, and three calls follow it. ringer.ring is a model of the members' phones: everyone rung answers at once with "hi", and Ben also calls Ana back straight away. The files are below.
The team needs two things: the recording holds every member's first words, and Ben's call back finds Ana busy.
Which order of the three calls meets both?
// call.tsexport class Call { recording: string[] | null = null; say(line: string): void { this.recording?.push(line); }}export const busy = new Set<string>();export const replies: string[] = [];export const media = { open: (room: string) => new Call() };export const recorder = { attach: (call: Call) => { call.recording = []; } };export const presence = { setBusy: (user: string) => { busy.add(user); } };export const ringer = { ring(call: Call, caller: string, members: string[]) { for (const m of members) call.say(`${m}: hi`); replies.push(busy.has(caller) ? 'busy' : 'ringing'); },}; // calls.ts (ORDER is the three calls after media.open, in the order chosen below)import { media, recorder, presence, ringer } from './call';export function startCall(room: string, caller: string, members: string[]) { const call = media.open(room); ORDER return call;} // main.tsimport { startCall } from './calls';import { replies } from './call';const call = startCall('design', 'ana', ['ben', 'cy']);console.log(`recording: ${(call.recording ?? []).join(', ') || 'empty'} · Ben's call back: ${replies.join(', ')}`);Question 2 of 7
A team chat's facade adds slow mode: one post per user per 10 seconds. The thread team's ThreadFacade posts replies through a ChatFacade it was handed; a bridge for e-mail replies appends to the message store itself. The seconds are numbers the code passes in. The files are below.
What does main.ts print?
// messages.tsexport class MessageStore { posted: string[] = []; append(user: string, text: string, at: number): void { this.posted.push(`${at}s ${user}: ${text}`); }} // chat-facade.tsimport { MessageStore } from './messages';export class SlowMode extends Error {}export class ChatFacade { private last = new Map<string, number>(); constructor(private store: MessageStore) {} post(user: string, text: string, at: number): void { const prev = this.last.get(user); if (prev !== undefined && at - prev < 10) throw new SlowMode(); this.last.set(user, at); this.store.append(user, text, at); }} // thread-facade.tsimport { ChatFacade } from './chat-facade';export class ThreadFacade { constructor(private chat: ChatFacade) {} reply(user: string, parent: string, text: string, at: number): void { this.chat.post(user, `re ${parent}: ${text}`, at); }} // main.tsimport { MessageStore } from './messages';import { ChatFacade } from './chat-facade';import { ThreadFacade } from './thread-facade';const store = new MessageStore();const chat = new ChatFacade(store);const threads = new ThreadFacade(new ChatFacade(store));const tries: Array<[string, () => void]> = [ ['app 0s', () => chat.post('ana', 'hello', 0)], ['thread 3s', () => threads.reply('ana', 'm1', 'me too', 3)], ['bridge 5s', () => store.append('ana', 'sent from e-mail', 5)], ['app 8s', () => chat.post('ana', 'again', 8)],];const out = tries.map(([name, post]) => { try { post(); return `${name} posted`; } catch (e) { return `${name} ${(e as Error).constructor.name}`; }});console.log(out.join(' · '));Question 3 of 7
A team chat's app subscribed to typing indicators at start-up, before the team wrote ChatFacade. Now the app also calls openRoom each time the user switches to a room; the facade has no way to close one. The files are below.
What does main.ts print?
// typing.tsexport class Typing { private subs: Array<{ room: string; show: (user: string) => void }> = []; subscribe(room: string, show: (user: string) => void): void { this.subs.push({ room, show }); } emit(room: string, user: string): void { for (const s of this.subs) if (s.room === room) s.show(user); }} // history.tsexport class History { last(room: string, n: number): string[] { return [`${room}: welcome`].slice(0, n); } } // chat-facade.tsimport { Typing } from './typing';import { History } from './history';export class ChatFacade { constructor(private typing: Typing, private history: History) {} openRoom(room: string, show: (line: string) => void): string[] { this.typing.subscribe(room, (user) => show(`${user} is typing`)); return this.history.last(room, 20); }} // main.tsimport { Typing } from './typing';import { History } from './history';import { ChatFacade } from './chat-facade';const typing = new Typing();const lines: string[] = [];typing.subscribe('general', (user) => lines.push(`${user} typing…`));const chat = new ChatFacade(typing, new History());for (const room of ['general', 'random', 'general']) chat.openRoom(room, (line) => lines.push(line));typing.emit('general', 'ana');console.log(`lines shown for one keystroke in general: ${lines.length}`);Question 4 of 7
A team chat's CallFacade starts a call, marks its caller busy and rings the room. When the ringing service is down, ring throws RingerDown. call-parts.ts and main.ts are the same in every version; call-facade.ts comes in three versions. The files are below.
The team needs main.ts to print design: design/call · open calls: design · busy: ana and then ops: CallFailed · open calls: design · busy: ana.
Which version does that, with main.ts unchanged?
// call-parts.tsexport class RingerDown extends Error {}export class CallFailed extends Error {}export class Media { open = new Set<string>(); start(room: string): string { this.open.add(room); return `${room}/call`; } close(room: string): void { this.open.delete(room); }}export class Presence { busy = new Set<string>(); set(user: string): void { this.busy.add(user); } clear(user: string): void { this.busy.delete(user); }}export class Ringer { down = false; ring(room: string): void { if (this.down) throw new RingerDown(); }} // call-facade.ts (version 1)import { Media, Presence, Ringer, CallFailed } from './call-parts';export class CallFacade { constructor(private media: Media, private presence: Presence, private ringer: Ringer) {} start(room: string, user: string): string { const id = this.media.start(room); this.presence.set(user); try { this.ringer.ring(room); } catch { this.presence.clear(user); this.media.close(room); throw new CallFailed(); } return id; }} // call-facade.ts (version 2)import { Media, Presence, Ringer, CallFailed } from './call-parts';export class CallFacade { constructor(private media: Media, private presence: Presence, private ringer: Ringer) {} start(room: string, user: string): string { const id = this.media.start(room); this.presence.set(user); try { this.ringer.ring(room); } catch { throw new CallFailed(); } finally { this.presence.clear(user); this.media.close(room); } return id; }} // call-facade.ts (version 3)import { Media, Presence, Ringer, CallFailed } from './call-parts';export class CallFacade { constructor(private media: Media, private presence: Presence, private ringer: Ringer) {} start(room: string, user: string): string { const id = this.media.start(room); this.presence.set(user); try { this.ringer.ring(room); } catch { this.media.close(room); throw new CallFailed(); } return id; }} // main.tsimport { Media, Presence, Ringer } from './call-parts';import { CallFacade } from './call-facade';const media = new Media();const presence = new Presence();const ringer = new Ringer();const calls = new CallFacade(media, presence, ringer);const report = (label: string, start: () => string) => { let r: string; try { r = start(); } catch (e) { r = (e as Error).constructor.name; } console.log(`${label}: ${r} · open calls: ${[...media.open].join(', ') || 'none'} · busy: ${[...presence.busy].join(', ') || 'nobody'}`);};report('design', () => calls.start('design', 'ana'));ringer.down = true;report('ops', () => calls.start('ops', 'ben'));Question 5 of 7
A team chat's RoomFacade joins a user to a room. Fill in join in room-facade.mjs; the parts it receives are:
members.add(room, user)(throws when the room is full) andmembers.remove(room, user);history.last(room, n), the room's lastnmessages, oldest first, each{ id, text };typing.subscribe(room, onTyping), which returns a function that unsubscribes;receipts.markRead(room, user, messageId), which can throw.
join(room, user, onTyping) adds the member, loads the last 20 messages, subscribes to typing and marks read up to the last loaded message (no call when there is none), in that order. It returns { messages, leave }; leave() unsubscribes and then removes the member. If any call throws, join undoes what earlier calls changed (unsubscribe first, then remove the member) and throws JoinFailed (from ./errors.mjs) with the original error as its cause.
Write join in full.
// room-facade.mjsimport { JoinFailed } from './errors.mjs';export class RoomFacade { constructor(members, history, typing, receipts) { Object.assign(this, { members, history, typing, receipts }); } join(room, user, onTyping) { // your code }}Question 6 of 7
A film service's player starts playback with one call, WatchFacade.start. The CDN is busy on the first token request only, and the player tries once more after an error. The files are below.
What does main.ts print?
// watch-parts.tsexport class CdnBusy extends Error {}export class Sessions { open: string[] = []; start(user: string): string { const id = `${user}#${this.open.length + 1}`; this.open.push(id); return id; }}export class Meter { running: string[] = []; start(sessionId: string): void { this.running.push(sessionId); } }export class Cdn { private calls = 0; token(title: string): string { this.calls++; if (this.calls === 1) throw new CdnBusy(); return `tok-${title}`; }} // watch-facade.tsimport { Sessions, Meter, Cdn } from './watch-parts';export class WatchFacade { constructor(private sessions: Sessions, private meter: Meter, private cdn: Cdn) {} start(user: string, title: string): string { const id = this.sessions.start(user); this.meter.start(id); return this.cdn.token(title); }} // main.tsimport { Sessions, Meter, Cdn } from './watch-parts';import { WatchFacade } from './watch-facade';const sessions = new Sessions();const meter = new Meter();const watch = new WatchFacade(sessions, meter, new Cdn());let token = '';for (let attempt = 1; attempt <= 2 && !token; attempt++) { try { token = watch.start('mia', 'film-42'); } catch { /* the player tries again */ }}console.log(`sessions open: ${sessions.open.length} · meters running: ${meter.running.length} · playing: ${token || 'nothing'}`);Question 7 of 7
A box office's kiosk team wants no use of BoxOfficeFacade to end with a ticket issued for a seat that was never paid for. box-office.ts is the same in every version; box-office-facade.ts comes in three versions. The files are below.
The team's check calls each version's public methods in every order, up to three calls, with seat B7 and a good or an expired card; a call that throws ends that sequence.
Which version leaves no sequence ending with a ticket for an unpaid seat?
// box-office.tsexport class Declined extends Error {}export class Seats { held = new Set<string>(); hold(seat: string): void { this.held.add(seat); } }export class Payments { paid = new Set<string>(); charge(seat: string, card: string): void { if (card === 'card-expired') throw new Declined(); this.paid.add(seat); }}export class Tickets { issued: string[] = []; issue(seat: string): string { this.issued.push(seat); return `T-${seat}`; } } // box-office-facade.ts (version 1)import { Seats, Payments, Tickets } from './box-office';export class BoxOfficeFacade { constructor(private seats: Seats, private payments: Payments, private tickets: Tickets) {} hold(seat: string): void { this.seats.hold(seat); } charge(seat: string, card: string): void { this.payments.charge(seat, card); } issue(seat: string): string { return this.tickets.issue(seat); }} // box-office-facade.ts (version 2)import { Seats, Payments, Tickets } from './box-office';export class BoxOfficeFacade { constructor(private seats: Seats, private payments: Payments, private tickets: Tickets) {} sell(seat: string, card: string): string { this.seats.hold(seat); this.payments.charge(seat, card); return this.tickets.issue(seat); }} // box-office-facade.ts (version 3)import { Seats, Payments, Tickets } from './box-office';export class BoxOfficeFacade { constructor(private seats: Seats, private payments: Payments, private tickets: Tickets) {} sell(seat: string, card: string): string { this.seats.hold(seat); this.payments.charge(seat, card); return this.tickets.issue(seat); } reprint(seat: string): string { return this.tickets.issue(seat); }}Go deeper
Pro tips
- Name the method after the task, and count the parts behind it. A method that calls one part is a pass-through: its callers still own the order. When: reviewing an MR that adds a facade method (steps 3, 10).
- If a method name mentions a vendor, you wanted an adapter. The curriculum row's pro tip; adapter against facade by intent is the prerequisite's lesson (
pat-structural-wrapperssteps 2 and 9). When: the row's typical question, "Adapter vs facade: what is the difference?" Verify: nothing to run; this is interview advice, and it keys nothing. Status: ADVICE.
Source
- refactoring.guru, Facade (the row's second source): https://refactoring.guru/design-patterns/facade · fetched 2026-10-17. Teaching only; every key here is executed. "The facade should be responsible for initializing the subsystem and managing its further life cycle unless the client code already does this." (step 7); "Facade defines a simplified interface to a subsystem of objects, but it doesn’t introduce any new functionality." and "Subsystem classes aren’t aware of the facade’s existence." (step 9); "A facade can become a god object coupled to all classes of an app." and "If the facade becomes too big, consider extracting part of its behavior to a new, refined facade class." (steps 11–12); "Additional facades can be used by both clients and other facades." (step 8). Valid as of 2026-10-17.
- The row's first source, Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), is a book and keys nothing.
Related lessons
Checked against the tools and docs as of 2026-10-17.