Skip to content

9.4.24 — Patterns in the JavaScript and TypeScript Wild

Open any JavaScript codebase and you will find the patterns from this folder already there, wearing different clothes and answering to different names. Nobody wrote a comment saying "Decorator here". They wrote withAuth(handler). Nobody labelled the Chain of Responsibility; they wrote app.use(...) six times in a row.

This page teaches you to read those disguises. By the end you will be able to open an unfamiliar repository and say, out loud and correctly, what shapes it is built from — which is the single most useful thing pattern knowledge does for you in a real job. What is the Module Pattern with namespace objects, or the Revealing Module Pattern? [EQ-46]

1. JavaScript ate half the catalog

Something happened to JavaScript that did not happen to most languages. The community invented patterns to work around features the language was missing, and then the language grew those features and swallowed the patterns whole.

That history is why some of the Gang of Four book reads as ancient in JavaScript and some of it reads as current. Knowing which is which stops you from hand-building something the language already gives you.

The patterns that became syntax or built-ins:

PatternWhat it is now in JavaScriptWhere the book covers it
IteratorSymbol.iterator, for…of, generators3.6.6
ObserverEventEmitter in Node, EventTarget in browsers9.4.13
Prototypethe object model itself3.6.4
Proxya built-in class, new Proxy(target, handler)9.4.10
Strategy, Commandplain functions, passed around as values9.4.12, 9.4.15

Look at the last row for a moment, because it explains why so much pattern writing feels heavy to a JavaScript developer. In a language where a function cannot be stored in a variable, "make the algorithm swappable" genuinely requires an interface, a class per algorithm, and a field to hold the current one. In JavaScript, it requires a variable. The tension the pattern solves is identical; the ceremony needed to solve it is not.

A line worth having ready in an interview: JavaScript is the language where half the Gang of Four book turned into syntax. It shows that you know both the catalog and why it is not applied literally here.

The patterns the ecosystem standardised on rather than the language. Callbacks became promises, then async/await — an entire community migrating from one pattern to another over about eight years (3.6.8). Error-first callbacks, where the first argument to your function is the error, were a pattern that lived purely as a convention nobody could enforce (3.8.5). And middleware — the (request, next) shape from 9.4.16 — became the shared language of every server framework, which is why learning it once lets you read all of them.

2. The module story, in code

JavaScript had no way to keep anything private and no way to organise code into units until 2015. Four generations of workaround led to what you write today, and the last generation is still worth knowing because you will write it deliberately, not just meet it in old code.

Generation 1 — the namespace object

javascript
var MyApp = MyApp || {};                      // (1)
MyApp.cart = {                                // (2)
  items: [],                                  // (3)
  add: function (item) { MyApp.cart.items.push(item); },
};

Line (1) creates one global object, reusing it if some other file made it first. Line (2) hangs a group of related things off that one global. Line (3) is the problem: items is right there, so any code anywhere can write MyApp.cart.items = [] and nothing stops it.

What this bought: one global name instead of fifty. What it did not buy: any privacy at all.

Generation 2 — the module pattern

javascript
var cart = (function () {                     // (1)
  var items = [];                             // (2)
  function add(item) { items.push(item); }    // (3)
  function total() { return items.length; }
  return { add: add, total: total };          // (4)
})();                                         // (5)

Line (1) opens a function, and line (5) calls it immediately. That combination has a name you will see in old code and old interview questions: an IIFE, which stands for immediately invoked function expression, and means nothing more than "a function written and called on the spot."

Line (2) is the whole point. items lives inside that function, so when the function finishes, the only things that can still reach items are the functions defined inside it. Line (3) is one of them. Line (4) hands back an object containing only the functions we want callers to have. items is now genuinely unreachable from outside — not by convention, but because the language has no way to get at it. This is closures doing the work of a private keyword (3.6.2).

