Facade pattern
Open in the interactive app: practice, quiz, mock interview →In one minute
- A facade gives callers one small, task-shaped interface over a subsystem with many parts.
git commitsits over the object store, the index and refs;requests.getsits over connection pools, TLS, redirects and decoding;os.WriteFilesits over open, write and close. - Callers state the task ("save this file", "place this order"); the facade owns the how: which parts to call, in what order, and how to clean up when a step fails.
- A good facade does not lock the subsystem away. Callers who need more can still use the parts directly, the way git exposes plumbing commands under its porcelain.
- The risk is what a facade hides. A convenient call can hide missing durability (
os.WriteFilenever callsfsync), missing timeouts, or ten network round trips. The facade's contract must say what it guarantees and what it does not. - Agents tend to put orchestration in every handler, copying the same five-step sequence into each endpoint. Review asks for one facade that owns the sequence and its failure handling, kept small enough to stay a facade and not become a god object.
How it works
The problem
A desktop sync client stores its settings, its sync cursor and its account tokens in small JSON files. Six code paths write them, each with its own version of:
with open(path, "w") as f:
f.write(json.dumps(state))Opening with "w" truncates the file first. A crash, power loss or full disk before the write completes leaves the file empty or half written, and the client cannot parse its own state on the next start. After every big OS update about 0.3% of users lose their sync cursor and trigger a full resync. The fix is known (write a temporary file, flush it, rename it over the old one), but it is several calls with ordering rules and cleanup, and six call sites each had to get it right.
The shape
A facade puts the sequence behind one method:
class SafeFiles:
def __init__(self, fs):
self.fs = fs
def write_atomic(self, path, data):
tmp = path + ".tmp"
self.fs.write_file(tmp, data) # new content in a side file
self.fs.fsync(tmp) # bytes reach stable storage
self.fs.rename(tmp, path) # readers now see old or new, never partial
self.fs.fsync_dir(dirname(path)) # the rename itself survives a crashThe six call sites become files.write_atomic(path, data). The ordering rules, the choice of temp name in the same directory (rename is atomic only within one filesystem) and the cleanup on failure live in one place with one test suite. The Practice builds this facade, including the failure handling the sketch leaves out.
Facades you already use
- git porcelain and plumbing.
git commitruns what plumbing commands such aswrite-tree,commit-treeandupdate-refdo one by one. Scripts that need control call the plumbing; people use the porcelain. subprocess.runwrapsPopen,communicate,waitand killing the child on timeout. It is the right default because the correct sequence (read both pipes concurrently to avoid deadlock, then reap the process) is easy to get wrong by hand.requestswraps aSession, anHTTPAdapterand urllib3's pool manager. Go'shttp.Getwrapshttp.DefaultClient, itsTransportand connection pool.- Service layers. In the design lessons,
BookingServiceandExpenseServiceare facades: the API layer calls one method per user action and the service coordinates seats, payments and ledgers. At system scale, an API gateway or backend-for-frontend is a facade over many services (see those lessons in the system design course).
Facade, adapter, mediator
The Adapter lesson separates the three wrappers by intent: an adapter changes an interface to fit what callers expect, a decorator adds behaviour behind the same interface, and a facade offers a new, simpler interface over several parts. A mediator is easy to confuse with a facade but works in the other direction: the parts of a mediated system know the mediator and talk to each other through it, while the parts behind a facade do not know the facade exists.
When not to use it
- Pass-through facades. A class whose methods mirror the subsystem one to one hides nothing and adds a layer to read. Facades earn their place by combining calls.
- Grab-bag facades.
PlatformFacadewith 60 methods across billing, search and email is a god object. Split facades by task or by caller. - Callers that need the details. A storage engine should not go through a file-saving convenience; give it the parts.
Reviewing agent-written code
Asked to "add an export endpoint", an agent will usually inline the whole sequence in the handler (fetch, transform, upload, record, notify), and the next endpoint repeats four of those steps with different error handling. Ask for a facade method that owns the sequence, and check what happens when step three fails: are earlier steps undone or left in a known state, and does the error name the failed step? The opposite failure is a facade that swallows errors to stay "simple", returning False with no reason. Simple means fewer calls, not less information.
The same shape elsewhere
A facade is where a multi-step consistency problem gets one owner. Every caller wants the result of several steps, and partial failure in the middle must leave the system in a known state. The same structure shows up as kubectl apply over a sequence of API calls, as terraform apply over plan, provider calls and state writes, and as a database's COMMIT, which hides log writes, flushes and lock release behind one statement. When the steps cross services, the facade becomes a saga orchestrator with compensations (see the Saga lesson).
Down the stack
Layer 1The contract
The facade's contract is stated in terms of the task, not the parts. For write_atomic(path, data): on success, a reader opening path sees exactly data, and it will still see it after a crash or power loss; at no point can a reader see a partial file. On failure before the rename, the original file is unchanged and the temporary file is removed. If the rename succeeded but the directory flush failed, the new content is in place but its durability is not guaranteed, and the caller is told. The contract also names what is not promised: two concurrent writers to one path both succeed and the last rename wins, so callers that need ordering must serialise writes themselves.
Layer 2The mechanism
POSIX rename(2) replaces the target atomically: any process that opens the path gets either the old inode or the new one. fsync(2) on the file descriptor flushes the file's data and metadata to the device and asks it to empty its volatile write cache; the man page notes that this does not make the directory entry durable, which needs a separate fsync on a descriptor for the directory. Go's os.WriteFile is OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm), Write and Close, with no fsync and no rename, so it is a convenience facade, not a safe-save facade. git does safe saves with lock files: it creates index.lock with O_CREAT|O_EXCL (which fails if another git process holds the lock), writes the new index there, and renames it over index. The same layering appears in HTTP: requests.get creates a Session, which routes through an HTTPAdapter to urllib3's PoolManager, which keeps one connection pool per host.
Layer 3The machine
The facade call itself costs nothing measurable: one more stack frame, often inlined. What matters is what it hides. rename is a metadata update in the filesystem journal, a few microseconds. Each fsync waits for the device: tens of microseconds on an NVMe drive with power-loss protection, 1 to 10 ms on consumer SSDs that must flush their cache, and 5 to 20 ms on spinning disks. Two fsync calls per save is invisible for a settings file written once a minute. The same facade called in a loop over 10,000 small records costs 20 to 200 seconds, where batching the records into one file would cost one save.
Other hidden costs follow the same pattern. Go's http.DefaultClient has no timeout, so http.Get against a server that stops responding blocks forever, and requests likewise waits indefinitely unless a timeout is passed. On macOS, fsync does not force the drive to flush its cache; fcntl(F_FULLFSYNC) does. Filesystems differ too: after ext4 introduced delayed allocation, applications that renamed a new file over an old one without fsync lost data in crashes, and ext4 added the auto_da_alloc heuristic to flush data on such renames. A facade is the one place to handle these per-platform details, and its documentation is the one place to warn about cost.
Why it's built this way
- Many tasks need several low-level calls in a precise order, and getting the order or the cleanup wrong causes rare, expensive failures.
- Repeating the sequence at every call site multiplies the chances of getting it wrong, so it moves behind one task-shaped method.
- The parts stay accessible because some callers need control the facade does not offer.
- Hiding steps also hides their cost and their limits, so the contract states durability, timeouts and concurrency explicitly.
- A facade that grows to cover every task becomes a god object, so facades are split by task or by caller.
Diagram
sequenceDiagram
participant C as Caller
participant F as SafeFiles
participant FS as Filesystem
C->>F: write_atomic("a/cfg.json", data)
F->>FS: write_file("a/cfg.json.tmp", data)
F->>FS: fsync("a/cfg.json.tmp")
F->>FS: rename(tmp, "a/cfg.json")
F->>FS: fsync_dir("a")
F-->>C: ok
Note over F,FS: failure before rename: remove tmp, original intact
Note over F,FS: failure at fsync_dir: new file in place, error returnedWorked example
The sync client team replaced the six write paths with SafeFiles.write_atomic. Each state file is under 8 KB and is written at most once every 30 seconds, so two fsync calls (about 4 ms on a typical laptop SSD) cost under 0.02% of one core's time. Corrupted-state reports went from roughly 900 a month (0.3% of 300,000 active users after a major OS update) to fewer than 5, all of which traced to disks that were already failing. Each avoided full resync had been costing about 2 GB of download per user.
One regression appeared in review. A migration that rewrote 40,000 per-folder metadata files through the facade took 11 minutes on a spinning-disk test machine: 80,000 synchronous flushes. The fix was not to bypass the facade but to add a second task-shaped method, write_many_atomic, that writes and flushes all temp files, renames them, and flushes each affected directory once. The migration dropped to 40 seconds. The facade grew because a new task appeared, not because a caller needed the parts.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Call the subsystem directly | Full control; no extra layer | Sequence and cleanup repeated; easy to get wrong | One caller, or the caller is itself infrastructure |
| Task-shaped facade | One owner for order, errors and platform quirks | Hides cost; can grow into a god object | Several callers need the same multi-step task |
| Facade plus exposed parts | Easy path and power path both available | Two surfaces to document and support | Libraries and tools with varied users (git, requests) |
| Service layer or application service | Keeps handlers thin; testable without HTTP | Becomes a dumping ground without boundaries | Business actions spanning several repositories or clients |
| API gateway or BFF | One endpoint per client task over many services | Network hop; shared bottleneck; ownership disputes | Clients that would otherwise call many services |
Pitfalls in production
- A convenience facade assumed to be durable (
os.WriteFile,Path.write_text) for state that must survive a crash. Fix: a safe-save facade with temp file,fsync, rename and directoryfsync. - Facades with no timeout by default, so one slow dependency hangs every caller. Fix: require a timeout or set a sane default, and expose it.
- A facade called in a loop, multiplying hidden I/O or round trips. Fix: document cost per call and add batch methods for bulk tasks.
- Swallowed errors that make failures impossible to diagnose. Fix: return or raise the underlying error, wrapped with the step that failed.
- Partial failure leaving debris (temp files, half-created resources). Fix: cleanup on every failure path, and tests that inject failure at each step.
- A facade that grows to dozens of unrelated methods. Fix: split by task or by caller, and keep each facade's purpose to one sentence.
Leadership lens
- Ask what the facade guarantees and what it hides: durability, timeouts, retries, cost per call. Those answers belong in its documentation, not in an incident review.
- Facades are a good place to encode hard-won operational knowledge once (safe saves, timeouts, retries) and make the right thing the easy thing for every team.
- Keep the parts reachable. A facade that is the only way in becomes a bottleneck for every team that needs one more option.
- For agents, a facade is a strong instruction: "use
SafeFiles.write_atomicfor any state file" is reviewable and greppable, while "make writes safe" is not.
Check yourself
os.WriteFilewrites a whole file in one call. Why is it not safe for state that must survive a crash?Show answer
It truncates the existing file and then writes, with no
fsyncand no rename, so a crash in between can leave an empty or partial file, and even a complete write may still be only in the page cache.Why must the temporary file be in the same directory as the target?
Show answer
renameis atomic only within one filesystem; across filesystems it fails, and a copy-and-delete fallback would expose partial content.What is the difference between a facade and a mediator?
Show answer
Parts behind a facade do not know the facade; it calls them. In a mediator, the parts know the mediator and communicate with each other through it.
A facade method is fast in development but slow in a batch job. What is the likely cause, and the fix?
Show answer
It hides per-call I/O or round trips that add up in a loop. Add a batch method that does the expensive step once per batch rather than once per item.
Watch
- Facade Pattern, Design Patterns ep 9 (Christopher Okhravi): the structure and intent of facade, and how it differs from adapter.
- 10 Design Patterns Explained in 10 Minutes (Fireship): a quick JavaScript example of facade alongside the other common patterns.
Free resources
- Git Internals: Plumbing and Porcelain (Pro Git): a famous facade with its parts deliberately left accessible.
- rename(2) (Linux man page): the atomic replacement guarantee that safe-save facades depend on.
- fsync(2) (Linux man page): what is flushed, and why the directory needs its own
fsync. - Ensuring data reaches disk (LWN): the full sequence of calls a durable write needs, and where applications get it wrong.
- Files are hard (Dan Luu): research on how often real applications get crash consistency wrong.
- os package source (Go):
WriteFilein eleven lines, a convenience facade with nofsync.