A5 · Part 3 · Advanced
Patterns & architecture
Shapes that show up again and again once code has to scale past one file.
08On this page
Functional programming basics
A pure function's output depends only on its inputs, and it touches nothing outside itself — no network call, no mutating an argument, no reading a global. The upside isn't philosophical: a pure function is trivially testable (call it, check the return value, no setup), safely memoizable (already covered), and safe to run in any order or in parallel, since it can't step on anything else's state.
// impure — depends on and mutates something outside itself
let discount = 0.1;
function applyDiscount(price) { return price - price * discount; }
// pure — same inputs, same output, forever, no matter what else is happening
function applyDiscountPure(price, rate) { return price - price * rate; }
Immutability — building new values instead of changing
existing ones — is what keeps a codebase full of pure functions
actually pure; it's the same idea
already covered for why
React checks === instead of deep-comparing.
A transducer is a composable transformation that's independent
of the collection it eventually runs against — instead of
arr.map(f).filter(p) building one throwaway intermediate
array between the two steps, a transducer combines map
and filter into a single combined step function,
run once per element, zero intermediate arrays:
const mapping = (fn) => (reducer) => (acc, val) => reducer(acc, fn(val));
const filtering = (pred) => (reducer) => (acc, val) => (pred(val) ? reducer(acc, val) : acc);
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const push = (acc, val) => (acc.push(val), acc);
const transform = compose(mapping((x) => x * 2), filtering((x) => x > 5));
console.log([1, 2, 3, 4, 5].reduce(transform(push), [])); // what happens?
[6, 8, 10] — every element is doubled, then kept only if
the doubled value clears 5, all inside one
reduce pass with no intermediate array built between the
two steps. This is a genuinely deep rabbit hole (it's the core idea
behind libraries like transducers-js) — the takeaway at
this level is what problem it solves: composing transformations
without paying for an intermediate array at every step.
Dry run: the transducer pipeline over [1, 2, 3, 4, 5]
| x | doubled (mapping step) | doubled > 5? (filtering step) | pushed to acc? |
|---|---|---|---|
| 1 | 2 | no | no |
| 2 | 4 | no | no |
| 3 | 6 | yes | yes |
| 4 | 8 | yes | yes |
| 5 | 10 | yes | yes |
Final acc: [6, 8, 10] — and the whole point
of a transducer is visible in this table: every element goes through
the mapping step, then straight into the filtering step, in a single
combined pass, with no intermediate array of doubled values ever built
between the two steps.
Design patterns, in JS terms
| Pattern | Shape | Already seen it |
|---|---|---|
| Module | a closure exposing a small public surface, hiding the rest | closures chapter, use #2 |
| Observer / Pub-Sub | subscribers register a callback; a publisher calls every one when something happens | addEventListener IS this pattern, built into the platform |
| Strategy | swap the algorithm at runtime by passing a different function/object with the same interface | the comparator argument to .sort() |
| Factory | a function that builds and returns objects, hiding the construction details | document.createElement |
| Singleton | exactly one instance, created lazily on first request | an ES module itself — importing it twice gives the same instance, module caching does this for free |
class EventBus {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(fn);
return () => this.off(event, fn); // returns its own unsubscribe function
}
off(event, fn) {
const fns = this.#listeners.get(event);
if (fns) this.#listeners.set(event, fns.filter((f) => f !== fn));
}
emit(event, ...args) {
(this.#listeners.get(event) || []).forEach((fn) => fn(...args));
}
}
const bus = new EventBus();
const unsubscribe = bus.on("greet", (name) => console.log("hello", name));
bus.emit("greet", "Ana");
unsubscribe();
bus.emit("greet", "Ravi"); // what happens?
Only "hello Ana" prints — the second
emit finds no listeners left, because calling the
function on() returned removed it. This exact shape,
hand-rolled, is what every pub/sub library and every framework's
event system is doing underneath, whether it's 20 lines like this one
or a much larger implementation.
Middleware — a chain of functions, each deciding to continue or stop
function compose(middlewares) {
return function (context) {
let index = -1;
function dispatch(i) {
if (i <= index) throw new Error("next() called multiple times");
index = i;
const fn = middlewares[i];
if (!fn) return;
return fn(context, () => dispatch(i + 1)); // "next" — hands control to the next middleware
}
return dispatch(0);
};
}
const logger = (ctx, next) => { console.log("->", ctx.path); next(); console.log("<-", ctx.path); };
const auth = (ctx, next) => { if (!ctx.user) return console.log("blocked"); next(); };
const handler = (ctx) => console.log("handled", ctx.path);
const app = compose([logger, auth, handler]);
app({ path: "/orders", user: { id: 1 } });
This exact shape — an ordered list of functions, each one receiving a
next it chooses whether and when to call — is what
Express, Koa, and Redux middleware all are underneath, whether it's
handling an HTTP request or an action dispatched to a store. Skipping
next() (as auth does when there's no user)
short-circuits everything after it in the chain; calling it lets
execution continue, and code written after the
next() call runs on the way back out — which is why
logger can log both entry and exit around everything
nested inside it.
Dependency injection
A function or class that receives what it depends on instead of reaching out and constructing or importing it directly.
// tightly coupled — this function can ONLY ever hit the real API
async function loadUser(id) {
return fetch("/api/users/" + id).then((r) => r.json());
}
// injected — the caller decides what "fetch a user" actually means
async function loadUserWith(fetchImpl, id) {
return fetchImpl(id);
}
loadUserWith(realApiFetch, 1); // production
loadUserWith(fakeFetchForTests, 1); // tests — no real network needed
Inversion of control is the broader principle this is one instance of: instead of a piece of code deciding and calling its own dependencies, something outside it decides and hands them in. A framework calling your component function, instead of your code calling into the framework, is the same inversion at a larger scale.
State machines
function createTrafficLight() {
const transitions = { red: "green", green: "yellow", yellow: "red" };
let state = "red";
return {
next() { state = transitions[state]; return state; },
current() { return state; },
};
}
const light = createTrafficLight();
console.log(light.current()); // what happens?
console.log(light.next(), light.next(), light.next()); // what happens?
"red", then "green" "yellow" "red" — the
entire idea of a state machine in one small table: a fixed set of
named states, and one function per state that says exactly what the
next state is allowed to be. The value over a scattering of booleans
(isLoading, isError, isSuccess,
all mutable independently) is that an impossible combination
— loading AND error AND success all true at once — simply can't be
represented at all, instead of being a bug waiting to happen.
Signals — reactivity without a virtual DOM diff
function createSignal(initial) {
let value = initial;
const subscribers = new Set();
return {
get() { return value; },
set(next) {
value = next;
subscribers.forEach((fn) => fn(value));
},
subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); },
};
}
const count = createSignal(0);
count.subscribe((v) => console.log("count is now", v));
count.set(1);
count.set(2);
A signal is a value plus its own list of subscribers, bundled
together — reading it is a normal function call, writing it directly
notifies whoever's listening, with no framework re-render or
virtual-DOM diff involved at all. This is the primitive underneath
Solid, Preact Signals, Angular's newer reactivity model, and Vue's
ref() — the pitch over React's re-render-the-whole-component
model is fine-grained updates: a signal changing can update
exactly the one DOM node that reads it, skipping every component in
between that never actually used that value.
A real implementation adds one piece this sketch leaves out:
automatic dependency tracking — a computed value that reads
count.get() inside its own function body subscribes
itself automatically, with no explicit .subscribe() call
written anywhere. That's the part that makes signals feel declarative
rather than manually wired — this hand-rolled version is the mental
model, not the full implementation.
Error boundaries and resilience
function withFallback(fn, fallback) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
console.error("recovered from:", error);
return fallback;
}
};
}
const safeLoad = withFallback(loadUserProfile, { name: "Guest" });
React's actual ErrorBoundary component is this same
idea at the UI layer — catch a failure from a whole subtree of
components, render a fallback UI instead of taking down the entire
page. The general architectural principle underneath both: contain a
failure at the smallest boundary that can meaningfully recover from
it, instead of letting it propagate and take out something much
bigger that didn't need to fail too.
API design
A request is idempotent if making it twice has the exact same
effect as making it once. PUT /users/1 { name: "Ana" }
is idempotent — running it five times still leaves the name
"Ana". POST /users to create a new one
usually isn't — five identical calls create five accounts.
Rule Idempotency is exactly what makes a
retry-with-backoff safe to write
blindly. Retrying an idempotent request after a timeout is harmless —
it might have already succeeded, and running it again changes
nothing. Retrying a non-idempotent one risks a real duplicate,
usually solved with a client-generated idempotency key the
server deduplicates by.
Caching closes the loop: the same request, made again, doesn't
even need to reach the server. An HTTP Cache-Control
header, an in-memory Map keyed by request, or the
memoize pattern from the closures
chapter are all the identical idea at different layers of the
stack — don't redo work whose answer hasn't changed.
Interview answer, one sentence
"Patterns are named shapes for recurring problems — middleware as a chain of functions, dependency injection to keep code testable, state machines for modes, signals for fine-grained reactivity — and each is worth reaching for only once the plainer version has started to hurt."
Before you move on
- Explain what makes a function "pure" and why that makes it trivially testable and safely memoizable.
- Trace why
bus.emit("greet", "Ravi")afterunsubscribe()logs nothing, using the EventBus's own#listenersmap. - Explain what
next()does in the middleware chain, and why code written afternext()runs on the way back out. - Say why an ES module behaves like a Singleton with no extra code, purely from how imports are cached.
- Explain why
PUTis idempotent andPOSTusually isn't, and how that decides whether a retry after a timeout is safe.
Opens in the editor: write it, run it, and check it against real tests.
All 31 for this level →Finished reading?
0 of 41 chapters read in this series