Notice what pattern this is. Private internals, and one small surface in front of them, chosen for the caller's convenience. That is a Facade (9.4.9) built out of closures.

Generation 3 — the revealing module

Same mechanism, one presentation change: define everything as named functions, and let the returned object be a list of which ones are public.

javascript
var cart = (function () {
  var items = [];
  function add(item) { items.push(item); }
  function total() { return items.length; }
  function validate() { /* internal only */ } // (1)

  return { add, total };                      // (2)
})();

Line (1) is deliberately left out of line (2), and line (2) now reads as a table of contents for the module. You can tell what is public by reading one line instead of scanning the whole file. That is the entire difference from generation 2, and it is a real one for a reader.

Generation 4 — the language grows up

javascript
// cart.js
let items = [];                               // (1)
export function add(item) { items.push(item); }
export function total() { return items.length; }

Line (1) has no wrapper function and no returned object. The file itself is the private scope, and export is the reveal. The language finally does what the pattern was faking (3.6.5). What is a namespace and how is it used in practical JavaScript? [EQ-193]

The one generation that survived on purpose

Generation 2's shape did not die, because it does something modules cannot: it gives you a fresh, private copy per call.

typescript
interface Cart { add(item: LineItem): void; total(): Money; }

export function createCart(): Cart {          // (1)
  const items: LineItem[] = [];               // (2)
  return {                                    // (3)
    add(item) { items.push(item); },
    total() { return sum(items.map(i => i.price)); },
  };
}

const a = createCart();                       // (4)
const b = createCart();                       // (5)
a.add(shoes);
console.log(a.total(), b.total());            // → 8900, 0

Line (1) is a factory function (9.4.2). Line (2) declares state that only the functions on line (3) can see. Lines (4) and (5) each get their own items array, so line (5)'s cart is genuinely empty even after line (4)'s cart gets something added — which the printed output confirms.

A module cannot do this. A module's let items is one array shared by every importer, for the lifetime of the process. So when you want per-instance private state and would rather not write a class, this is the tool, and it is worth recognising by name: the module pattern, once per instance. It is the closure-shaped alternative to a class, and the two are genuinely interchangeable for most jobs (3.6.2 section 7).

3. Naming what your framework is already doing

Frameworks rarely use the Gang of Four names. Learning the translation is what lets you read a framework's documentation as recognition rather than as new material.

Express and the other server frameworks

An Express app is a Chain of Responsibility at runtime, and almost nothing else.

typescript
const app = express();                        // (1)
app.use(helmet());                            // (2)
app.use(express.json());
app.use(authenticate);
app.use("/orders", orderRoutes);              // (3)
app.use(errorHandler);                        // (4)

Line (1) is a factory function that builds the app object. Lines (2) through (4) each add one link to a chain, and the order you write them in is the order a request travels through them (9.4.16). Line (3) mounts a whole sub-chain under one path. Line (4) has to be last, because Express recognises the error handler by position and by its four arguments.

Every server framework in every language re-derives this. Learn the shape once and you can read all of them. Chapter 9.9.1 is this one idea taken all the way to production.

React

React has the richest pattern vocabulary in front-end development, and its documentation names almost none of it. Here is the translation, and every row is worth being able to say out loud.

What React calls itWhat it isWhy it matters
The component treeComposite (9.4.11)containers and leaves share one contract
children, render propsStrategy (9.4.12)behaviour passed in as a value
Higher-order componentsDecorator (9.4.8)same shape in, same shape out, one concern added
dispatch plus a reducerCommand (9.4.15) plus State (9.4.14)actions are values; the reducer is a transition function
Class lifecycle methodsTemplate Method (9.4.17)the framework owned the sequence, you filled the blanks

The fourth row is the one that pays off most in an interview, so it is worth unpacking. An action object such as { type: "ADD_ITEM", id: 7 } is a command written as data instead of as a class. Because it is data, you can log every action, save the log, and replay it — which is exactly how the browser tool that lets you step backwards through a React app's history works. You are not looking at a debugger trick. You are looking at the command log from 9.4.15, used for the purpose that pattern has always had.

