Skip to content

Replace Constructor with Factory Function

Replace a direct call to a constructor with a call to a factory function. A constructor is constrained: it must share the type’s name and it always returns an instance of exactly that type. A factory function is free — it can have a descriptive name, choose which subtype to return, hand back a cached instance, or validate before it builds. Callers ask the factory for what they want, and the factory decides how to make it.

A raw constructor leaks construction decisions onto every caller. You see new Employee("Sara", "manager") and the caller is left to remember which string means what. Worse, when the type has variants — a manager is really a different kind of employee than an engineer — the bare constructor cannot return the right subtype, so callers branch on a type code themselves. The constructor’s name is fixed to the class, so it cannot say hire or fromJson or default. Each of these is the constructor being too limiting for the job.

An employee type whose constructor takes a type string. After, a factory function picks the right subtype and reads clearly.

// Before
class Employee {
constructor(public name: string, public type: string) {}
monthlyBonus(): number {
return this.type === "manager" ? 1000 : 200;
}
}
const e = new Employee("Sara", "manager");
// After
abstract class Employee {
constructor(public name: string) {}
abstract monthlyBonus(): number;
}
class Manager extends Employee {
monthlyBonus(): number {
return 1000;
}
}
class Engineer extends Employee {
monthlyBonus(): number {
return 200;
}
}
function hire(name: string, role: string): Employee {
switch (role) {
case "manager":
return new Manager(name);
case "engineer":
return new Engineer(name);
default:
throw new Error(`Unknown role: ${role}`);
}
}
const e = hire("Sara", "manager");
  1. Write a factory function whose body does nothing but call the existing constructor and return the result. Give it a name that says what it makes, like hire or defaultConfig.
  2. Redirect callers to the factory one at a time, replacing each constructor call with a factory call. Run your tests after each.
  3. Once every caller goes through the factory, you have a single chokepoint. Now you can enrich it: validate arguments, return a cached instance, or branch to a subtype.
  4. If you are introducing subtypes, create them, move each branch’s behaviour onto the matching subtype, and let the factory return the right one based on its input.
  5. Run your tests after each change. The factory’s signature stays stable for callers even as what it builds evolves behind it.
  6. If the language allows, make the raw constructor private or non-public so the factory is the only supported way in.

Reach for a factory when construction needs a clearer name than the class allows, when the right concrete type depends on the inputs, when you want to hide or pool instances, or when you want to validate before an object exists. The factory becomes the single, well-named door through which objects enter the system.

The trade-off is a layer of indirection: callers no longer see new and the concrete type directly, which can make navigation slightly less obvious. For a plain value type with one shape and no variants, a direct constructor is simpler and a factory is needless ceremony. Add the factory when the constructor’s rigidity starts to cost you — not before.

What is a key limitation of a constructor that a factory function escapes?
What is the safe first step when introducing a factory?
Why does routing all construction through one factory help?
When is a plain constructor preferable to a factory?