Design Patterns & LLD

Patterns You Already Use: Middleware, Hooks, Dependency Injection, and Anti-Patterns

Express and Koa middleware, EventEmitter, Redux, React, and the anti-patterns that spoil designs.

Lesson 0420 min read#middleware#anti-patterns#react

On this page

  1. Words used in this lesson
  2. Middleware is Chain of Responsibility (and a decorator)
  3. Events are Observer
  4. Redux is Command plus Observer
  5. React hooks and Context versus patterns
  6. Proxy objects are dynamic decorators
  7. Dependency injection in practice
  8. Anti-patterns: when the pattern is the problem
  9. When NOT to use a pattern
  10. Interview questions
  11. Key takeaways
  12. What's next

In one line: you already use design patterns every day without the names. Express and Koa middleware is Chain of Responsibility, EventEmitter is Observer, Redux is Command plus Observer, React Context is dependency injection, and the JavaScript Proxy object is a dynamic decorator. Knowing this lets you answer pattern questions with real tools. Knowing the anti-patterns (god object, premature abstraction, singleton abuse, anemic model, over-engineering) tells you when to put patterns away.

Lessons 1 to 3 gave the names. This lesson maps them to code you have probably written, and then shows the other side: patterns used too early or in the wrong place. In an interview, saying "this is the same as Express middleware" is a strong move, because it shows you understand the idea and not only the word.

Related lessons: Backend, Lesson 4 (error handling in Express), Senior Frontend Depth, Lesson 1 (hooks in depth) and Lesson 3 (state management decisions).