The fifth row is worth reading as a story rather than a mapping. React's original design gave you a class with named methods that the framework called at fixed moments in a fixed order. That is Template Method, exactly. Then React replaced it with hooks, which are small composable functions you assemble yourself. The most-used front-end framework in the world walked from inheritance to composition in public, over several years, and gave the same reason 9.2.4 gives: one class can only ever be one variant, and the hierarchy got in the way. Design patterns for React class components. [EQ-201]

The framework-does-the-wiring family

Some frameworks — the Angular and NestJS style, and the same idea exists in Java and C# — take over object creation entirely. You annotate a class, and the framework works out what it needs and builds it for you.

Translated into this folder's vocabulary, such a framework is: a wiring tool that builds every object (9.4.2 section 10) and decides how long each one lives; annotations that are literally the TypeScript decorator feature (3.7.6); and a set of request-handling hooks under framework-specific names — guards, pipes, interceptors — which are the same chain from 9.4.16 with the links given job titles.

Nothing in the list is new after this folder. That is the point of learning the catalog: a new framework becomes a vocabulary lookup instead of a re-education.

4. Dependency injection, and how much machinery it actually needs

Dependency injection has a reputation for being complicated, and almost all of that reputation belongs to the tools, not the idea. The idea is one sentence: a piece of code should be handed the things it needs, instead of creating them itself (9.3.9).

typescript
// creating its own collaborator — no seam, hard to test
class OrderService {
  private mailer = new SendGridMailer(process.env.KEY!);   // (1)
}

// being handed its collaborator — the whole of dependency injection
class OrderService {
  constructor(private mailer: Mailer) {}                    // (2)
}

Line (1) means every test of OrderService sends real email, and swapping the mail provider means editing this file. Line (2) means the class names a role it needs and lets whoever builds it decide what fills that role. That is it. That is the entire principle, and it costs nothing.

What costs something is the machinery you use to do the handing. There are four levels, and the skill is picking the lowest one that carries your application.

Level 1 — write the wiring by hand, in one file.

typescript
// main.ts — the composition root
const db      = createPool(config.databaseUrl);
const mailer  = new SesMailer(config.awsRegion);
const orders  = new OrderService(mailer, new OrderRepo(db));
const app     = buildServer({ orders });

Everything is explicit, you can search for it, and the compiler checks it. The only pain this ever causes is that the file gets long. It carries far more of an application than most teams expect, and it is the right default for a Node service.

Level 2 — group the wiring into one factory per feature.

typescript
export function makeCheckoutModule(deps: CheckoutDeps) {
  const repo    = new OrderRepo(deps.db);
  const service = new OrderService(deps.mailer, repo);
  return { routes: orderRoutes(service) };            // the feature's public surface
}

This is the revealing module from section 2, doing architecture. Each feature hides its own internal wiring and exposes only what other parts of the app need. It handles a few hundred objects comfortably.

Level 3 — a small registry, when lifetimes start to differ. The moment some objects must be created fresh per web request while others are shared for the whole program, hand-wiring starts producing real bugs — the kind where one customer's data leaks into another customer's response because a shared object was holding per-request state. A small lookup of "name to how-to-build-it, plus how long it lives" is worth it here.

Level 4 — a full framework container. Annotations on classes, and the framework reads the constructor's types to work out what to build. What you get: the wiring becomes declarative, lifetimes are managed for you, and swapping a real thing for a fake in a test is one line. What you pay: an older, non-standard flavour of decorators (3.7.6); mistakes that used to be compile errors now surface when the app boots and tries to build the graph; and you are now inside that framework's world.

The rule for climbing. Take the lowest level that carries the app, and climb only when you can name the pain that made you climb — the wiring file became unreadable, or scope bugs shipped. Never climb because a framework tutorial started there.

