EngineeringMarch 28, 2024 · 4 min readUpdated September 6, 2026

Node.js Design Patterns: Which One, and When

The pull request had six files changed. Each one was a client for a different delivery platform, and each one had the same forty lines pasted in: retry three times, wait a bit longer each time, log the failure. The author was not lazy. Nobody had ever given that forty-line block a name, so nobody thought of it as a thing that could exist once.

That is what design patterns are for. Not architecture diagrams, not interview questions. Names for choices you are already making, so that the third time you make one, you recognise it and put it in one place.

I first wrote this series in 2024. Since then Node has changed enough that half the examples looked dated: CommonJS require everywhere, no test runner, TypeScript needing a build step. As of September 2026, Node 24 is the active LTS, Node 26 becomes LTS in October, and from Node 27 every release is LTS. ES modules are the default for new projects, node:test ships in the runtime, and Node runs .ts files directly by stripping the types. So I rewrote the whole series against that baseline, with examples you can paste into a .mjs file and run.

The seven, by the problem they solve

You keep noticing…PatternIn Node today
Internals leaking, callers poking at things they should not touchModuleAn ES module with a small export surface
A switch on a type string that grows every quarterFactoryA function that returns the right client for a partner
Two database pools, three config loaders, all "the" instanceSingletonModule scope, plus a test seam so it is not global state
One event, five things that need to react to itObserverEventEmitter, EventTarget, and the trap of async listeners
The same step done three different ways depending on a partner or a flagStrategyA map of functions chosen at runtime
Every request needs auth, logging, validation, in that orderMiddlewareA pipeline of async (ctx, next) functions
Retry, cache, timing, wrapped around something that already worksDecoratorA higher-order function, or a Proxy for whole objects

Read the left column first. If none of it sounds like your codebase, you do not need the pattern yet, and adding it early is how a small service ends up with an AbstractClientFactoryProvider.

What the language gives you for free now

Most of these patterns were invented for languages where functions are not values and modules are not built in. JavaScript has both, and modern Node has more, which is why the examples in this series are short:

logger.mjsJavaScript
// Module pattern is just an ES module: everything not exported is private.
const prefix = "[app]";
 
export function log(message) {
  console.log(`${prefix} ${message}`);
}
app.mjsJavaScript
import { log } from "./logger.mjs";
import { setTimeout as sleep } from "node:timers/promises";
 
log("starting");
await sleep(10); // top-level await, no wrapper function needed
log("done");

Three things in those nine lines would not have worked when I wrote the first version of this post on a Node 18 project: top-level await in the entry file, the node: prefix that makes built-ins unmistakable, and the promise-based timers. None of them are patterns. They are the reason the patterns got smaller.

What changed since 2024

Reading my old posts back was uncomfortable in a useful way. The code samples were textbook-shaped: an add function decorated with a logger, a Dog class from a factory. None of it looked like the code I actually maintain, which talks to payment gateways and delivery aggregators and fails at 11pm on a Friday. So each rewritten post now opens on a real situation, shows the pattern in Node 24 syntax, and spends a section on where it goes wrong in production, because that is the part I wish someone had written down for me.

Two honest limits. The samples are deliberately small; a real client has timeouts, cancellation with AbortSignal, and structured logs, and I trimmed those so the pattern stays visible. And I stopped writing "Node.js is single-threaded" as though that settles anything: worker threads have been stable for years, and the interesting concurrency problems in integration work are about ordering and idempotency, not threads.

Start with Module if you are new to this, or jump to Decorator if that forty-line retry block sounded familiar.

Nguyễn Hải Nam

Nguyễn Hải Nam

Project Management Lead. 16+ years from code to delivery. PMP®. Writing here about project management and engineering.

About me