What I ran / checked. I ran every TypeScript block with Node 22.22.0 (node --experimental-transform-types) and pasted the real output. All files also pass tsc --strict (TypeScript 7.0.2). The libraries are real: Express 5.3.0 (it served real requests on a random port, called with Node's fetch), koa-compose 4.1.0, React 19.3.0 with react-dom/server (server rendering to a string only, so I did not test clicks or effects in a browser), and Redux 5.0.1 (I used legacy_createStore, the name it exports for the classic function; I did not check its documentation, and I did not try Redux Toolkit). Comparisons such as "Context is dependency injection" and the anti-pattern names are common practice and opinion, not facts about the libraries.

Words used in this lesson

  • Middleware is a function that sits between the request and the final handler. It can look at the request, change it, answer it, or call next() to pass it on.
  • The onion model means each middleware runs code before next() and again after it, so calls go in and then back out, like layers of an onion.
  • An event emitter is an object where code can listen for named events and other code can fire them.
  • A reducer is a pure function (state, action) => newState. An action is a plain object that says what happened.
  • A store is the object that holds the state and runs the reducer.
  • A hook is a React function whose name starts with use. Context is React's way to make a value available to a whole subtree without passing it down by hand.
  • A dynamic proxy is a Proxy object that wraps any target and decides how to treat each property access at run time.
  • An anti-pattern is a common solution that looks fine and causes trouble.
  • A god object is a class that knows and does too much.
  • An anemic model is a class with data and no behaviour, while the rules live elsewhere.
  • Premature abstraction is making something general before you have seen it repeat.
  • Over-engineering is more structure than the problem needs.

Middleware is Chain of Responsibility (and a decorator)

Lesson 3 described a chain of handlers: each one handles a request or passes it on. Web middleware is that, with one more feature: after next() returns, a middleware can still do work. That is the onion. Here is the whole idea in a few lines, compared with the real koa-compose library:

JavaScript
import koaCompose from "koa-compose";type Ctx = { log: string[] };type Next = () => Promise<void>;type Mw = (ctx: Ctx, next: Next) => Promise<void> | void;// The whole idea in 8 lines: each middleware gets a next() that runs the rest.function compose(mws: Mw[]) {  return (ctx: Ctx) => {    const dispatch = async (i: number): Promise<void> => {      const mw = mws[i];      if (!mw) return;      await mw(ctx, () => dispatch(i + 1));    };    return dispatch(0);  };}const timing: Mw = async (ctx, next) => { ctx.log.push("timing in"); await next(); ctx.log.push("timing out"); };const auth: Mw = async (ctx, next) => { ctx.log.push("auth in"); await next(); ctx.log.push("auth out"); };const handler: Mw = ctx => { ctx.log.push("handler"); };const stopper: Mw = ctx => { ctx.log.push("stopper (does not call next)"); };const mine: Ctx = { log: [] };await compose([timing, auth, handler])(mine);console.log(mine.log);const real: Ctx = { log: [] };await koaCompose<Ctx>([timing, auth, handler])(real);console.log("same as koa-compose:", JSON.stringify(real.log) === JSON.stringify(mine.log));const short: Ctx = { log: [] };await compose([timing, stopper, handler])(short);   // chain of responsibility: stopper ends itconsole.log(short.log);

Output:

TEXT
[ 'timing in', 'auth in', 'handler', 'auth out', 'timing out' ]same as koa-compose: true[ 'timing in', 'stopper (does not call next)', 'timing out' ]

The order is timing in, auth in, handler, auth out, timing out: in on the way down, out on the way up. My 8-line compose matched koa-compose exactly. In the last run, stopper did not call next(), so handler never ran, but the outer timing out still did. That is Chain of Responsibility: any link may end the chain.

Now the same shape in the real Express 5:

JavaScript
import express from "express";import type { NextFunction, Request, Response } from "express";const order: string[] = [];const app = express();app.use((req, _res, next) => { order.push("logger " + req.path); next(); });app.use((req, res, next) => {  if (req.path.startsWith("/admin") && req.get("x-token") !== "secret") {    order.push("auth: stop");    res.status(401).send("no");                     // does not call next(): chain ends    return;  }  next();});app.get("/hello", (_req, res) => { order.push("handler"); res.send("hi"); });app.get("/admin", (_req, res) => { order.push("admin handler"); res.send("admin area"); });app.get("/boom", async () => { throw new Error("async failure"); });   // Express 5 catches rejected promisesapp.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {   // 4 args = error middleware  order.push("error handler");  res.status(500).send("oops: " + err.message);});const server = app.listen(0);const base = `http://localhost:${(server.address() as { port: number }).port}`;for (const [path, headers] of [["/hello", {}], ["/admin", {}], ["/admin", { "x-token": "secret" }], ["/boom", {}]] as const) {  const r = await fetch(base + path, { headers });  console.log(path, r.status, await r.text());}console.log(order.join(" > "));server.close();

Output:

TEXT
/hello 200 hi/admin 401 no/admin 200 admin area/boom 500 oops: async failurelogger /hello > handler > logger /admin > auth: stop > logger /admin > admin handler > logger /boom > error handler

Read the log line. /hello went through logger and then handler. /admin without a token stopped at auth: stop and never reached a handler (a 401). With the token it passed. /boom threw inside an async handler, and Express 5 forwarded the rejected promise to the error handler, which is the function with four arguments. (In older Express versions you had to call next(err) yourself for async code. I ran only 5.3.0 here.) Two rules follow:

  • Order is the design. logger first, auth before the routes, the error handler last. Put them in a different order and the behaviour changes.
  • Every middleware must either respond or call next(). If it does neither, the request hangs.

Why it is also a decorator: a middleware wraps the rest of the chain and adds something (timing, auth, a request id) without the handler knowing. See Lesson 2.

Events are Observer

Node's EventEmitter is the Observer pattern from Lesson 3, built in. on is subscribe, emit is publish, off is unsubscribe.

JavaScript
import { EventEmitter } from "node:events";const bus = new EventEmitter();const seen: string[] = [];const onOrder = (id: number) => seen.push("A" + id);bus.on("order", onOrder);bus.on("order", (id: number) => seen.push("B" + id));bus.once("order", (id: number) => seen.push("once" + id));console.log("emit returns", bus.emit("order", 1));   // true if anyone listenedbus.emit("order", 2);bus.off("order", onOrder);bus.emit("order", 3);console.log(seen, "| no listener ->", bus.emit("nothing"));// Listeners run synchronously, in the order they were added.bus.on("tick", () => console.log("  listener ran"));console.log("before emit"); bus.emit("tick"); console.log("after emit");// Special case: an "error" event with no listener throws.try { bus.emit("error", new Error("nobody listening")); }catch (e) { console.log("threw:", (e as Error).message); }// The classic leak warning after more than 10 listeners for one event.process.on("warning", w => { if (w.name === "MaxListenersExceededWarning") console.log("warning:", w.name); });for (let i = 0; i < 11; i++) bus.on("leak", () => {});await new Promise(r => setTimeout(r, 10));console.log("default max listeners:", EventEmitter.defaultMaxListeners);

Output:

TEXT
emit returns true[ 'A1', 'B1', 'once1', 'A2', 'B2', 'B3' ] | no listener -> falsebefore emit  listener ranafter emitthrew: nobody listeningwarning: MaxListenersExceededWarningdefault max listeners: 10

What the output shows:

  • emit returned true when someone listened and false when nobody did.
  • Listeners ran in the order added (A1, B1, once1). once ran only for the first event. After off, A stopped hearing events (A3 is missing).
  • Listeners run synchronously: listener ran printed between before emit and after emit. So a slow listener slows the emitter.
  • Emitting "error" with no listener throws. Always add an error listener on an emitter that can fail.
  • The 11th listener on one event triggered MaxListenersExceededWarning. The default limit is 10 and it exists to warn you about the forgotten-unsubscribe leak from Lesson 3.

Same pattern, other places: DOM addEventListener, streams (stream.on("data")), and process.on("exit").

Redux is Command plus Observer

Redux separates what happened from how state changes. That matches two patterns:

  • An action ({ type: "added", price: 3 }) is a command as data. It is a small object that says what to do, and it can be stored, logged and replayed.
  • The reducer is a pure function, so the same actions give the same state.
  • store.subscribe is Observer.
JavaScript
import { legacy_createStore as createStore } from "redux";type State = { count: number; total: number };type Action = { type: "added"; price: number } | { type: "removed"; price: number };// Pure: (state, action) -> new state. No clocks, no network, no mutation.function reducer(state: State = { count: 0, total: 0 }, a: Action): State {  switch (a.type) {    case "added": return { count: state.count + 1, total: state.total + a.price };    case "removed": return { count: state.count - 1, total: state.total - a.price };    default: return state;  }}const store = createStore(reducer);const history: Action[] = [];const unsubscribe = store.subscribe(() => console.log("subscriber sees", store.getState()));   // observerconst dispatch = (a: Action) => { history.push(a); store.dispatch(a); };    // an action is a command as datadispatch({ type: "added", price: 3 });dispatch({ type: "added", price: 12 });dispatch({ type: "removed", price: 3 });unsubscribe();// Actions are plain data, so we can replay them: same actions, same state.const replay = history.reduce(reducer, undefined as unknown as State);console.log("replayed:", replay, "| same:", JSON.stringify(replay) === JSON.stringify(store.getState()));

Output:

TEXT
subscriber sees { count: 1, total: 3 }subscriber sees { count: 2, total: 15 }subscriber sees { count: 1, total: 12 }replayed: { count: 1, total: 12 } | same: true

Three dispatches made three notifications. Then unsubscribe() was called before the replay. The replay folded the stored actions through the reducer and reached the same state (same: true). This is why Redux tools can offer time-travel debugging: the history is a list of commands. The cost is boilerplate. For many apps, state kept near the component (or server data in a cache) is simpler; see Frontend Depth Lesson 3 for how to choose.

React hooks and Context versus patterns

Hooks are not one of the classic patterns. They are React's own way to attach state and effects to a function component. But two pattern ideas show up around them:

  • A custom hook (useCounter) is reuse by composition. Instead of a base class with state that children inherit, you call a function and get the behaviour. It is the "composition over inheritance" rule from Lesson 0.
  • Context is dependency injection for a component tree. A component asks for Api and does not care who provided it; a test or a parent swaps it.
JavaScript
import React from "react";import { renderToString } from "react-dom/server";// A custom hook = reuse of behaviour by composition (no base class).function useCounter(start: number) {  const [n, setN] = React.useState(start);  return { n, inc: () => setN(n + 1) };}// Context = dependency injection for a component tree.interface Api { greeting(): string; }const ApiContext = React.createContext<Api>({ greeting: () => "default api" });function Hello() {  const api = React.useContext(ApiContext);  const { n } = useCounter(5);  return React.createElement("p", null, `${api.greeting()} #${n}`);}console.log(renderToString(React.createElement(Hello)));const fake: Api = { greeting: () => "fake api" };      // swap the dependency for a testconsole.log(renderToString(React.createElement(ApiContext.Provider, { value: fake }, React.createElement(Hello))));

Output:

TEXT
<p>default api #5</p><p>fake api #5</p>

The same Hello component rendered two different greetings, because a provider supplied a different Api. The counter started at 5 and came from the custom hook. I only rendered to a string on the server; I did not run clicks, effects or re-renders, so the example shows the wiring and nothing about timing.

Honest limits: Context is not a store (changing the value re-renders every consumer, so keep it for things that change rarely: theme, current user, API client), and a custom hook can still hide global state. A hook that reaches into a module-level variable is a singleton in disguise.

Proxy objects are dynamic decorators

In Lesson 2 you saw Proxy guarding one object. It can also wrap any service, because it intercepts calls without knowing the method names:

JavaScript
// A dynamic decorator: wrap ANY service so every method call is logged and timed.function withLogging<T extends object>(target: T, name: string): T {  return new Proxy(target, {    get(obj, prop, receiver) {      const value = Reflect.get(obj, prop, receiver);      if (typeof value !== "function") return value;      return (...args: unknown[]) => {        const out = value.apply(obj, args);        console.log(`${name}.${String(prop)}(${args.join(", ")}) ->`, out);        return out;      };    },  });}class Calculator { add(a: number, b: number) { return a + b; } mul(a: number, b: number) { return a * b; } }const calc = withLogging(new Calculator(), "calc");calc.add(2, 3);calc.mul(4, 5);console.log(calc instanceof Calculator);     // the proxy still looks like a Calculator

Output:

TEXT
calc.add(2, 3) -> 5calc.mul(4, 5) -> 20true

withLogging logged both methods without editing Calculator, and calc instanceof Calculator stayed true. The same trick builds timing, retry or "feature flag" wrappers. The cost is a hidden layer that can surprise a reader, so name the function clearly, and avoid it on hot paths.

Dependency injection in practice

You have seen three forms in this lesson: Express hands req, res and next to your function (the framework builds them), React Context hands a value to a subtree, and in Lesson 1 a plain constructor hands a repo and a clock to a service. The shared idea: the code that needs a thing does not decide where it comes from. A container such as NestJS automates the wiring, but constructor arguments and a composition root remain the basis.

Anti-patterns: when the pattern is the problem

Anti-patternWhat it looks likeBetter
God objectOne App or Manager class that validates, saves, emails and formatsSplit by reason to change (Lesson 0, S). A facade may coordinate, but the work stays in small classes
Premature abstractionA generic format(kind, flagA, flagB) made after seeing one caseWait for the third repeat; two small functions beat one function with flags
Singleton abusegetInstance() or a module-level object used everywhere, so tests share stateCreate it once and inject it (Lesson 1 showed the leak)
Anemic modelClasses of public fields; all rules in *Service classesPut rules next to the data they protect
Over-engineeringInterface, factory and strategy for something with one implementationPlain function now, pattern when the second case is real

Two of them in code. First the anemic model:

JavaScript
// ANEMIC: a bag of public fields. Every caller must remember the rules.class AccountData { balance = 0; }const data = new AccountData();data.balance = -50;                       // nothing stops thisconsole.log("anemic balance:", data.balance);// RICH: the object protects its own rules.class Account {  private balance = 0;  deposit(n: number) { if (n <= 0) throw new Error("deposit must be positive"); this.balance += n; }  withdraw(n: number) {    if (n > this.balance) throw new Error("insufficient funds");    this.balance -= n;  }  get current() { return this.balance; }}const acc = new Account();acc.deposit(100);try { acc.withdraw(150); } catch (e) { console.log("rich:", (e as Error).message, "| balance", acc.current); }

Output:

TEXT
anemic balance: -50rich: insufficient funds | balance 100

The anemic AccountData let balance = -50 happen with no complaint, because any caller must remember the rule. The rich Account kept its balance private and refused a withdrawal of 150 when the balance was 100. The rule lives in one place. (Domain-driven design books argue about this at length. I treat it as a guide: if the same rule is checked in three services, move it to the object.)

Then over-engineering:

JavaScript
// OVER-ENGINEERED: five names to say hello.interface GreetingStrategy { greet(name: string): string; }class EnglishGreetingStrategy implements GreetingStrategy { greet(n: string) { return "Hello " + n; } }interface GreetingStrategyFactory { create(): GreetingStrategy; }class EnglishGreetingStrategyFactory implements GreetingStrategyFactory {  create() { return new EnglishGreetingStrategy(); }}class GreetingService {  constructor(private factory: GreetingStrategyFactory) {}  run(name: string) { return this.factory.create().greet(name); }}console.log(new GreetingService(new EnglishGreetingStrategyFactory()).run("Ann"));// PLAIN: same result. Add the pattern on the day a second language really arrives.const greet = (name: string) => "Hello " + name;console.log(greet("Ann"));

Output:

TEXT
Hello AnnHello Ann

Both lines print the same text. The first took five names and three classes. The second is one function. Add the strategy and the factory on the day a second language is real, not before.

A rule that helps: the rule of three. The first time, write it plainly. The second time, copy it and notice what differs. The third time, you have enough examples to choose the right abstraction. A wrong abstraction is more expensive than a little duplication.

When NOT to use a pattern

  • You cannot name the change it makes cheap. ("What if a second payment provider comes?" is a real reason. "It looks professional" is not.)
  • There is only one implementation and no sign of a second.
  • A function would do. Many patterns (strategy, command, factory, decorator) are just a function in JavaScript.
  • The pattern adds more names than the problem has rules. In a 45-minute interview, three clean classes beat nine clever ones.
  • You are forcing a name onto code that already works. It is fine for code to be simple.

Interview questions

Which design pattern is Express middleware? (Must know)

Chain of Responsibility, with the onion twist. Each middleware may handle the request, change it, or call next(). Because code after next() runs on the way back, it also acts like a decorator around the rest of the chain. I showed this with an 8-line compose that matched koa-compose.

What is the difference between Express and Koa middleware? (Senior signal)

Both use the same chain idea. In Koa, next() returns a promise, so await next() gives a clean onion with code before and after. Express passes req, res and next, and has special four-argument error middleware. Express 5 also forwards rejected promises from async handlers to the error handler, which I tested. I would not claim more than this about their insides without reading their code.

A request hangs forever. What do you check in the middleware chain? (Trap)

A middleware that neither responds nor calls next(). Also an await next() missing so the response is sent before the later work finishes, or next() called twice. I add a log line at each middleware to see where the chain stops.

How is EventEmitter the Observer pattern, and what are its traps? (Must know)

on subscribes, emit publishes, off unsubscribes. The traps: listeners run synchronously, so a slow listener blocks the emitter; an error event with no listener throws; and listeners that are never removed leak, which is why Node warns after 10 listeners on one event.

How does Redux relate to the Command pattern? (Senior signal)

An action is a command as plain data. Because actions are data, you can log, store and replay them: my replay reached the same state. The reducer is a pure function that applies a command. The twist: classic Command objects carry do() and undo(); Redux actions carry only data, and undo works by keeping old states or replaying history.

Are React hooks a design pattern? (Senior signal)

Not one of the classic ones. They are React's API for state and effects. But custom hooks give you reuse by composition instead of by inheritance, and Context works as dependency injection. I keep the two ideas apart: the API is React's, and the thinking is composition and DI.

Is React Context a state manager? (Trap)

No. It passes a value down a tree. It does not manage updates efficiently: when the value changes, every consumer re-renders. I use it for rarely changing values such as theme, current user or an API client, and a real store or server cache for fast-changing data.

How would you add logging to every method of a service without editing it? (Senior signal)

A Proxy with a get trap that wraps functions, as in withLogging. The decorator pattern with an explicit wrapper class is more readable when the interface is small and fixed. I choose the Proxy when the service has many methods, and I say its cost: extra work on every access and a layer that is easy to forget.

What is a god object, and how do you fix it? (Must know)

A class that knows and does too much: validation, storage, formatting, sending email. Every change touches it and every test needs all of it. I split it by reason to change into small classes, with a facade only if callers need a simple entry point. The facade coordinates and does not do the work.

What is wrong with an anemic domain model? (Senior signal)

The data classes have no behaviour, so each rule is repeated in services and one caller can forget it, as balance = -50 showed. I move rules next to the data. But I do not push it too far: a rule that needs outside services (email, database) belongs in a service, not inside the entity.

What is premature abstraction, and how do you avoid it? (Must know)

Making something general before seeing it repeat, which usually produces functions with flags and "kind" parameters. I use the rule of three: write it plainly the first time, copy it the second time, and abstract on the third, when I can see what really varies. A wrong abstraction costs more than a little duplicated code.

When is a pattern the wrong answer in an interview? (Trap)

When I cannot say what change it makes cheap. If the requirements have one payment method and no mention of more, a payment factory is noise. I start with the simplest design, name the change that would make me add a pattern, and show where it would fit. That shows judgment, which is what they are testing.

Key takeaways

  • Middleware is Chain of Responsibility with an onion shape. Order is the design, and each link must respond or call next().
  • EventEmitter is Observer. Watch for synchronous listeners, the error event, and unsubscribed leaks.
  • Redux: actions are commands as data, reducers are pure, subscribe is Observer.
  • Custom hooks are reuse by composition. Context is dependency injection, not a state manager.
  • A Proxy object is a dynamic decorator or guard. It costs extra work on each access.
  • Anti-patterns: god object, premature abstraction, singleton abuse, anemic model, over-engineering. Use the rule of three.
  • Name the change a pattern makes cheap. If you cannot, do not add it.

What's next

Next: the series moves from ideas to full problems. Start with the parking lot, Lesson 5, then the LRU cache and hash map, Lesson 6. Practise: open the last middleware you wrote, say aloud which pattern it is, and check that it either responds or calls next() on every path.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series