Skip to content
aipatterns.dev

Design patternCreational Patterns

Factory Method

Let a subclass or a supplied function decide which concrete object to create, so shared workflow code depends only on an interface.

Complexity
Low
Seen in the wild
High
Also known as
Virtual Constructor

Intent

Declare a method that returns an object of some interface type, and let subclasses (or a passed-in function) override it to choose the concrete class. The surrounding workflow is written once against the interface and never names the concrete type it works with.

Forces

  • Shared workflow vs varying collaborator: the steps around an object stay the same, but which object is used differs per variant.
  • Calling new directly vs depending on abstractions: a hard-coded constructor ties reusable code to one implementation.
  • Adding a variant vs editing working code: a new kind of product should not mean another branch in the shared method.
  • Testability vs construction buried inside logic: you can't swap in a fake when the real object is built halfway through a method.

Therefore

Move the new call into an overridable creation method, and write the rest of the workflow against the interface it returns.

The base class (the creator) owns the algorithm and calls createX() when it needs a product. Each subclass overrides only that method. In languages without inheritance, or when you prefer composition, the factory method becomes a function field or constructor parameter: same idea, fewer classes.

Structure

Diagram source (Mermaid)
classDiagram
  class ContactImport {
    <<abstract>>
    +run(raw) ImportSummary
    +createParser() RecordParser
  }
  class CsvImport {
    +createParser() RecordParser
  }
  class AddressBookImport {
    +createParser() RecordParser
  }
  class RecordParser {
    <<interface>>
    +parse(raw) Contact[]
  }
  class CsvParser {
    +parse(raw) Contact[]
  }
  class BlockParser {
    +parse(raw) Contact[]
  }
  ContactImport <|-- CsvImport
  ContactImport <|-- AddressBookImport
  ContactImport ..> RecordParser : uses
  RecordParser <|.. CsvParser
  RecordParser <|.. BlockParser
  CsvImport ..> CsvParser : creates
  AddressBookImport ..> BlockParser : creates

Participants

  • Product (RecordParser): the interface the workflow depends on.
  • Concrete products (CsvParser, BlockParser): the implementations that actually get built.
  • Creator (ContactImport): runs the shared workflow and declares the factory method createParser().
  • Concrete creators (CsvImport, AddressBookImport): override the factory method to return one concrete product.

Use it when

  • A base class or framework runs a fixed workflow but must let extensions decide which helper object it uses.
  • You find the same method duplicated across classes except for the one new SomethingSpecific() line.
  • A library needs users to plug in their own implementation of an internal component without forking it.
  • You need a seam for tests: overriding the creation method is the smallest change that lets a test inject a fake.

Do not use it when

  • There is one concrete product and no realistic second. Call the constructor; add the seam when the second variant shows up.
  • The only reason for subclassing is to pick the product. Pass the product - or a factory function such as () => RecordParser - into the constructor instead; in a language with first-class functions this is plain dependency injection and avoids a class per variant. Keep the overridable method when subclasses must weave creation into their own setup logic.
  • You need to create several related objects that must match each other. Use Abstract Factory.
  • The selection is a lookup from a config string to a class. A map of key to constructor is clearer than a creator hierarchy.

Application grill

Inspect first: the runtime choices between concrete classes and who makes them (callers, config, environment); existing createX() helpers - many codebases already have this pattern under a plain name; a DI container's presence.

Missing pieces: none beyond the language: pass a creator function. A DI container helps only when construction graphs are wide and wired in many entry points; do not introduce one for a single product choice.

Ask before you build:

  • Scale: how many products exist and how often does the set change - a stable pair is a conditional, not a factory?
  • Performance: is creation on a per-request path where the indirection shows?
  • Reliability: must an unknown product fail at startup (validate the registry) or at first use?
  • Budget: the indirection's review cost versus the branch it removes - does the set actually change?
  • Implementation: where does the product registry live - code, configuration, or plugins - and who extends it?

