Skip to article
ALGORITHMICSPatterns
Patterns5 min read

Facade

One door into a subsystem — and why the door must not become the only way in.


Publishing a video means: store the bytes, probe the format, pick a transcode preset, wait for the job, capture a thumbnail at the midpoint, index it, email the owner.

Seven steps, six objects, one required order.

What people write first

The controller does all seven.

const file = await storage.put(bucket, key, bytes);
const meta = await probe.inspect(file.path);
const job  = await transcoder.enqueue(file, PRESETS[meta.codec]);
await job.wait();
const thumb = await thumbnailer.capture(file, meta.duration / 2);
await index.upsert({id, path: file.path, thumb, duration: meta.duration});
await mailer.send(owner, "upload-ready", {id});

Six objects, one required order, and one silent trap — forget to await the transcode and the email links to a file that is not there yet.

Notice what the caller has been made responsible for. It must know that probing comes before transcoding, that the transcode job must be awaited before the thumbnail is captured, and that the email must come last. None of that is about handling an HTTP request.

And the ordering trap is real: skip the await job.wait() and the email goes out linking to a file that does not exist yet. It works in development, where the file is small.

The pattern

One class, one method, the sequence inside it.

class VideoLibrary {
constructor(
private readonly storage: Storage,
private readonly probe: Probe,
private readonly transcoder: Transcoder,
private readonly thumbnailer: Thumbnailer,
private readonly index: SearchIndex,
private readonly mailer: Mailer,
) {}
async publish(owner: User, bytes: Buffer): Promise<Video> {
const file = await this.storage.put(bucketFor(owner), keyFor(owner), bytes);
const meta = await this.probe.inspect(file.path);
const job = await this.transcoder.enqueue(file, PRESETS[meta.codec]);
await job.wait(); // ← the ordering rule, stated once
const thumb = await this.thumbnailer.capture(file, meta.duration / 2);
const video = await this.index.upsert({owner, file, thumb, meta});
await this.mailer.send(owner, 'upload-ready', {id: video.id});
return video;
}
}

The controller becomes await library.publish(user, bytes).

What it is for

Onboarding. A new developer needs one method, not seven objects in a specific order.

A stable surface. Callers depend on VideoLibrary; the subsystem behind it can be rewritten. This is why a facade is often the first step in strangling a legacy subsystem — put a door on it, move callers to the door, then replace what is behind the door.

Testing. Tests of the controller mock one thing.

The common path. The facade should cover the case that happens 95% of the time. The other 5% goes direct, and that is correct rather than a failure.

Where it already is

fetch() is a facade over DNS, TCP, TLS, HTTP framing and redirect handling. jQuery was a facade over browser inconsistencies. docker run is a facade over namespaces, cgroups and an overlay filesystem. Most well-liked libraries are mostly facade.

That is worth taking as design guidance: the reason those APIs feel good is not that they invented anything. It is that they chose one common path and made it one call.