Both ends of this ladder fail in a characteristic way, and it is worth knowing both.

At the bottom end, a team rejects the principle because it dislikes the tool: "we do not need dependency injection, we are not using a framework for it." The result is new scattered through business logic, no way to substitute anything, and tests that need real credentials. The principle was free and they turned it down.

At the top end, a nine-endpoint service adopts a full container because that is what the tutorial did. Now there is resolution magic to learn, boot-time failures where there used to be type errors, and an onboarding tax, in exchange for managing fifteen objects.

5. Names for the ways code goes wrong

Naming a problem is most of fixing it, because a named problem can be discussed without anyone taking it personally. Here is the recurring cast. Each one gets its tell — the thing you can actually observe — and where the fix lives.

God object. One class or module that knows about everything. It is usually called AppManager, Helper, or Utils. The tell: it appears in nearly every pull request, because every change needs it. The fix: split it along the lines of who asks for changes (9.3.5).

Anemic domain model. Your objects hold data and nothing else, and all the actual rules live in service files. The tell: service code full of if (order.status === ...) — one object reaching into another object's data to make decisions about it. The fix: move each rule to the object that owns the data it reads (9.2.2).

Shotgun surgery. One conceptual change requires editing twenty files. The tell: you fixed the bug in four places and a fifth turns up in production next week. The fix: find why that one idea is spread out, and give it one home (9.1).

Event soup. So much of the control flow runs through events that working out what causes what becomes archaeology. The tell: debugging starts with grepping for an event name. The fix: the honest-guarantees discipline in 9.4.13 — say for each event whether anyone is required to act on it.

Callback hell, resurrected. Promises fixed the original problem, then people nested .then() calls just as deeply and got it back. The tell: indentation drifting right for no reason. The fix: async/await, and returning promises instead of nesting them (3.6.8).

Boolean state soup. isLoading, isError, isEmpty, isStale on one object, where several combinations are nonsense but nothing prevents them. The tell: a bug report describing a combination you thought impossible. The fix: one state value with named states (9.4.14).

Golden hammer. The team's favourite tool applied to everything — every piece of state in a global store, every function wrapped in a class. The tell: you cannot remember the last time somebody chose differently. The fix: the tension test from 9.4.1 section 6 — name the problem before naming the tool.

Speculative generality. Extension points nobody asked for: an interface with one implementation, a config value that has never changed, a plugin system with no plugins. The tell: you can delete the abstraction and nothing breaks. The fix: delete it, and build it when the second case actually arrives (9.3.4).

Lava flow. Dead code kept because somebody might be using it. The tell: commented-out blocks, and functions with zero callers. The fix: delete it. Version control is the archive; the codebase is not.

Copy-paste divergence. Two nearly identical modules that quietly drift apart, so a bug gets fixed in one and not the other. The tell: you fix something and it comes back from the other module. The fix: ask whether the two copies change for the same reason. If yes, they are one thing and should be one thing. If no, the resemblance is a coincidence and copying them was right (9.3.2).

Singleton sprawl. getInstance() calls reaching out of the middle of business logic. The tell: a class's real dependencies are invisible in its constructor. The fix: 9.4.6's separation of "how many exist" from "how code gets to one".

One rule about using these names. A name works only when it comes with evidence and a direction. "This is drifting towards a god object — it is in eight of our last ten pull requests. Can we split it along the billing and notification line?" is a useful sentence. "This is spaghetti" is noise, and it makes the next person defensive rather than curious (9.3.1).

6. Reading an unfamiliar codebase in fifteen minutes

This is a real interview task and a real first-week-at-a-new-job task. Pattern vocabulary turns it from wandering into a checklist, because you know what shapes to look for and what their absence means.

Where does object creation happen? Look for one startup file that builds things and passes them around. If you find it, somebody was disciplined. If instead new and direct imports of vendor libraries appear inside request handlers, there are no seams, and expect the tests to be thin or absent. A quick measure: count new outside the startup file.

