Factory Method pattern
Open in the interactive app: practice, quiz, mock interview →In one minute
- A factory method is a function or method whose job is to decide which concrete type to create, so callers ask for "a storage backend for this URL" and receive something that satisfies an interface without naming the class.
- The Gang of Four form puts the factory method on a base class and lets subclasses override it. The everyday forms are simpler: a constructor function (
NewClient), an alternative constructor (datetime.fromtimestamp), or a registry that maps a key such as"s3"or"postgres"to a factory. - Unlike a constructor, a factory can return a subtype, a cached instance or an error, and it can be looked up by name at runtime. That is what makes plugin systems (Go's
database/sqldrivers) possible. - Do not add one for a single implementation, and in Go prefer returning concrete structs unless callers truly need to be shielded from the type. A string-keyed registry trades compile-time errors for runtime ones.
- Agents reach for
if kind == "a": return A() elif ...inside business logic. Review asks for the decision in one factory, at the edge of the system, with a clear error for unknown kinds.
How it works
The problem
A data export job writes reports to whatever storage a customer configures: local disk in development, S3 for most customers, Google Cloud Storage for a few, an in-memory store in tests. The first version has this in the export function:
def export(report, dest):
if dest.startswith("s3://"):
store = S3Store(bucket_from(dest), region=os.environ["AWS_REGION"])
elif dest.startswith("gs://"):
store = GcsStore(dest[5:])
else:
store = FileStore(dest)
store.put(report.name, report.render())The export logic now knows every backend, their constructor arguments and the environment variables they need. A second job that also writes to storage copies the same chain, and the copies drift: one supports gs://, the other forgot. Tests of export cannot use an in-memory store without adding a branch for it.
Pull the decision into one place
A factory takes the decision out of the business logic:
def open_store(url: str) -> Store:
scheme, _, rest = url.partition("://")
factory = STORES.get(scheme.lower())
if factory is None:
raise ValueError(f"unknown scheme: {scheme}")
return factory(rest)
def export(report, store: Store):
store.put(report.name, report.render())export depends only on the Store interface and receives a store from its caller. The composition root calls open_store(config.dest) once. Adding Azure Blob Storage means writing a class and one registration; nothing that uses stores changes. That is the open/closed principle applied to object creation.
The classic form: an overridable method
In the Gang of Four version, a base class has a template algorithm that calls an abstract create_x() method, and each subclass overrides it:
class ReportExporter:
def export(self, rows):
writer = self.create_writer() # the factory method
for row in rows:
writer.write(row)
return writer.finish()
class CsvExporter(ReportExporter):
def create_writer(self):
return CsvWriter(delimiter=",")This is useful inside frameworks, where the framework owns the algorithm and the application supplies one piece of it (Django's class-based views call get_queryset(), which you override). Outside frameworks, passing the writer or a writer factory as a parameter does the same job with composition, and avoids a class hierarchy whose only purpose is to choose one object.
Alternative constructors
Python's dict.fromkeys, datetime.fromtimestamp and int.from_bytes are factory methods in the plainest sense: classmethods that build an instance from a different kind of input. Go's convention is the same idea without classes: NewReader(r io.Reader) *Reader, errors.New, http.NewRequest. They exist because a constructor has one signature and cannot return an error (in Python) or can only be one function (in Go). Named factories give each way of building an object its own name and validation.
What a factory can do that a constructor cannot
- Return a different concrete type depending on input:
document.createElement("video")returns anHTMLVideoElement. - Return an existing instance: Java's
Integer.valueOfreturns cached objects for -128 to 127;logging.getLogger(name)returns the same logger for the same name. - Fail cleanly: return
(nil, err)or raise a specific error instead of half-initialising an object. - Be chosen at runtime by name, from configuration or a URL.
When not to use it
- One implementation.
UserServiceFactorythat always returnsUserServiceadds a file and a hop for readers. Call the constructor; add the factory when the second implementation arrives. - Returning interfaces in Go by reflex. Go's guidance is "accept interfaces, return structs": a constructor returning
*Clientlets callers use every method and define the narrow interface they need themselves. Return an interface when the concrete type is genuinely chosen at runtime. - Factories of factories. If configuring the factory needs its own factory, the abstraction is fighting you; pass the dependencies in directly.
- String keys where an enum would do. A registry keyed by free-form strings fails at runtime on a typo; if the set is closed and known at compile time, an exhaustive
switchover an enum fails at compile time instead.
Reviewing agent-written code
Asked to "support GCS as well as S3", an agent will find the if dest.startswith("s3://") chain and add an elif, usually in the business function, often in two places if the chain was copied. It may also invent a StorageFactory class with one static method, a StorageFactoryProvider and an abstract base for both. Both shapes are signs to stop and steer. Point the agent at one factory function at the edge, a registry if backends come from configuration, and constructor injection of the resulting Store into the code that uses it. In review, check that unknown kinds produce an error that names the bad value, that the factory is the only place that reads backend-specific environment variables, and that tests pass an in-memory implementation instead of patching the factory.
The same shape elsewhere
A factory keyed by scheme or name is the finding things fast shape in miniature: a lookup table from a key to a handler. The same structure routes HTTP paths to handlers, maps MIME types to decoders, maps Linux filesystem types (ext4, xfs) to their mount code in the kernel's filesystem registry, and resolves jdbc:postgresql:// URLs to drivers. Each one has the same failure modes: duplicate registration, a missing entry discovered only at runtime, and initialisation order deciding what is registered.
Down the stack
Layer 1The contract
A factory promises: for a known key and valid input it returns an object that satisfies the interface; for an unknown key or invalid input it fails with an error that names the problem, and never returns a half-built object or a nil interface without an error. Whether it returns a fresh or a shared instance is part of the contract and must be documented, because callers that mutate a shared instance will affect each other. A registry promises that registration happens before first use and that duplicate keys are rejected rather than silently overwritten.
Layer 2The mechanism
Go's database/sql is the textbook plugin factory. A driver package calls sql.Register("postgres", &Driver{}) in its init() function; Register takes a write lock on a package-level map[string]driver.Driver and panics on a duplicate name. Importing the driver for side effects (import _ "github.com/lib/pq") runs that init. sql.Open("postgres", dsn) takes a read lock, looks up the name, and returns an error sql: unknown driver "postgres" (forgotten import?) if it is missing. The image package does the same with image.RegisterFormat, matching magic bytes at the start of a file to pick a decoder. Python packages advertise factories through entry points in their metadata, which tools such as pytest load with importlib.metadata.entry_points(group=...). In the browser, document.createElement(tag) looks up the tag in a table of element interfaces and custom elements registered with customElements.define.
Layer 3The machine
Returning through an interface has a cost in Go: a concrete value stored in an interface usually escapes to the heap, so a factory returning Store instead of *MemoryStore may add one allocation per call, and the compiler can no longer inline methods called through it. Each call becomes an indirect call through the itab, about 1 to 2 ns when predicted. Go's profile-guided optimisation can devirtualise hot interface call sites when the profile shows one dominant concrete type. The JVM does this speculatively: if class-hierarchy analysis or the profile shows one receiver type at a call site, it inlines the method behind a cheap type guard. In V8, objects created by one factory with fields assigned in the same order share a hidden class, which keeps property access monomorphic; a factory that builds the "same" object in different field orders on different branches creates several shapes and slows every reader.
The other machine-level win is that factories can recycle. Because callers never call the constructor directly, the factory can hand out objects from a free list or a sync.Pool, which turns per-request allocation into reuse and cuts GC work. Constructors cannot do that.
Why it's built this way
- The code that uses an object rarely needs to know its concrete type, but someone has to choose it.
- Choosing in every caller duplicates the decision, and the copies drift, so the choice moves into one function.
- The choice often depends on runtime data (a URL, a config file, a file's magic bytes), so it must be a lookup, not a compile-time
new. - A function can return subtypes, cached instances and errors, which a constructor cannot.
- Indirection costs allocations, inlining and readability, so a factory is worth adding only when there are real alternatives to choose between.
Diagram
flowchart LR
CFG["config: dest URL"] --> F["open_store(url)"]
F --> P{"parse scheme"}
P -- "missing" --> E1["error: bad url"]
P --> R{"registry lookup"}
R -- "not found" --> E2["error: unknown scheme"]
R -- "mem" --> M["MemoryStore (cached per name)"]
R -- "s3" --> S["S3Store"]
R -- "gs" --> G["GcsStore"]
M --> U["export(report, store)"]
S --> U
G --> UWorked example
A company runs 14 batch jobs that write files. Before the refactor each job had its own if/elif over destinations, 14 copies in total, and an audit found 5 different sets of supported schemes. Two customers on GCS could not use 3 of the jobs. Adding a new backend meant 14 pull requests across 6 repositories.
The team created a small shared package with a Store interface, a registry, and open_store(url). Each backend lives in its own module and registers itself. The jobs take a Store as a parameter, and each job's main calls open_store once. Adding Azure Blob Storage afterwards was one module of about 120 lines and one line in the registry's setup, deployed by a dependency bump. Test suites switched to mem:// stores; the 14 jobs' tests dropped 230 mock.patch calls on cloud clients.
The performance check came out as expected: open_store runs once per job, so the lookup and the extra interface hop are invisible next to a 40 ms object-storage round trip. The one regression was in a hot path where a job opened a store per row (2 million rows) and each S3 store built a new HTTP client with its own connection pool. Caching stores by bucket inside the S3 factory, the same trick the memory factory uses per name, brought that job from 50 minutes to 6.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Direct constructor call | Explicit; inlinable; errors at compile time | Caller knows the concrete type | One implementation, or the caller genuinely needs it |
Factory function with a switch | One place to decide; still compile-time checked | Adding a variant edits the function | Small, closed set of variants in one codebase |
| Registry keyed by name | New variants plug in without edits; choice from config | Runtime errors on typos; registration order matters | Plugins, drivers, backends chosen by URL or config |
| GoF overridable factory method | Framework owns the algorithm; subclass supplies one part | Inheritance couples subclass to base | Frameworks with template algorithms |
| Inject the object or a factory parameter | Caller decides; trivial to test | Wiring code in the composition root | Most application code |
Pitfalls in production
- Business logic branching on concrete types (
isinstance(store, S3Store)) after the factory, which undoes the abstraction. Fix: add the capability to the interface or handle it inside the backend. - Silent overwrite on duplicate registration, so whichever module loads last wins. Fix: reject duplicates loudly, as
sql.Registerdoes with a panic. - Missing registration discovered in production because the plugin module was never imported. Fix: import registrations explicitly in
main, and add a startup check that every configured key resolves. - A factory that returns a shared instance documented as fresh, so two callers mutate the same object. Fix: document the lifetime and test it.
- Expensive clients created per call inside the factory. Fix: cache per key inside the factory, or create once in the composition root and pass it down.
- Error messages that hide the input (
invalid backend). Fix: name the bad key and list the known ones.
Leadership lens
- Ask what varies and who adds variants. If other teams or plugins add backends, a registry is worth its runtime-error cost; if one team owns a closed set, a
switchis safer. - Make the composition root the only caller of factories in an application. Code that calls factories deep inside business logic is code that is hard to test.
- Treat the factory's key space as an API: document the supported schemes, version them, and fail with helpful errors. Customers and on-call engineers read those messages.
- Use this as a reviewable instruction for agents: "new backend = new module + one registration; do not edit callers". It turns a risky cross-cutting change into an additive one.
Check yourself
Why can a factory return a cached instance when a constructor cannot?
Show answer
A constructor always allocates a new object; a factory is an ordinary function that can look up and return an existing one, as
logging.getLoggerandInteger.valueOfdo.Go's guidance is "accept interfaces, return structs". When should a Go factory return an interface anyway?
Show answer
When the concrete type is chosen at runtime from input, such as a URL scheme or driver name, so the caller cannot know it in advance.
A registry is keyed by strings from a config file. What failure does that introduce, and how do you contain it?
Show answer
A typo or missing plugin is only found when the key is looked up. Validate every configured key at startup and fail fast with an error that names the key.
When is an exhaustive
switchover an enum better than a registry?Show answer
When the set of variants is closed and owned by one team; the compiler then reports every unhandled case, and nothing depends on registration order.
Watch
- Factory Method Pattern (Christopher Okhravi): the Gang of Four structure worked through step by step on a whiteboard.
- 10 Design Patterns Explained in 10 Minutes (Fireship): a quick comparison of factory with the other creational patterns in JavaScript.
Free resources
- The Factory Method Pattern (Brandon Rhodes, python-patterns.guide): why Python usually passes callables instead of subclassing, and where the pattern still fits.
- database/sql source (Go):
RegisterandOpen, a production driver registry in a few dozen lines. - image.RegisterFormat (Go docs): a factory chosen by magic bytes rather than by name.
- Entry points specification (Python Packaging): how Python plugins advertise factories for other packages to discover.
- Go Code Review Comments (Go wiki): the guidance on defining interfaces at the consumer and returning concrete types.
- Profile-guided optimization (Go docs): how the Go compiler devirtualises hot interface calls that factories introduce.