01 / A task instead of a recipe
The caller wants to join the call.
A single handler can check the inputs, connect, and enable the microphone directly. For one short, stable caller, keeping those calls together is easy to understand. A class is not required merely because three services are involved.
The pressure comes from repeated knowledge. One screen enables audio before the room is ready. Another forgets validation. Every caller now has to remember the ordering and decide what to say when the second or third step fails.
A facade offers a simpler interface to a subsystem, shaped around what its callers need
to do. Here, call.join(request) owns the sequence. Rules still validate; the transport still
connects; the audio service still enables a microphone. The facade coordinates their work.
The caller owns the room, device choice, trigger, and response to the outcome. It can stop knowing the internal call order. It cannot stop caring whether the requested setup actually completed.
02 / The shape in the comparison
One request, three collaborators.
Start with the basic coordinating function. The practical form holds the same collaborators and translates connection and microphone errors into task-level outcomes. That translation is a convenience added here, closer to the work Adapter teaches; owning the sequence over several collaborators is what makes this a facade. The complete form includes deterministic memory services so you can run every branch.
The contract checks room first, then microphone, for exact emptiness. Whitespace is preserved. Validation failure calls neither setup service. Connection failure stops before audio. Microphone failure leaves the connection intact and preserves any earlier microphone setting.
A successful receipt means the room is connected and the requested microphone is active. Repeating the same room reuses the connection. Joining a different room fails until the session is reset. Those promises come from these memory services, not from the pattern.
One entry point owns validate → connect → enable microphone. This small function already has the facade relationship and propagates collaborator errors.
export type JoinRequest = Readonly<{ room: string; microphone: string }>;
export interface Services {
rules: { check(request: JoinRequest): void };
transport: { connect(room: string): void };
audio: { enable(microphone: string): void };
}
export function joinBasic(services: Services, request: JoinRequest): void {
services.rules.check(request);
services.transport.connect(request.room);
services.audio.enable(request.microphone);
} type JoinRequest struct{ Room, Microphone string }
type JoinRules interface{ Check(JoinRequest) error }
type Transport interface{ Connect(string) error }
type Audio interface{ Enable(string) error }
type Services struct {
Rules JoinRules
Transport Transport
Audio Audio
}
func JoinBasic(s Services, r JoinRequest) error {
if err := s.Rules.Check(r); err != nil {
return err
}
if err := s.Transport.Connect(r.Room); err != nil {
return err
}
return s.Audio.Enable(r.Microphone)
} Reading the TypeScriptStructural collaborators and exceptions
The Services interface describes three small objects. CallFacade stores that bundle and calls it in order. A thrown validation error propagates; separate catch blocks normalize the two setup failures. The returned receipt is a new object containing the submitted string values.
The lab owns one memory subsystem per experiment. Creating another facade around the same services would share their state. Creating another subsystem gives an independent session.
Reading the GoInterfaces and explicit early returns
Each collaborator implements the method its interface requires. Join checks each returned error before calling the next service. The returned receipt and error express the same task outcome as the TypeScript version.
The memory subsystem is used by one goroutine at a time. Pointer fields distinguish no connection or microphone from a supplied string. Snapshot copies those values and the event slice, so inspecting an earlier attempt does not change the session.
Reading the PythonProtocols, dataclasses, and explicit exceptions
Protocols describe the three collaborator roles, while frozen dataclasses hold the request, receipt, services bundle, and snapshot. CallFacade keeps the same Services bundle and passes validation errors through while normalizing setup failures.
MemorySubsystem implements all three protocols. Its snapshot copies the event list, so a caller can inspect an attempt without mutating the session.
03 / Follow a partial setup
Connected does not yet mean ready to speak.
Watch a failed connection, then a failed microphone setup, then a successful retry. Step through keeps each result available for inspection. Try it lets you change the request and fail either subsystem while retaining the session between attempts.
Before running the microphone failure, predict the room and audio state separately.
One call. An honest outcome.
- Validate· Not reached
- Connect· Not reached
- Microphone· Not reached
No setup attempted.
One request starts the setup.
The caller supplies a room and microphone. Neither subsystem is ready yet.
Reduced motion: choose a scene to see its completed state.
Read this scene
The caller supplies a room and microphone. Neither subsystem is ready yet.
Room: none. Microphone: off. Called: nothing yet.
After a successful join, try changing from Desk mic to Headset with microphone failure selected. Desk mic remains active. “Microphone setup failed” describes the attempted change; it does not mean the earlier device was turned off.
04 / Describe the outcome
What can the caller honestly say?
The smaller interface should reduce coordination work while preserving the information needed to recover. Start from a fresh session and choose the response that agrees with the state after audio setup fails.
05 / Who owns the workflow
Keep setup and ownership visible.
Application setup constructs the services and passes them to a facade scoped to one call session. Event handlers submit a request through that boundary. The collaborators still own connection and device behavior; specialists can have separate lower-level operations when they need them.
A real call joins asynchronously. Its contract needs cancellation, timeouts, cleanup on leaving, and a decision about concurrent attempts. A later microphone selection must not be overwritten by a stale completion.
Retried setup also needs a documented identity and lifetime. Here the room string identifies the one retained connection, and the doubles fail before mutating the failed step. Real services can fail after work completes or lose the response. Repeating the request may then require more than calling the same method again.
One method is not a transactionMake partial completion part of the contract
There is no disconnect or compensating action in this example. When audio fails, the connection remains. A production workflow might deliberately keep the connection, offer a listen-only state, or disconnect. Each choice needs explicit behavior and tests.
The teaching facade uses readable error messages. A production API can use structured outcomes to distinguish invalid input, connection failure, and connected-with-audio-failure, while preserving underlying diagnostics for investigation. Callers should not parse display copy as a machine protocol.
Build UIs?Every Join button that calls three services in a row is a facade in the wrong place, and one day a reconnect banner will need the same sequence.
Where it already is in your components
The Join button is a facade you have already written, in the wrong place. Its click
handler calls the rules service, the transport, and the audio service in a row, and keeps
a flag for each step so the screen can say something. That handler owns the sequence from
section 01 and the error handling from section 02, so the component is the only thing that
knows connect comes before enable, or that an error thrown after the connected flag was
set means the room is fine. The reconnect banner needs the same sequence, and the easy
thing is to paste it. The textbook sample below is that handler, once in React with a useState per flag and once in Svelte with a $state per flag.
When you have to own it
Move the sequence behind CallFacade.join and let the component own only what
the screen needs. In React that is a useJoinCall hook; in Svelte a createJoinCall function a component calls once. Each holds one facade for the
session, exposes join(request), derives a status from the receipt or from the
failure, and remembers the device the last successful join enabled. The hook coordinates;
the facade owns the sequence, so the reconnect banner calls the same join.
Keep the submitted request distinct from fields the user is still editing: the status carries the request it was built from, so the screen names the room that was joined rather than whatever the input says now. A connection failure means this attempt did not reach audio setup. A microphone failure after connecting should leave the joined room visible and offer a recovery consistent with the device state. On a fresh session that can mean “Connected; microphone off.” After a failed device change it may mean “Connected; still using Desk mic.” Do not infer either state from a generic error alone. The lab keeps the last attempt visible until another request is submitted.
A browser integration would need its own device permission, media-track lifecycle, and stale-request handling. The UI race conditions lesson explores ownership of competing results.
The Join button: three services called in a row from a click handler, with a flag per step and the facade’s sequence leaking into the component.
import { useState } from 'react';
import type { Services } from '../call';
// The three services from section 01, handed in by whoever owns the session.
export function JoinButton({ services }: { services: Services }) {
const [room, setRoom] = useState('');
const [microphone, setMicrophone] = useState('');
// Three flags, one per step, each set by hand. Nothing keeps them consistent.
const [joining, setJoining] = useState(false);
const [connected, setConnected] = useState(false);
const [microphoneOn, setMicrophoneOn] = useState(false);
const [error, setError] = useState<string | null>(null);
// async because a real connect awaits the network; the lesson's memory services answer at once.
async function join() {
setJoining(true);
setError(null);
// validate → connect → enable microphone, stopping at the first failure. The sequence
// and its error handling are the facade's job (section 02) leaking into a click handler.
// A reconnect banner would repeat every line of it.
try {
services.rules.check({ room, microphone });
services.transport.connect(room);
setConnected(true);
services.audio.enable(microphone);
setMicrophoneOn(true);
} catch (cause) {
setError(cause instanceof Error ? cause.message : 'Join failed');
} finally {
setJoining(false);
}
}
return (
<>
<input value={room} onChange={(e) => setRoom(e.target.value)} />
<input value={microphone} onChange={(e) => setMicrophone(e.target.value)} />
<button onClick={() => void join()} disabled={joining}>
Join
</button>
{/* Which step failed? Only the message knows, and only if you parse it. */}
{error && <p>{error}</p>}
{/* room is whatever the field says now, not what was submitted. */}
{connected && (
<p>
Joined {room}; microphone {microphoneOn ? 'on' : 'off'}
</p>
)}
</>
);
}
06 / Already in your toolbox
Recognize a smaller surface for a larger task.
Node’s stream.pipeline() connects streams and generators, forwards errors, and reports completion. The caller supplies the pieces while the utility coordinates their flow. Its cleanup and reuse rules are documented promises of that utility.
React’s createRoot returns an object with render and unmount. Application code asks React to manage content through that narrow public surface. Its lifecycle remains part of the contract: unmounting detaches the root, and that root cannot render again.
Both are facade-like relationships you can read from documented public APIs.
07 / What the extra layer buys
What does the caller get to stop knowing?
A facade earns its place when multiple callers share a meaningful subsystem task, or when the general subsystem API exposes more decisions than a common operation needs. Its value is the responsibility it centralizes, not the number of objects behind it.
When joining requires one more readiness check, callers can keep submitting the same request if its promise stays intact. If the product changes to connecting without enabling audio, the task’s promise changes too; a facade cannot hide that semantic change from callers that depend on it.
Keep a direct function for a short, stable sequence. Avoid a broad PlatformFacade that accumulates unrelated account, billing, and device methods. Split entry points around coherent tasks and owners, and give specialist operations meaningful names instead of unexplained boolean flags.
Distinguish the nearby patterns
Adapter translates an interface into the one a caller requires. Proxy controls access to a represented object. Our facade introduces a task over several service operations.
Mediator coordinates interactions among participants. These services simply respond to facade calls; they do not communicate with each other through it. Template method fixes an algorithm’s sequence while selected steps vary through overrides. The emphasis here is a convenient subsystem boundary.
Dependency injection supplies the collaborators. The facade decides how to use them to fulfill the task. An injected bundle alone is setup, not the simpler workflow.
08 / Take the idea with you
Name the task and its promise.
Explain the design without the pattern name: “I put call setup in one place so each entry screen follows the same sequence. Its outcome distinguishes a connection failure from a connected room whose microphone setup failed.”
Now name one workflow in your own code. What does each caller have to remember? Which decisions could an entry point own? Which partial outcome must remain visible even if the underlying steps are hidden?
Finally, replay the microphone failure from memory. Say which room is connected, which microphone is active, and why returning an error does not restore the earlier session. Then repeat the reasoning after a successful join followed by a failed device change.
Connections to follow nextRelated lessons
Adapter translates one interface into the one a caller expects; a facade gives callers a task over several collaborators.
Mediator owns the rules between participants that talk to each other; these services only answer the facade.
Dependency injection explains how application setup supplies the services the facade coordinates.