In what order does the middleware run? Read the app.use lines aloud. Security, then body parsing, then authentication, then rate limiting, then routes, then the error handler last, is a competent chain. Ordering mistakes here are not a matter of tidiness — an authentication check placed after the routes it was meant to protect is an authentication bypass.

Where do decisions live? Service files full of if statements about another object's fields means an anemic model. Objects that answer questions about themselves, with thin code coordinating them, is healthy.

How is lifecycle status handled? Find any entity with a status field. Is there one function that all changes go through, or are there if (status === ...) checks scattered around, relying on everyone remembering the rules? The second one is where the illegal-transition bugs live (9.4.14).

Which files does everything import? The most-imported file and the longest file are your two best guesses at where the trouble is (9.1).

Do domain files import vendor libraries directly? If your order logic imports a payment SDK by name, the vendor's shape has reached your core, and replacing them will be a project rather than a task (9.3.9).

Each finding maps to a name and a chapter, which is the actual point of all this: the catalog is a diagnostic instrument you can use at reading speed, not a style guide.

7. Why this outlives whatever framework you are using

The same catalog gets re-derived everywhere. Express middleware, Rails' middleware, Django's middleware, and the ASP.NET request pipeline are one pattern with four names. React's reducers, the Elm language's architecture, and Android's event handling are one pattern with three names. Every framework that wires your objects for you is doing what Angular, NestJS, and Java's Spring do.

So learn the pattern at the level 9.4.1 teaches it — the tension it resolves and the roles involved — and every new framework becomes a lookup. This is the part of your knowledge with the longest shelf life. Frameworks turn over roughly every five years. The tensions underneath have not meaningfully changed since 1994.

Fluency runs in both directions. Early on, you can go from a name to a structure: "Strategy is an interface with several implementations." What this folder is training is the harder direction, from structure to name, at reading speed. You see sort(comparator) and think Strategy. You hear "we need retries around this client, and caching too, and we want to turn either one off" and reach for a Decorator stack. You spot four booleans on one object and prescribe a state machine. That second direction is what an interviewer at a product company is actually probing, because it predicts how quickly you will be useful in their codebase.

Next: 9.5.1 leaves single-object patterns behind for work that happens at the same time — races and critical sections first, then the locks that guard them, and then the queues, pools and workflows that several drills in this folder have been pointing at.

Recall

  • JavaScript absorbed much of the catalog. Iterator became Symbol.iterator and generators, Observer became EventEmitter and EventTarget, Prototype is the object model, Proxy is a built-in class, and Strategy and Command are just functions. The tension is unchanged; the ceremony needed is much smaller.
  • The module lineage: namespace object (one global, no privacy) → module pattern with an IIFE (closures give real privacy) → revealing module (the returned object reads as a table of contents) → ES modules (the language does it). The survivor is the factory function returning an object literal, which is the module pattern per instance and the closure-shaped alternative to a class.
  • Framework translation: Express is a Chain of Responsibility with a factory entry point. React is a Composite tree, with Strategy props, Decorator wrappers, Command-plus-State reducers, and hooks that replaced a Template Method lifecycle. Wiring frameworks are a factory generalised over every object, plus lifetimes, plus a role-named chain.
  • Dependency injection is free; containers are not. Four levels: hand-wiring in one file → per-feature factories → a small registry once lifetimes differ → a full framework container. Take the lowest level that carries the app and climb only on named pain. Both extremes fail: rejecting the principle because you dislike the tool, and buying the tool for fifteen objects.
  • Anti-patterns are only useful with evidence and a direction. God object, anemic model, shotgun surgery, event soup, callback hell, boolean state soup, golden hammer, speculative generality, lava flow, copy-paste divergence, singleton sprawl. Name plus evidence plus a suggested move, or say nothing.

