Skip to content
academia.sh

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.

Start typing to search.

↑↓ Esc navigate · open · close