Patterns & architecture
Shapes that show up again and again once code has to scale past one file.
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.
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.
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.
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.
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 three
chapters back are all the identical idea at different layers of the
stack — don't redo work whose answer hasn't changed.
Opens in the editor — write it, run it, and check it against real tests.