mdashikjs/blog
All posts
Factory Functions: When `new` Feels Wrong
Backend

Factory Functions: When `new` Feels Wrong

Backend4 min

Factory Functions: When `new` Feels Wrong

Classes and `new` are fine until you need to return different shapes, delay construction, or validate inputs before committing. Factory functions handle all three without ceremony.

JavaScriptTypeScriptDesign Patterns
Share:

The first sign you want a factory

You want to return different implementations depending on input:

function createStorage(config: { driver: 'memory' | 'redis' | 's3' }): Storage {
  if (config.driver === 'memory') return new MemoryStorage();
  if (config.driver === 'redis') return new RedisStorage();
  return new S3Storage();
}

A constructor that can return three different classes is doing two jobs at once. A factory function is the natural home for that dispatch.

Validation before construction

Constructors that throw are painful to test and awkward to compose. A factory can return a Result:

type Result<T> = { ok: true; value: T } | { ok: false; error: string };

function createUser(input: unknown): Result<User> {
  const parsed = UserSchema.safeParse(input);
  if (!parsed.success) return { ok: false, error: parsed.error.message };
  return { ok: true, value: new User(parsed.data) };
}

Callers handle both outcomes explicitly. No try/catch, no half-constructed objects.

Hiding the construction step entirely

Sometimes the class itself should not be exported. Callers get objects but cannot subclass or inspect internals:

// user.ts
class UserImpl { /* ... */ }

export type User = InstanceType<typeof UserImpl>;
export function createUser(data: UserData): User {
  return new UserImpl(data);
}

Consumers import createUser and the User type. They cannot new UserImpl(). This gives you freedom to swap the implementation later — maybe UserImpl becomes a plain object, maybe it becomes a Proxy. Nothing outside the module breaks.

The pattern I use most

For domain objects I rarely use classes at all. A factory returning a plain object with closures is smaller and easier to test:

export function createOrder(items: Item[]) {
  const lines = [...items];
  return {
    addItem: (item: Item) => lines.push(item),
    total: () => lines.reduce((sum, l) => sum + l.price, 0),
    lines: () => [...lines],
  };
}

No this binding headaches. No worrying about .bind() when passing methods as callbacks. The methods close over lines directly.

When classes actually win

Classes are better when:

  • You need instanceof checks (error hierarchies, especially)
  • You are interoperating with frameworks that expect classes (NestJS decorators, TypeORM entities)
  • You have a large method count and want prototype-based memory savings

For everything else, factory functions are my default. They compose better, test better, and require less mental overhead when reading.

The rule of thumb

If construction is a single, always-succeeding step with no runtime decisions, use a class. The moment you want to validate, branch, or hide the type, reach for a factory.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.