structural
Facade
Simplify a complex subsystem behind one coherent interface.
Facade provides a unified, higher-level interface to a set of interfaces in a subsystem. It does not hide the subsystem — callers who need direct access still have it — but it defines a common entry point that handles the typical case correctly. Facade is what turns a dozen interrelated services with mandatory ordering and retry logic into one method that does the right thing.
The problem
Uploading and publishing a video requires: presign the upload, put the file in blob storage, probe the media for duration and codecs, transcode to three renditions, generate a thumbnail, register the asset in the database, and emit a 'published' event. Getting any of these out of order, or retrying the wrong one, corrupts the catalog. Every caller — the CLI, the admin panel, the mobile upload endpoint — re-implements the same orchestration and inevitably drifts from the others.
The solution
Introduce a `VideoPublisher` facade with a single `publish(sourceUrl)` method. It owns the orchestration: presign → upload → probe → transcode → thumbnail → register → emit. Callers use the facade and get consistent behavior; escape hatches exist for the handful of callers that genuinely need to run one step in isolation, but the common case is one call.
When to use it
- A subsystem has many classes with mandatory ordering or protocol that callers routinely get wrong.
- You want to provide a simple default for the common case while leaving the subsystem accessible.
- Multiple entry points (CLI, HTTP, jobs) share the same orchestration and are drifting apart.
- You want to layer the system — a stable outer surface, an internal surface that can change.
When not to use it
- The subsystem is already simple — a facade just adds an indirection.
- Every caller needs a different sequence — a shared facade will not fit and will be bypassed.
- The facade would start to contain business logic of its own instead of orchestrating subsystem calls.
Analogy
A restaurant ordering counter: you say 'one chicken burrito' and get a fully assembled meal. Behind the counter, five stations (grill, rice, prep, wrap, expo) run in parallel with their own rules. You do not negotiate with each station — the counter provides a single, coherent interface to the whole kitchen.
Diagram
Implementation
interface StorageService {
presign(key: string): Promise<string>;
put(key: string, body: Buffer): Promise<void>;
}
interface Transcoder {
probe(url: string): Promise<{ durationMs: number; width: number; height: number }>;
transcode(url: string, preset: "720p" | "1080p" | "4k"): Promise<string>;
}
interface AssetRepository {
register(asset: { key: string; renditions: string[]; durationMs: number }): Promise<string>;
}
interface EventBus {
emit(name: string, payload: Record<string, unknown>): Promise<void>;
}
class VideoPublisher {
constructor(
private readonly storage: StorageService,
private readonly transcoder: Transcoder,
private readonly assets: AssetRepository,
private readonly events: EventBus,
) {}
async publish(sourceUrl: string): Promise<string> {
// 1. Copy the source into our bucket — we own the canonical asset.
const key = `sources/${crypto.randomUUID()}.mp4`;
const uploadUrl = await this.storage.presign(key);
await this.storage.put(key, await download(sourceUrl));
// 2. Probe once; the info drives transcoding and registration.
const info = await this.transcoder.probe(uploadUrl);
// 3. Transcode in parallel — they are independent.
const [p720, p1080, p4k] = await Promise.all([
this.transcoder.transcode(uploadUrl, "720p"),
this.transcoder.transcode(uploadUrl, "1080p"),
this.transcoder.transcode(uploadUrl, "4k"),
]);
// 4. Register, then announce. Order matters: emit only after registration.
const assetId = await this.assets.register({
key,
renditions: [p720, p1080, p4k],
durationMs: info.durationMs,
});
await this.events.emit("video.published", { assetId, key });
return assetId;
}
}
// Callers — CLI, HTTP endpoint, batch job — all use one method.
await publisher.publish("https://external.example.com/incoming.mp4");VideoPublisher owns the ordering (upload → probe → transcode → register → emit), the parallelism (three transcodes), and the error ordering (emit only after registration). The subsystem classes are still available — the transcoder can be called directly when a batch job needs a one-off rendition — but the common path is one method. Adding a subtitle-generation step means editing VideoPublisher once, not every caller.
Advantages
- One entry point for a routine that must be orchestrated consistently.
- Callers no longer duplicate ordering and error-handling logic.
- The subsystem remains accessible — the facade is not a wall.
- Subsystem changes (adding a rendition, changing the event name) happen in one place.
Disadvantages
- Facade can become a God object if it accumulates orchestration for every scenario.
- It is tempting to put business logic in the facade — it should orchestrate, not decide.
- Callers with genuinely different orchestration must bypass it, which reintroduces duplication.
Related patterns
- Adapter — Adapter changes one interface to another; Facade provides a simpler interface over many. Adapter is 1:1, Facade is 1:many.
- Mediator — Both centralize coordination. Facade centralizes calls from clients to a subsystem; Mediator centralizes calls among peers. Facade does not add behavior, Mediator often does.
- Singleton — A facade is sometimes implemented as a Singleton (one entry point per process), but the two solve different problems and should not be conflated.