Self-test: Walk the four generations of the module story and say what each one added. Explain why the factory-returning-an-object shape survived ES modules. Translate five React features into catalog names. Give the four levels of dependency-injection machinery and the rule for moving up. Name six anti-patterns with the tell you would actually observe for each. Why does this knowledge outlast frameworks?

Quiz Bank

FoundationalWalk the module pattern lineage and say what survives of it in modern code.

The namespace object came first: var MyApp = MyApp || {} and then hanging everything off it. It bought exactly one thing, which is one global name instead of fifty. It bought no privacy whatsoever, because every field was reachable and writable from anywhere.

The module pattern came next, and it was the real breakthrough. A function is written and immediately called — the shape people call an IIFE — so its variables live in a scope nothing outside can reach. The functions defined inside that scope can still see those variables, and the returned object hands out only those functions. That is closures being used as a private keyword (3.6.2), and the resulting shape is a Facade: private internals, one small deliberate surface.

The revealing module is the same mechanism with a presentation improvement. Everything inside is a named function, and the returned object is a short list naming the public ones. A reader learns the public surface from one line instead of scanning the file.

ES modules are the language absorbing all of it. The file is the private scope, export is the reveal, and the import cache gives you one shared instance for the whole process (3.6.5).

What survived, and why it survived. A module gives you one private scope for the whole program. Sometimes you need a fresh private scope per thing you create. That is the factory function returning an object literal — createCart() closing over its own items array — and two calls to it produce two genuinely separate carts. This is the module pattern applied once per instance, and it is the closure-shaped alternative to writing a class. You will also recognise the shape in almost every client library that asks you to create a client and then gives you methods on it. What is a namespace and how is it used in practical JavaScript? [EQ-193]

FoundationalTranslate React into catalog vocabulary — five mappings, each with one sentence of justification.

The component tree is Composite. Containers and leaves both satisfy the same rendering contract, and a container renders by asking its children to render, which is the recursion living inside the objects rather than in the caller (9.4.11).

Passing behaviour as children or as a render prop is Strategy. The parent decides which behaviour to hand down, and the child runs whatever it was given without knowing what it is (9.4.12).

A higher-order component is a Decorator. withAuth(Profile) takes a component and returns a component — same shape in, same shape out — with one concern added, and they stack. It also brings the same cost Decorator always brings: a deep stack of wrappers is hard to read in a stack trace (9.4.8).

dispatch({ type, payload }) plus a reducer is Command plus State. The action object is an operation written as data, which is why the whole sequence can be recorded and replayed — that is the command log from 9.4.15, and it is what the browser tool that steps backwards through your app's history is built on. The reducer is a function from current state plus event to next state, which is exactly the transition function from 9.4.14.

Class lifecycle methods were Template Method. The framework owned the sequence and called your named methods at fixed moments; you filled in the blanks (9.4.17). Hooks replaced that with small functions you compose yourself, for the reason 9.2.4 gives: one class can only be one variant, and the hierarchy got in the way. The most-used front-end framework in the world chose composition over inheritance in public.

AppliedLay out the levels of dependency-injection machinery, the rule for choosing between them, and how each extreme fails.

First separate the idea from the tooling, because conflating them is where teams go wrong. The idea is that code is handed what it needs rather than creating it. That costs nothing and you should always do it.

Level 1 — hand-wired, one file. A single startup file creates everything and passes it along. Explicit, searchable, compiler-checked. Its only failure mode is length, and it carries much more of an application than teams expect. This is the right default for a Node service.

Level 2 — a factory per feature. makeCheckoutModule(deps) builds that feature's internals and returns only its public surface. The revealing module from section 2, applied to architecture. Comfortable up to a few hundred objects.

Level 3 — a small registry. Worth it once objects have genuinely different lifetimes — some shared for the whole program, some created fresh per request. Hand-managing that distinction is where cross-request data leaks come from, so a lookup that records both how to build a thing and how long it lives starts paying for itself.

