Factory Functions and the Factory Pattern
new ApiClient(config) appeared in fifty-three files. I know the number because I had to touch every one of them.
The client talked to our payments provider. Then we signed a customer whose contract said their traffic had to go through a different provider, with a different auth handshake and a different retry rule. Same three methods on the outside (charge, refund, status), completely different guts. The clean answer is a second implementation chosen per tenant. The problem is that new ApiClient(config) always hands you back an ApiClient. The keyword is welded to the class. There is no version of new ApiClient() that returns something else instead, not without a trick ugly enough to fail review.
So I edited fifty-three files, routed them all through one new function, and that function is the whole subject of this article.
Here is the anticlimax. A factory is a function that returns an object. That is the entire definition. If those call sites had said createApiClient(config) from day one, the day the requirement changed I would have edited one function instead of fifty-three files, because a function is free to decide what it returns and new is not. In JavaScript a factory is very often the better default, so this is really a lesson about making objects well that happens to have a pattern’s name attached.
The plainest possible factory
Strip everything down. You want an object with some behaviour and some state the outside world should not touch. The factory version is a function that builds it and hands it back:
function createApiClient({ baseUrl, token }) {
// baseUrl and token are private: closed over, not properties on anything
async function request(path, options = {}) {
const res = await fetch(baseUrl + path, {
...options,
headers: { ...options.headers, authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`${res.status} on ${path}`);
return res.json();
}
return {
get: (path) => request(path),
post: (path, body) => request(path, { method: "POST", body: JSON.stringify(body) }),
};
}
const api = createApiClient({ baseUrl: "https://api.acme.dev", token });
await api.get("/me");
No new. No this. No class. baseUrl and token live in the closure, unreachable from outside because nothing out there can name them. If that shape looks familiar, it should: a function that closes over private state and returns an object of methods is the module pattern, just parameterized so you can call it more than once. This article is that idea used on purpose.
Compare it to the class you would otherwise write:
class ApiClient {
#baseUrl;
#token;
constructor({ baseUrl, token }) {
this.#baseUrl = baseUrl;
this.#token = token;
}
async #request(path, options = {}) { /* ...same body... */ }
get(path) { return this.#request(path); }
post(path, body) { return this.#request(path, { method: "POST", body: JSON.stringify(body) }); }
}
const api = new ApiClient({ baseUrl: "https://api.acme.dev", token });
Both work. Both give you real privacy (the # fields are enforced by the parser, not by an underscore convention). They produce the same-shaped thing through two different doors.
That last line on each side is the whole argument, and the rest of this article is unpacking it. The constructor’s contract is rigid: new Foo() returns a Foo, every time, and the caller wrote the new. The factory made no such promise. It returns an object, and it can change its mind about which object without a single caller noticing.
Why reach for a factory over a constructor
Not always. A lot of the time new Thing() or a plain object literal is exactly right and a factory would be noise. But there are five recurring reasons the factory earns its place, and they compound.
You want to hide the assembly. Real construction is rarely one line. You read config, pick defaults, wire a logger, maybe open a connection. A factory is the one place that knows how the thing gets built, so callers say createReportService() and stay ignorant of the six dependencies it stitched together. The knowledge lives in one function instead of smeared across every call site.
You want to decide the concrete type at runtime. This is the opening story. Given a tenant, a flag, or a parsed record, return the right implementation behind a shared surface. new cannot do this, because the keyword names the class before you know which one you want.
You want to return a cached or pooled instance. Sometimes “give me a client for region X” should hand back the same client every time, or recycle one from a pool. A factory can check a cache before it builds. new always builds.
You want no new and no this. A factory returns a plain object whose methods close over their state, so there is no this to lose when a method gets passed to setTimeout, an event listener, or array.map. Pull const { get } = createApiClient(cfg) and get still works, detached, forever. Do the same to a class instance and this is gone unless you bound it first. That whole category of binding bugs simply does not exist here.
You want to change your mind later without breaking callers. This is the quiet one that matters most on real teams. If you ship class Widget and half the codebase writes new Widget(), you have promised, forever, that construction is synchronous and returns exactly a Widget. The day you need it to be async, or to return a subtype, or to come from a pool, every one of those new sites is a breaking change. A factory never made that promise. createWidget() can start pooling, start returning a variant, even become async, and the call sites do not move.
There is one real cost to the factory worth stating plainly. Every object it returns carries its own copy of each method, because the methods are closures created fresh per call. A class puts one copy on the prototype and shares it across every instance. For a handful of clients this is nothing. For a hundred thousand particles in a render loop it is the difference, and the class (or a plain data object with free functions) wins. Measure before you worry about it, but know the tradeoff is there.
The quirk that explains why new boxes you in
You might think new is a hard wall: call it, get an instance of that class, no exceptions. Almost. There is one escape hatch in the language, and seeing it makes the factory’s advantage concrete.
If a constructor returns an object, that object becomes the result of the new expression and the freshly built this is thrown away. Return a primitive or nothing, and you get the normal this back.
function User(name) {
this.name = name;
return { name: "someone else" }; // a non-primitive: this becomes the result
}
new User("ada").name; // "someone else", not "ada"
So a constructor technically can behave like a factory by returning a different object. People do reach for this, and it works. But look at what the caller wrote: new User("ada"), which reads like a promise that you get a User. Hijacking that return value quietly breaks the promise the syntax made. The factory does the same useful thing (return whatever object fits) without the misleading new in front of it. When your creation logic wants the freedom to return something other than a plain instance, stop fighting new and use the function that was built for it.
Polymorphic creation, the Factory Method idea
Here is the shape that gave the pattern its name. You have a family of related things and a runtime value that decides which one. The naive version scatters that decision:
// this exact block, copy-pasted into three files, drifting apart
let shape;
if (kind === "circle") shape = new Circle(opts.r);
else if (kind === "square") shape = new Square(opts.side);
else if (kind === "triangle") shape = new Triangle(opts.base, opts.height);
else throw new Error("unknown shape");
Every caller now knows the concrete classes, the constructor arguments, and the branching. Add a shape and you hunt down every copy. Pull it into one factory and callers stop caring:
function createShape(kind, opts = {}) {
switch (kind) {
case "circle":
return { kind, area: () => Math.PI * opts.r ** 2, name: () => "circle" };
case "square":
return { kind, area: () => opts.side ** 2, name: () => "square" };
case "triangle":
return { kind, area: () => 0.5 * opts.base * opts.height, name: () => "triangle" };
default:
throw new Error(`unknown shape: ${kind}`);
}
}
const s = createShape("circle", { r: 10 });
s.area(); // 314.159...
Every branch returns the same shape of object: a kind, an area(), a name(). Callers get a uniform surface and never learn which branch ran. That uniformity is the point. You can hold a list of mixed shapes and call .area() on each without a single instanceof.
The Gang of Four called the object-oriented version of this Factory Method: a base class defines a createSomething() method and each subclass overrides it to decide the concrete type. In a language without first-class functions that ceremony earns its keep. In JavaScript it usually collapses to what you just saw, a type flag and a switch, or when the list grows, a lookup table of small factories:
const registry = {
circle: (o) => ({ kind: "circle", area: () => Math.PI * o.r ** 2 }),
square: (o) => ({ kind: "square", area: () => o.side ** 2 }),
triangle: (o) => ({ kind: "triangle", area: () => 0.5 * o.base * o.height }),
};
function createShape(kind, opts = {}) {
const make = registry[kind];
if (!make) throw new Error(`unknown shape: ${kind}`);
return make(opts);
}
Now adding a shape is one line in the registry and nothing else moves. Notice this is a hair away from the Strategy pattern, an object of interchangeable functions keyed by name, which the patterns intro walks through. The line between “pick a behaviour” and “build the right object” is thin, and in JavaScript both are an object of functions. Do not lose sleep over which label applies.
Try it. Pick a type, drag the size, and watch one createShape call produce a different concrete object each time, all answering the same .area() and .name():
Returning a cached or a pooled instance
A factory sits between the caller and construction, so it can decide not to construct. The most common form is a memoized factory: build once per key, reuse after.
const clients = new Map();
export function getClient(region) {
let client = clients.get(region);
if (!client) {
client = createApiClient(configFor(region)); // first call for this region
clients.set(region, client);
}
return client; // every later call for the same region gets this exact object
}
Call getClient("eu") in ten modules and all ten share one client. That is one-instance-per-key, a scoped cousin of the singleton the module pattern gives you for free. It is the same idea behind pooling a real database connection so you are not opening a socket per request, which connection pooling covers in earnest.
Be honest about the flip side before you sprinkle this everywhere. A cached factory is a shared mutable instance wearing a friendly name. If two tests both grab getClient("eu") and one mutates it, the other inherits the change, and you get the flaky, order-dependent failures that shared singletons are famous for. Cache clients and pools. Do not cache things that carry per-request or per-user state.
The heavier cousin is a true object pool: keep a stash of reusable instances, hand one out on acquire, take it back on release.
function createPool(make, reset) {
const free = [];
return {
acquire: () => free.pop() ?? make(),
release: (obj) => { reset(obj); free.push(obj); },
};
}
This is a real optimization in a real, narrow situation: hot loops that would otherwise allocate and discard thousands of short-lived objects, where the garbage collector’s pauses start showing up as jank. Games, particle systems, parsers chewing through big buffers. Outside that, modern garbage collectors are fast and a pool is just a bug surface (forget to reset and you leak state from the last user into the next). Reach for it when a profiler tells you allocation is the problem, not before.
The builder, for step-by-step construction
Some objects have too many knobs to pass as positional arguments. You have met the constructor from hell:
new HttpRequest("POST", "/users", { "content-type": "application/json" }, body, 3, 5000, true, false);
// which flag was retryOnTimeout and which was keepAlive? nobody knows without the docs
Two common fixes. The boring, usually correct one is an options object: new HttpRequest({ method, url, headers, retries }), named fields, done. The other, when construction is genuinely stepwise or you want a fluent reading order, is the builder: a small object whose methods each set one thing and return the builder, ending in a build() or send() that produces the finished result.
function request(method, url) {
const config = { method, url, headers: {}, retries: 0 };
const builder = {
header(key, value) { config.headers[key] = value; return builder; },
retries(n) { config.retries = n; return builder; },
timeout(ms) { config.timeout = ms; return builder; },
send() { return fetch(url, config); },
};
return builder;
}
request("POST", "/users")
.header("content-type", "application/json")
.retries(3)
.timeout(5000)
.send();
Each method returns builder, which is what makes the calls chain. send() is the moment the accumulated config turns into an action. It reads top to bottom in the order you would explain it out loud, and no argument is a mystery boolean.
The living examples of this are query builders. db.select("id", "email").from("users").where("active", true).limit(10) is a builder that assembles a query object one clause at a time and runs it at the end, which is exactly what ORMs and query builders are doing under the fluent surface. If you want a builder that cannot be mutated out from under you, have each method return a fresh copy of the config with structuredClone instead of mutating in place, and you get an immutable step-by-step API. That is a real tool, though most builders do not need it.
Be careful not to reach for a builder when an options object would do. If there is no ordering, no validation between steps, and no fluent DSL worth reading, new Thing({ ...opts }) is less code and less indirection. A builder for three optional fields is the same over-engineering as a factory for a one-line object.
Abstract factory, in one honest paragraph
The Gang of Four’s Abstract Factory is a factory of related factories: one object that produces a whole matched family of things. The textbook example is a UI kit where createDarkTheme() returns a set of components (button, input, modal) that all belong together, and swapping to createLightTheme() swaps the entire coordinated set at once. In JavaScript you rarely build the full apparatus. You pass an object of factories, or you import the right module. A createStorage(driver) that returns { read, write, list } wired for memory, disk, or a bucket is the same idea at a tenth of the ceremony. If you find yourself writing an AbstractWidgetFactoryProvider, stop and ask whether an object literal of functions does the job. It almost always does.
When not to bother
A factory is indirection, and indirection has a price: one more hop the next reader has to follow before they reach the thing that actually does the work. Pay it when it buys you something. The list above is what it buys. When it buys nothing, skip it.
The clearest waste is the factory that wraps one construction with no added logic:
// pure ceremony: this earns nothing a literal doesn't
function createPoint(x, y) {
return { x, y };
}
const p = createPoint(3, 4);
// just write it
const p = { x: 3, y: 4 };
If your factory is one return new Thing(args) with nothing before it (no defaults, no branching, no caching, no wiring) it is a rename of the constructor, and the constructor was fine. This is the PriceFormatterFactory from the over-engineering trap: a wrapper that adds a layer and subtracts nothing. Name it for what it is and delete it.
The same restraint the whole part preaches applies here. Extract the factory the third time you see the construction logic drift or the third time a new scattered across call sites bites you, not the first. Two new Thing() calls are not a problem. Fifty, on the day the type needs to change, are the story this article opened with.
Summary
- A factory is any function that returns an object. No
new, nothis, no class required. In JavaScript it is often the better default, because a function chooses what it returns and a constructor does not. - The plain factory closes over private state and returns a small surface of methods. That is the module pattern parameterized, resting on closures.
- Reach for a factory to hide construction, decide the concrete type at runtime, return a cached or pooled instance, avoid
this, or keep the freedom to change what you return later. That last one matters most:new Thing()welded across call sites is a promise you cannot cheaply take back. newalmost always returns an instance of its class. The one escape (returning an object from the constructor) works but lies to the caller, which is exactly the honesty a factory gives you for free.- Polymorphic creation (the GoF Factory Method) is usually a type flag and a
switch, or a registry of small factories, not a class hierarchy. Every branch returns the same shape so callers stay uniform. - A factory can cache or pool instances, since it sits between the caller and construction. Great for clients and connections, a trap for anything holding per-request state, and pooling is a niche win for hot loops under GC pressure.
- The builder assembles a complex object step by step with chained methods that each return the builder, ending in a
build(). Query builders are the everyday example. When there is no ordering or validation, an options object beats it. - Abstract factory (a factory of related factories) is real but rarely needs its full ceremony in JavaScript; an object of factories usually covers it.
- When to use: hidden or varied construction, runtime type choice, reuse, or an evolving public creation API. When to avoid:
new Thing()or a plain object literal already reads clearly, and the factory would only add a hop.