Implementation steps

  1. Find the place where shared code calls a concrete constructor, and extract an interface for what the code actually uses from that object.
  2. Make the shared code depend on the interface type only.
  3. Replace the new call with a call to a creation method (createParser()). Start with a default implementation that returns the existing class, and keep tests green.
  4. Make the creation method abstract (or keep the default) and move each variant into a subclass that overrides it.
  5. If subclasses exist only to override that one method, switch to a constructor parameter holding a factory function and delete the subclasses.
  6. Keep any parameters needed for construction (delimiter) on the concrete creator, not on the shared workflow.

Example

A CRM imports contacts from CSV exports and from address-book exports. The import workflow (parse, validate, count) is the same; only the parser differs.

interface Contact {
  email: string;
  name: string;
}

interface RecordParser {
  parse(raw: string): Contact[];
}

class CsvParser implements RecordParser {
  constructor(private readonly delimiter: string) {}
  parse(raw: string): Contact[] {
    const [header, ...rows] = raw.trim().split("\n");
    const cols = header.split(this.delimiter);
    return rows.map((row) => {
      const f = row.split(this.delimiter);
      return { email: f[cols.indexOf("email")] ?? "", name: f[cols.indexOf("name")] ?? "" };
    });
  }
}

// Address-book export: "key: value" lines, a blank line between contacts.
class BlockParser implements RecordParser {
  parse(raw: string): Contact[] {
    return raw.trim().split("\n\n").map((block) => {
      const fields = new Map(block.split("\n").map((l) => l.split(": ", 2) as [string, string]));
      return { email: fields.get("email") ?? "", name: fields.get("name") ?? "" };
    });
  }
}

abstract class ContactImport {
  // The factory method: subclasses decide which parser to build.
  protected abstract createParser(): RecordParser;

  run(raw: string): { imported: number; rejected: number } {
    const contacts = this.createParser().parse(raw);
    const valid = contacts.filter((c) => c.email.includes("@"));
    return { imported: valid.length, rejected: contacts.length - valid.length };
  }
}

class CsvImport extends ContactImport {
  constructor(private readonly delimiter: string) {
    super();
  }
  protected createParser(): RecordParser {
    return new CsvParser(this.delimiter);
  }
}

class AddressBookImport extends ContactImport {
  protected createParser(): RecordParser {
    return new BlockParser();
  }
}

console.log(new CsvImport(";").run("email;name\nana@example.com;Ana\nnot-an-email;Bo"));
// { imported: 1, rejected: 1 }
const book = "email: li@example.com\nname: Li\n\nemail: kim@example.com\nname: Kim";
console.log(new AddressBookImport().run(book)); // { imported: 2, rejected: 0 }

Trade-offs

Gains

  • The shared workflow depends on an interface, not on any concrete class.
  • A new input format is a new creator and product; run() is untouched.
  • Construction is a single, overridable seam that tests can replace.

Costs

  • One creator subclass per product is a parallel hierarchy that grows with every variant.
  • Inheritance couples the variant to the base class's internals. A constructor parameter avoids that.
  • Readers must look in the subclass to learn what the workflow actually uses.

Relations

  • Abstract Factory is often a set of factory methods grouped on one object, producing a matched family instead of one product.
  • Template Method is the larger shape a factory method usually lives in: a fixed algorithm with an overridable step, where the step is "create the collaborator".
  • Prototype creates by copying a configured instance, so it needs no creator subclass per product.
  • Builder concerns how one complex object is assembled step by step. Factory Method only decides which class to instantiate.
  • Strategy is what you often get after refactoring: pass the parser in, and the creator hierarchy disappears.

Agent checklist

  • There are at least two concrete products today, or the code is a library extension point. Otherwise keep the constructor call.
  • You considered a constructor parameter (an object or a factory function) before adding subclasses.
  • The shared workflow references only the product interface, with no instanceof checks on the result.
  • Construction parameters live on the concrete creator, not on the shared workflow.
  • Existing tests pass unchanged, and each concrete creator has a test that checks which product it returns.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.