Level 4 — a full framework container. Annotate classes, and the framework reads their constructor types and builds the graph. You get declarative wiring, managed lifetimes, and one-line test substitution. You pay an older non-standard decorator dialect (3.7.6), failures that move from compile time to boot time, and the framework's gravity on everything else you do.

The rule: take the lowest level that carries the application, and move up only when you can point at the pain that forced it — the wiring file nobody can read, or a scope bug that shipped. Not because a tutorial started there.

The failure at the bottom. A team refuses the principle because it dislikes the tooling: "we do not need dependency injection, we are not using one of those frameworks." Now new appears throughout the business logic, nothing can be substituted, and testing anything requires real credentials. They turned down the free part.

The failure at the top. A nine-endpoint service adopts a full container by default. Fifteen objects are now wired by reflection, type errors have become boot errors, and every new hire spends a week learning the resolution rules. They bought the expensive part with nothing to spend it on.

InterviewYou get fifteen minutes with an unfamiliar Express codebase and are asked for first impressions. What do you look at, and what does each finding tell you?

The point of the exercise is not to read everything. It is to know which six places carry the most information.

Find where objects get created. One startup file that builds things and hands them out means somebody was deliberate about seams. If instead you find new and direct vendor imports inside request handlers, there are no seams, and the test suite will be thin because there is no way to substitute anything. Counting new outside that startup file gives you a number in ten seconds.

Read the middleware order aloud. Security, body parsing, authentication, rate limiting, routes, error handler last, is a competent chain (9.4.16). Then check that the error handler exists and is genuinely last. Ordering mistakes here are not stylistic: an authentication step registered after the routes it protects is a bypass, and a rate limiter placed after an expensive handler limits nothing that matters.

Look at where decisions are made. Service files full of if statements about another object's fields is the anemic model, and it means the rules are scattered away from the data they govern (9.2.2). Objects that answer questions about themselves, with thin coordinating code above them, is the healthy version.

Find every status field. Then ask whether all changes to it go through one function, or whether the codebase relies on every author remembering which transitions are legal. The second is where "this order shipped twice" comes from (9.4.14).

Find the biggest file and the most-imported file. These two are your best cheap guesses at where the pain is concentrated (9.1).

Check whether domain files import vendor libraries by name. If order logic imports a payment SDK directly, the vendor's shape has reached your core and replacing them becomes a project (9.3.9).

Every one of those findings comes with a name and a chapter, and that is what makes this answer strong in an interview. You are not giving opinions about code you just met. You are running a diagnostic and reporting results.

StaffFive Node teams in your org have five different styles: one uses a full framework container everywhere, one hand-wires, one has anemic services, one has event soup, one is strictly functional. You are asked to write the org's design standards. What do you standardise, what do you leave alone, and how do you write it so all five teams accept it?

The trap here is obvious once you name it: standardise shapes and you start five arguments and lose at least three, because every team depends on its own shape and switching costs real work for no visible payoff. So standardise properties that any style can satisfy, and let each team satisfy them in its own spelling.

Standard one — volatile dependencies sit behind a role you own. Vendors and I/O are reached through an interface your domain defines (9.3.9). This is checkable without reading any code: domain packages import zero vendor libraries. The container team satisfies it with providers, the hand-wiring team with constructor parameters, the functional team by passing effect functions. All three comply, none has to change style.

Standard two — one writer per state machine. Any entity with a lifecycle status gets exactly one function through which transitions happen, and that function rejects illegal ones (9.4.14). This is the event-soup team's actual cure, and note that it does not require them to abandon events. It only requires that state changes funnel through one gate.

Standard three — rules live with the data they govern. Written as a review question rather than a code shape: who owns this decision? (9.2.2). That phrasing is what makes it acceptable to the functional team, whose closures over state satisfy the property perfectly well and who would rightly reject a mandate to write classes.

