Lesson 11 / 30
Facade
Simplifying a complex subsystem: comparing, by direct dependency count, import closure, and known-step count, the arrangement where clients sequence a six-step shipment acceptance flow themselves against the arrangement placing it behind a single entry point; measuring the difference that shows up when a step is skipped; counting the files edited when a new step is added to the flow.
Contents
The code assembling the decorator chain had to know which additions to apply in which order. That is tolerable knowledge for a single call. When the whole library works this way, the client has to learn every sequential step.
Shipment acceptance is six steps: fix the address, resolve the zone, compute the base fee, apply the additions, choose the carrier, find the delivery date. The order is not arbitrary — the zone comes from the fixed postal code, the fee depends on the zone, and so does carrier coverage. If two clients know these six steps separately, the same order sits in two places, and skipping a step surfaces as a wrong amount at run time, with no error. The facade places a single entry point in front of the subsystem. The measures: the client’s direct dependency count, the import closure size, and how many files hold the sequential-step knowledge.
Problem: Order Knowledge Lives in the Client
The subsystem is six small modules; each does one job and knows nothing of the others.
// sub/address.mjs — subsystem 1: normalizes the address export const fixAddress = (address) => ({ postalCode: String(address.postalCode ?? "").replace(/\s+/g, ""), province: String(address.province ?? "").trim(), });
// sub/zone.mjs — subsystem 2: fee zone from postal code const POSTAL_ZONE = { 34: "near", "06": "mid", 35: "mid", 65: "far" }; export const findZone = (postalCode) => POSTAL_ZONE[postalCode.slice(0, 2)] ?? "unknown";
// sub/tariff.mjs — subsystem 3: weight tier and zone coefficient const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]]; const COEFFICIENT = { near: 100, mid: 118, far: 145, unknown: 165 }; export const baseFee = (weight, zone) => { const t = TIER.find(([cap]) => weight <= cap) ?? [0, 24990]; return Math.round((t[1] * COEFFICIENT[zone]) / 100); };
// sub/additions.mjs — subsystem 4: insurance and tax additions export const applyAdditions = (amount, options, value) => { const insured = options.insurance === true ? Math.max(500, Math.round(value * 0.005)) : 0; return Math.round((amount + insured) * (options.tax === true ? 1.2 : 1)); };
// sub/carrier.mjs — subsystem 5: cheapest carrier by zone const MULTIPLIER = { ground: 1, air: 1.35, sea: 0.9 }; const COVERAGE = { near: ["ground", "air", "sea"], mid: ["ground", "air"], far: ["ground"], unknown: ["ground"] }; export const cheapestCarrier = (amount, zone) => COVERAGE[zone] .map((t) => ({ carrier: t, amount: Math.round(amount * MULTIPLIER[t]) })) .sort((a, b) => a.amount - b.amount)[0];
// sub/delivery.mjs — subsystem 6: delivery date by zone const DAYS = { near: 1, mid: 2, far: 4, unknown: 5 }; export const deliveryDate = (start, zone) => { const t = new Date(`${start}T00:00:00Z`); t.setUTCDate(t.getUTCDate() + DAYS[zone]); return t.toISOString().slice(0, 10); };
// requests.mjs — the requests both arrangements process export const REQUESTS = [ { code: "G-1", weight: 0.8, value: 120000, address: { postalCode: " 34100 ", province: " Istanbul " }, options: { insurance: true, tax: true }, date: "2026-03-02" }, { code: "G-2", weight: 12, value: 40000, address: { postalCode: "06500", province: "Ankara" }, options: { tax: true }, date: "2026-03-02" }, { code: "G-3", weight: 26, value: 900000, address: { postalCode: "65100", province: "Van" }, options: {}, date: "2026-03-30" }, ];
In the first arrangement, two clients sequence the six modules themselves. The second client returns a different result but repeats the same six steps from scratch.
// direct/accept.mjs — client sequences the six modules itself import { fixAddress } from "../sub/address.mjs"; import { findZone } from "../sub/zone.mjs"; import { baseFee } from "../sub/tariff.mjs"; import { applyAdditions } from "../sub/additions.mjs"; import { cheapestCarrier } from "../sub/carrier.mjs"; import { deliveryDate } from "../sub/delivery.mjs"; export function accept(request) { const address = fixAddress(request.address); const zone = findZone(address.postalCode); const base = baseFee(request.weight, zone); const amount = applyAdditions(base, request.options, request.value); const choice = cheapestCarrier(amount, zone); return { code: request.code, zone, ...choice, date: deliveryDate(request.date, zone) }; }
// direct/quote.mjs — second client repeats the same sequence from scratch import { fixAddress } from "../sub/address.mjs"; import { findZone } from "../sub/zone.mjs"; import { baseFee } from "../sub/tariff.mjs"; import { applyAdditions } from "../sub/additions.mjs"; import { cheapestCarrier } from "../sub/carrier.mjs"; import { deliveryDate } from "../sub/delivery.mjs"; export function quote(request) { const address = fixAddress(request.address); const zone = findZone(address.postalCode); const base = baseFee(request.weight, zone); const amount = applyAdditions(base, request.options, request.value); const choice = cheapestCarrier(amount, zone); return { amount: choice.amount, date: deliveryDate(request.date, zone) }; }
Solution: A Single Entry Point in Front of the Subsystem
The facade does not hide the subsystem; it opens one path to it. It carries no business rule — only order, and two external operations.
// facade/accept-facade.mjs — facade: the only place that knows the subsystem's order, exposes two operations import { fixAddress } from "../sub/address.mjs"; import { findZone } from "../sub/zone.mjs"; import { baseFee } from "../sub/tariff.mjs"; import { applyAdditions } from "../sub/additions.mjs"; import { cheapestCarrier } from "../sub/carrier.mjs"; import { deliveryDate } from "../sub/delivery.mjs"; const calculate = (request) => { const address = fixAddress(request.address); const zone = findZone(address.postalCode); const base = baseFee(request.weight, zone); const amount = applyAdditions(base, request.options, request.value); return { zone, choice: cheapestCarrier(amount, zone), date: deliveryDate(request.date, zone) }; }; export function accept(request) { const s = calculate(request); return { code: request.code, zone: s.zone, ...s.choice, date: s.date }; } export function quote(request) { const s = calculate(request); return { amount: s.choice.amount, date: s.date }; }
// facade/accept.mjs — same client, single dependency import { accept as facadeAccept } from "./accept-facade.mjs"; export const accept = (request) => facadeAccept(request);
// facade/quote.mjs — second client, single dependency import { quote as facadeQuote } from "./accept-facade.mjs"; export const quote = (request) => facadeQuote(request);
// run.mjs — do the two arrangements produce the same acceptance record and the same quote import { REQUESTS } from "./requests.mjs"; import { accept as directAccept } from "./direct/accept.mjs"; import { quote as directQuote } from "./direct/quote.mjs"; import { accept as facadeAccept } from "./facade/accept.mjs"; import { quote as facadeQuote } from "./facade/quote.mjs"; let mismatched = 0; for (const request of REQUESTS) { const a = JSON.stringify([directAccept(request), directQuote(request)]); const b = JSON.stringify([facadeAccept(request), facadeQuote(request)]); if (a !== b) mismatched += 1; console.log(`${request.code} ${JSON.stringify(directAccept(request))}`); } console.log(`mismatched results = ${mismatched} / ${REQUESTS.length}`);
G-1 {"code":"G-1","zone":"near","carrier":"sea","amount":6037,"date":"2026-03-03"}
G-2 {"code":"G-2","zone":"mid","carrier":"ground","amount":21226,"date":"2026-03-04"}
G-3 {"code":"G-3","zone":"far","carrier":"ground","amount":36236,"date":"2026-04-03"}
mismatched results = 0 / 3
Measuring Dependency and Closure
The measurement combines two separate measures from the Design Principles course: direct dependency count (a file’s own import lines) and import closure size (every file that file is indirectly connected to).
// graph.mjs — the clients' direct dependency count, import closure, and files aware of step order import { readdirSync, readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; const STEP = ["fixAddress", "findZone", "baseFee", "applyAdditions", "cheapestCarrier", "deliveryDate"]; const imports = (path) => [...readFileSync(path, "utf8").matchAll(/from "(\.[^"]+)"/g)] .map((m) => resolve(dirname(path), m[1])); const closure = (path, seen = new Set()) => { for (const c of imports(path)) if (!seen.has(c)) { seen.add(c); closure(c, seen); } return seen; }; for (const path of ["direct/accept.mjs", "direct/quote.mjs", "facade/accept.mjs", "facade/quote.mjs"]) { const text = readFileSync(path, "utf8"); const step = STEP.filter((a) => text.includes(a)).length; console.log(`${path.padEnd(20)} direct dependency=${imports(path).length} import closure=${closure(path).size} ` + `known steps=${step}/6`); } const surface = (files) => files .reduce((t, y) => t + (readFileSync(y, "utf8").match(/^export /gm) ?? []).length, 0); console.log(`subsystem surface = ${surface(readdirSync("sub").map((d) => `sub/${d}`))} functions, ` + `facade surface = ${surface(["facade/accept-facade.mjs"])} operations`);
direct/accept.mjs direct dependency=6 import closure=6 known steps=6/6 direct/quote.mjs direct dependency=6 import closure=6 known steps=6/6 facade/accept.mjs direct dependency=1 import closure=7 known steps=0/6 facade/quote.mjs direct dependency=1 import closure=7 known steps=0/6 subsystem surface = 6 functions, facade surface = 2 operations
Three numbers say three different things. Direct dependency dropped from 6 to 1. Known steps dropped from 6/6 to 0/6: order knowledge left both clients and gathered into a single file. By contrast, the import closure did not shrink — it went from 6 to 7 — the facade itself was added to the closure as a file. The facade does not reduce build dependency; what it reduces is the number of names and the order the client must know. For the closure to actually shrink, part of the subsystem would have had to go unused.
The last line gives the facade’s surface: the subsystem offers 6 functions, the facade 2 operations. This narrowing is as much a cost as a gain; it is measured below.
When a Step Is Skipped
The cost of order knowledge sitting in the client is that a skipped step produces a wrong result without an error.
// skipped.mjs — what happens when the first step is skipped in the direct arrangement import { REQUESTS } from "./requests.mjs"; import { accept } from "./direct/accept.mjs"; import { findZone } from "./sub/zone.mjs"; import { baseFee } from "./sub/tariff.mjs"; import { applyAdditions } from "./sub/additions.mjs"; import { cheapestCarrier } from "./sub/carrier.mjs"; import { deliveryDate } from "./sub/delivery.mjs"; // Same order, missing one step: fixAddress was not called. function acceptSkipping(request) { const zone = findZone(request.address.postalCode); const base = baseFee(request.weight, zone); const amount = applyAdditions(base, request.options, request.value); const choice = cheapestCarrier(amount, zone); return { code: request.code, zone, ...choice, date: deliveryDate(request.date, zone) }; } const g = REQUESTS[0]; const correct = accept(g), skipped = acceptSkipping(g); console.log(`full order : ${JSON.stringify(correct)}`); console.log(`step skipped: ${JSON.stringify(skipped)}`); const days = (Date.parse(skipped.date) - Date.parse(correct.date)) / 86400000; console.log(`amount difference = ${skipped.amount - correct.amount} cents, delivery difference = ${days} days`);
full order : {"code":"G-1","zone":"near","carrier":"sea","amount":6037,"date":"2026-03-03"}
step skipped: {"code":"G-1","zone":"unknown","carrier":"ground","amount":10601,"date":"2026-03-07"}
amount difference = 4564 cents, delivery difference = 4 days
A single space in the postal code turned the zone “unknown”; the fee came out 4564 cents high, the delivery date 4 days late, and no error was thrown. In the facade arrangement, this mistake cannot be made in the client, because the client calls no step at all.
When a New Step Enters the Flow
The new requirement is prohibited-item screening, and it enters the middle of the flow, right after zone resolution.
mkdir -p new && cp -r sub direct facade requests.mjs run.mjs graph.mjs new/ ls new/sub
additions.mjs address.mjs carrier.mjs delivery.mjs tariff.mjs zone.mjs
// new/sub/screening.mjs — subsystem 7: prohibited item screening const PROHIBITED = new Set(["flammable", "corrosive"]); export const isProhibited = (request) => (request.contents ?? []).some((i) => PROHIBITED.has(i));
cd new sed -i.y -e 's#^import { baseFee } from "../sub/tariff.mjs";#&\nimport { isProhibited } from "../sub/screening.mjs";#' \ -e 's#^ const zone = findZone(address.postalCode);#&\n if (isProhibited(request)) return { code: request.code, rejected: "prohibited item" };#' direct/accept.mjs sed -i.y -e 's#^import { baseFee } from "../sub/tariff.mjs";#&\nimport { isProhibited } from "../sub/screening.mjs";#' \ -e 's#^ const zone = findZone(address.postalCode);#&\n if (isProhibited(request)) return { rejected: "prohibited item" };#' direct/quote.mjs sed -i.y -e 's#^import { baseFee } from "../sub/tariff.mjs";#&\nimport { isProhibited } from "../sub/screening.mjs";#' \ -e 's#^ const zone = findZone(address.postalCode);#&\n if (isProhibited(request)) return { rejected: "prohibited item" };#' \ -e 's#^ return { code: request.code, zone: s.zone, ...s.choice, date: s.date };# if (s.rejected !== undefined) return { code: request.code, rejected: s.rejected };\n&#' \ -e 's#^ return { amount: s.choice.amount, date: s.date };# if (s.rejected !== undefined) return { rejected: s.rejected };\n&#' facade/accept-facade.mjs sed -i.y 's# { code: "G-3", weight: 26# { code: "G-4", weight: 2, value: 5000, address: { postalCode: "34100", province: "Istanbul" },\n options: {}, date: "2026-03-02", contents: ["flammable"] },\n { code: "G-3", weight: 26#' requests.mjs rm -f direct/*.y facade/*.y *.y node run.mjs | tail -2 node graph.mjs | head -2 for d in direct facade; do echo "$d edited: $(diff -rq "../$d" "$d" | grep '^Files ' | sed 's#.*/\([a-z-]*\.mjs\).*#\1#' | tr '\n' ' ')" done
G-3 {"code":"G-3","zone":"far","carrier":"ground","amount":36236,"date":"2026-04-03"}
mismatched results = 0 / 4
direct/accept.mjs direct dependency=7 import closure=7 known steps=6/6
direct/quote.mjs direct dependency=7 import closure=7 known steps=6/6
direct edited: accept.mjs quote.mjs
facade edited: accept-facade.mjs
Both clients were edited in the direct arrangement and direct dependency count went from 6 to 7. In the facade arrangement, only the facade was edited; both client files stayed as they were and the dependency count held at 1. Both arrangements gave the same answer for all four requests — both rejected G-4, which carries a prohibited item. The gap grows linearly with client count: with ten clients, the direct arrangement would edit ten files, the facade arrangement still one.
Cost and When Not to Apply It
The facade has three costs. The first is one file and a level of indirection; the import closure grew by one too. The second is the surface narrowing: the subsystem offers 6 functions against the facade’s 2 operations. A capability the facade does not pass through — resolving only the zone, computing a fee without choosing a carrier — is closed to the client. The pattern does not seal off the subsystem; its modules stay usable — but the moment a client goes straight to the subsystem, the order guarantee the facade provided ends.
The third is the most common decay: the facade starting to accumulate business rules. The facade here holds no decision at all — only call order. The moment a discount threshold, a rejection rule, or a tariff choice gets written into the facade, that file becomes the subsystem’s seventh module, and a pile of responsibility grows that nothing measures. The pattern’s boundary is this: the facade knows order, not rules.
It does not apply in two situations. If the subsystem is a single module with a single call, there is no order knowledge; the facade only renames. If each client uses a different subset of the subsystem in a different order, a single facade fits none of them; every option added to the facade then piles up as a flag parameter, and the flag cost measured in the Clean Code course returns.
Summary
- The problem the facade solves is sequential-call knowledge repeating in every client; the solution is placing a single entry point that knows the order in front of the subsystem.
- Direct dependency count per client dropped from 6 to 1, known step count from 6/6 to 0/6; the results for three requests stayed the same across both arrangements.
- The import closure grew from 6 to 7: the facade does not reduce build dependency, it reduces the number of names and the order the client must know.
- In the direct arrangement, skipping a step raised no error; the fee drifted 4564 cents and the delivery date 4 days. When a seventh step entered the flow, both clients were edited in the direct arrangement, only the facade in the facade arrangement.
- The cost is one file, a level of indirection, and the surface narrowing from 6 functions to 2 operations; the facade knows order, not rules — the moment rules accumulate, it turns into a module of the subsystem.
Next Step
The four patterns so far each built a different way of combining objects, and every measure was files, types, or dependency count. The next problem measures a different resource: memory. If a tariff object gets built for every shipment during fee calculation, the same tier table is copied hundreds of thousands of times in a batch job with hundreds of thousands of shipments. Yet the table’s content does not vary by shipment; what varies is only weight and address. The next lesson counts the object count, writes the flyweight pattern that gathers the unchanging information into a shared object, and shows the gain both in object count and in a heap measurement flagged as environment-dependent.
To keep your progress and take notes, Log in
My notes
Log in to take notes.