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

Factory Pattern in Node.js

Part of my Node.js Design Patterns series.

Every integration codebase I have worked on has a file that starts as if (partner === "A") and ends, two years later, as a switch with eleven cases, three of which nobody can explain. Adding a partner means editing that file, and editing that file means re-testing all eleven. That file is a factory that never admitted it was one.

The Factory pattern is simply: callers ask for "a client for partner X" and get one back, without knowing which class or function built it. What makes it useful in Node is not classes. It is that functions and modules are values, so the factory can be a lookup instead of a branch.

The pattern in modern Node

clients.mjsJavaScript
// Each partner client exposes the same shape: fetchOrders(since) and ack(id).
function grabLike({ baseUrl }) {
  return {
    name: "grab-like",
    async fetchOrders(since) {
      return [{ id: "G-1", since, from: baseUrl }];
    },
    async ack(id) {
      return { id, ok: true };
    },
  };
}
 
function shopeeLike({ baseUrl, shopId }) {
  return {
    name: "shopee-like",
    async fetchOrders(since) {
      return [{ id: `S-${shopId}-1`, since, from: baseUrl }];
    },
    async ack(id) {
      return { id, ok: true };
    },
  };
}
 
// The registry IS the factory. Adding a partner = adding one line here.
const registry = new Map([
  ["grab", grabLike],
  ["shopee", shopeeLike],
]);
 
export function createClient(partner, config) {
  const build = registry.get(partner);
  if (!build) {
    throw new Error(`Unknown partner "${partner}". Known: ${[...registry.keys()].join(", ")}`);
  }
  return build(config);
}
app.mjsJavaScript
import { createClient } from "./clients.mjs";
 
const partners = [
  ["grab", { baseUrl: "https://api.grab.example" }],
  ["shopee", { baseUrl: "https://api.shopee.example", shopId: 42 }],
];
 
for (const [name, config] of partners) {
  const client = createClient(name, config);
  console.log(client.name, await client.fetchOrders("2026-09-06"));
}

The caller loop does not care which partner it holds. That is the whole point: the sync job, the retry logic, the metrics all work against one shape, and partner-specific mess stays inside the partner's builder.

Note the error message. A factory that throws Unknown partner with the list of known ones has saved me more debugging hours than any other single line in this series, because the failure is usually a typo in a config file at deploy time.

Loading builders lazily

When partners get big, put each one in its own file and let the factory import on demand:

clients-lazy.mjsJavaScript
const loaders = {
  grab: () => import("./partners/grab.mjs"),
  shopee: () => import("./partners/shopee.mjs"),
};
 
export async function createClient(partner, config) {
  const load = loaders[partner];
  if (!load) throw new Error(`Unknown partner "${partner}"`);
  const mod = await load();
  return mod.default(config);
}

Dynamic import() is cached by Node, so the second call for the same partner is free. This also keeps a partner's heavy SDK out of memory on services that never talk to it.

Where it goes wrong

The factory becomes the god object. Once there is one place that knows every partner, people start putting partner-specific behaviour into it: "if grab, also refresh the menu". Resist. The factory builds and hands over. Behaviour lives in the thing it built.

Config validated nowhere. The registry happily builds a Shopee client with no shopId, and the failure shows up as a 401 an hour later. Validate in the builder, fail at construction: if (!shopId) throw new Error("shopee client needs shopId"). Cheap, and it turns a runtime mystery into a startup error.

Factories for things with one implementation. If there is one payment gateway and there will be one for the foreseeable future, createGateway() is a function that returns the only option and a reader has to open it to learn that. Import the thing directly. Introduce the factory when the second implementation shows up, not before.

When not to use it

The registry version above is about fifteen lines. If your "factory" is longer than the things it creates, the abstraction is upside down. And if the choice is known at build time rather than runtime (this deployment only ever talks to one partner), a plain import with an environment variable in the config is simpler and easier to grep.

Next: the Singleton Pattern, which in Node is mostly a question of where you put the const.

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