Standard four — every event or queue edge is labelled. For each one, state in writing whether it is a notification that may be missed or something that must happen (9.4.13). This is the event-soup team's real defect stated as a guarantees question instead of as "events are bad", which is both more accurate and more likely to be adopted.

Standard five — creation knowledge is concentrated. new inside business logic is a review flag. Measurable, and every style can meet it.

What you deliberately leave free: classes versus closures, which container if any, which framework, and which spelling of a given pattern. Publish the four levels of section 4 as guidance with triggers for climbing, not as a mandate.

How to make it survive contact. Write each standard in three parts — the property, what a violation looks like, and where the fix is explained. Then, for each team, publish a one-page translation from their local vocabulary to the shared one. That is recognition rather than conversion, and it is what makes a standards document readable by somebody who did not write it. Enforce through review language and two or three measurable checks, not through an architecture review board.

The underlying move is worth stating plainly, because it generalises well beyond this question. A standard that names properties and guarantees can unite five different styles. A standard that names shapes starts a fight with four of them.

Scenario Drill

DrillYou join a startup whose Node monolith mixes all five styles above, and your first bug is that users intermittently see another customer's feature flags. Walk your first week: locate it fast, fix it minimally, and turn the incident into the org's first two standards without starting a rewrite war.

Days 1 and 2 — locate it by shape, not by reading everything.

One customer's data appearing in another customer's response has three usual causes, and you can name all three before opening a single file. Something that should exist per request is instead being held by something that lives for the whole program. That shows up as a module-level client configured by whichever request happened to touch it first, a module-level let used as a cache, or a customer identifier stashed on a shared object.

That taxonomy converts a vague bug into three greps: getInstance, module-level let anywhere near the flag code, and every place a flag client gets constructed. Minutes, not days, because knowing the shapes tells you what to search for.

Say the search lands on flags.ts, which exports const client = new FlagClient() and configures it lazily with whatever customer arrives first. That explains the intermittent part exactly, which is the detail that makes the diagnosis convincing rather than plausible: which customer wins depends on the order of traffic after each deploy, and it differs per worker process, so it is different every time (3.8.6).

Day 3 — the smallest fix, on the right seam.

Do not rewrite. Make the customer an explicit part of the lookup — flags.for(customerId).isEnabled(key) — with one client cached per customer behind a keyed factory (9.4.6). Then thread the customer identifier down from the authentication middleware using AsyncLocalStorage (3.8.7), so that you are not editing fifty function signatures on your third day.

Notice the shape of that fix. You changed how long an object lives and what it is keyed by, and you changed nothing about the architecture. That is what makes it shippable this week.

Day 4 — the write-up that becomes policy.

Name the failure precisely in the postmortem: an object that lives for the whole program was holding data that belongs to a single request. Then propose exactly two standards, and propose only the two this incident proves — a postmortem that arrives with seven recommendations gets read as an agenda.

The first: lifetimes are explicit. Every object holding state declares whether it is shared for the program, per customer, or per request, and per-request data never lives on a program-lifetime object. Enforceable as a lint rule against module-level mutable state in the domain folder.

The second: creation is concentrated. Objects get built in a startup file, and new inside a request handler is a review flag.

Both are properties, not shapes, so all five teams can satisfy them in their own style. The container team maps them to provider scopes, the functional team to factory closures. Nobody is asked to convert.

Day 5 — the first entry in the shared vocabulary.

Publish one page mapping the modules involved in this incident to their catalog names: the accidental shared instance, the keyed factory that fixed it, the request-scoped context that carried the customer through. The org now has a shared vocabulary whose first entry came from a bug that actually happened, rather than from an architecture memo.

That arc is the whole lesson of this folder. The vocabulary let you find the bug in hours instead of days. Standards written as properties rather than styles got adopted without a fight. And the evidence came first, which is why anyone believed it. The catalog earns its place as an engineering instrument, not a style guide.