PatternsGENERALLANGUAGE-SPECIFICFRAMEWORK-SPECIFIC

Factory

Encapsulates a construction decision that callers should not make. When there is no decision, a factory is a function that calls new and charges you a file for it.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

When does creating an object deserve its own abstraction, and when is new Thing() the right answer forever?

The requirement

Creating a Report requires choosing a renderer by format, validating that the requested date range is permitted for the requester's plan, and wiring in a clock and a locale. Six call sites do this, three of them slightly differently.

The obvious build

Let each call site construct the report. The constructor takes what it needs, and callers pass it. Adding a wrapper around new is ceremony.

Why it breaks

The three "slightly different" call sites are the finding. One forgot the licence check, one passes a locale string instead of a resolved locale, one defaults the clock to now() inline and is untestable as a result (Time as a Dependency).

How it breaks as requirements change
  • The three "slightly different" call sites are the finding. One forgot the licence check, one passes a locale string instead of a resolved locale, one defaults the clock to now() inline and is untestable as a result (Time as a Dependency).
  • Adding the spreadsheet renderer means six edits, and there is no list of the six (Shotgun Surgery).
  • The constructor grows to eight parameters, four of which are only needed for one renderer, and callers pass null for the rest (Long Parameter List).
  • Validation ends up after construction, so a Report can exist in an invalid state for a few lines — which is exactly long enough for someone to persist it.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The date-range rule is a licensing constraint and must be identical at every call site (Enforcing Invariants).
  • The renderer set is open: PDF and CSV today, a spreadsheet export next quarter.
  • The team already has a convention of a factory per entity, and most of them contain one line (Pattern Overuse).
Invariants
  • A Report that exists is valid: the range is permitted, the renderer is set, the locale is resolved. There is no half-built report (Making Illegal States Unrepresentable).
  • Two callers requesting the same report on the same day get the same object graph — construction must not depend on ambient state (A Deterministic Core).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • One place owns "what does it take to make a valid report" — the licence check, the renderer choice, the defaults.
  • The constructor owns nothing but assignment. Once a factory exists, a constructor that also validates duplicates the rule (Duplicate Knowledge).
  • Callers own the *request* — format, range, requester — and none of the resolution.
Boundaries
  • The factory is the boundary between "a request for a report" and "a valid report object", and the invariant is enforced at exactly that line (Where Invariants Live).
  • Make the constructor private or internal, or the boundary is advisory and someone will route around it under deadline (Exposing Too Much).
  • The renderer selection is a second, smaller boundary inside the factory, and it is the part that changes when a format is added (Strategy).

The factory that should not exist

Start with the failure case, because it is the common one. A factory whose body is a constructor call has no decision in it: it does not choose an implementation, does not enforce an invariant, does not resolve a dependency. It exists because the codebase has a convention.

The cost is small per instance and it is charged every time. Six factories like this are six files, six names, six hops, and one strong signal to new engineers that indirection is what good code looks like here (Speculative Generality).

smellFactory with nothing to decide

looks like A class or function whose entire body is return new Thing(a, b, c) — the arguments passed straight through, no branch, no validation, no lookup, and exactly one type ever returned.

suggests A convention applied without its condition. Somebody was told not to call new, or a code generator produces one of these per entity, and nobody has asked what decision is being encapsulated.

fix Delete it and call the constructor, or give it something to decide. If the name is the only value it adds, make it a static method on the type rather than a separate file, so the validation stays next to what it protects.

when this is fine Genuinely fine in three cases. First, when the type is exported from a library and you want to keep the freedom to change the constructor without a breaking change — the indirection is a versioning seam (API Stability). Second, when the language cannot express a named constructor and the factory's *name* carries meaning new cannot: Duration.ofSeconds(30) beats new Duration(30) and always will (Units in Names and Types). Third, in a codebase whose framework requires it, where fighting the convention costs more than the hop (What a Framework Charges).

The factory that earns it

The version below has three decisions in it and each one is the answer to a specific failure that happened: a call site that skipped the licence check, a call site that defaulted the clock inline, and a renderer selection that was duplicated. None of those are hypothetical and none of them are fixed by a constructor.

Note the return type. The licence failure is an expected outcome of a valid request, so it is a value; a missing renderer is a wiring bug, so it throws. Getting those two the wrong way round is how a factory ends up wrapped in a try at every call site (Exceptions, Where They Help and Where They Hide the Flow).

Three decisions, one place
1export function createReport(
2 req: ReportRequest,
3 deps: { clock: Clock; licence: LicenceCheck; renderers: Map<Format, Renderer> },
4): Result<Report, RangeNotPermitted> {
5 // decision 1: an invariant no call site may skip
6 if (!deps.licence.permits(req.requester, req.range))
7 return err(new RangeNotPermitted(req.range, req.requester.plan))
8
9 // decision 2: which implementation, from data
10 const renderer = deps.renderers.get(req.format)
11 if (!renderer) throw new Error(`no renderer for ${req.format}`) // wiring bug
12
13 // decision 3: resolve ambient things once, here
14 return ok(new Report(req, renderer, deps.clock.now(), resolveLocale(req)))
15}
16
17// Report's constructor is internal to the module. There is
18// one way to make a valid report, and it is this one.

The constructor still exists and still just assigns. The factory is not replacing it — it is holding the three decisions that were previously copied, skipped and inlined across six call sites.

Which shape, and what each one costs

The word "factory" covers four distinct things with different dependency implications, which is why the review comment "use a factory" so reliably produces the wrong one. Pick by what varies, not by the name.

Construction has grown a decision. Where does it go?

Something about creating this object is not trivial. What is the smallest structure that holds it?

Public constructor

when Construction assigns fields and makes no decision. The overwhelmingly common case.

cost None — this is the baseline. It becomes wrong only when a rule must hold for every instance and cannot be expressed in the constructor.

Named static factory

when The name carries meaning (Money.fromCents), or there are several ways to build the same type, or validation belongs with the type.

cost One static method. Cannot take injected dependencies without smuggling them in as parameters or globals (Hidden Global State).

Standalone factory function or class

when Construction needs collaborators the type must not know about — a clock, a licence service, a registry — or must enforce a rule across every call site.

cost A file, a name, a hop, and callers must now be able to reach the factory, which usually means it goes into the DI graph (Wiring and the Composition Root).

Abstract factory (an interface with implementations)

when A whole *family* of related objects must vary together: a real and an in-memory stack, a per-tenant variant, a platform-specific widget set.

cost Two layers of indirection and a second implementation to keep in step. Almost never justified for a single object type, and this is the shape people build when they meant the third option (Pattern Overuse).

How to build it

Most important first.

  • Ask what decision construction makes. If the answer is "none — it assigns four fields", there is no factory here and adding one is pure cost.
  • Prefer a named constructor first: Report.forRange(...) is a factory, costs one static method, no new file, no new type, and it keeps the validation with the type it protects.
  • Move to a standalone factory when construction needs dependencies the type should not know about — a clock, a licence service, a renderer registry — because a type that reaches for those has acquired them permanently (Volatile Dependencies).
  • Return a result rather than throwing when the licence check fails, since "this range is not permitted on your plan" is an expected business outcome, not a bug (An Error Taxonomy That Survives Contact).
  • Keep the selection data-driven — a map from format to renderer — so a new format is one entry rather than a new branch (Wiring and the Composition Root).
  • Reach for an abstract factory (a factory *interface* with several implementations) only when the whole family of things being created must vary together — a test harness, a multi-tenant variant. That case is rare and is not the same as "a factory".

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • Named change — "add a spreadsheet format": with the factory, one renderer plus one registry line; without it, six call sites, three of which will be found by grep and one by a bug report. The factory earns its cost here.
  • Named change — "reports over ninety days need a plan check": with the factory, one edit and it applies everywhere by construction; without it, the rule lands in five of six places and the sixth is the batch job nobody remembered.
  • Named change — "add a field to Report": the factory makes this *no cheaper at all*, and adds one place to update. A factory that only wraps new is exactly this case with none of the benefits above.
  • The acid test, stated plainly: if you cannot name a construction decision the factory encapsulates, it does not make any change cheaper and it costs a file, a name and a hop (Pattern Overuse).
What the recommended approach costs
  • Construction is no longer visible at the call site. Finding out what a Report actually contains now requires opening the factory.
  • Making constructors private buys the invariant and breaks the tools that expect to construct freely — ORMs, serializers, some test frameworks.
  • The rule "callers should not construct" is enforced by convention in most languages, so the boundary is only as strong as review.

What can go wrong

Failure modes
  • The factory becomes a service: it constructs, then saves, then emits an event. Now nothing can build a report without a database (God Object).
  • A second construction path appears — deserialization, a test builder, a migration script — and it bypasses the licence check. This is the most common way factory-enforced invariants leak in production (Invariant Leaks).
  • The factory takes a parameter for every possible variation, and its signature is worse than the constructor it replaced.
  • The mitigation fails too: making the constructor private breaks the ORM, which needs to instantiate without going through the factory, so an escape hatch appears and is used by everyone (What an ORM Buys and What It Costs).
Dependencies, and their direction
  • The factory depends on the licence service, the clock, the locale resolver and the renderer registry — a deliberate concentration, so the created object depends on none of them (Dependency Direction).
  • Callers depend on the factory and on the request type. They lose the ability to construct directly, which is the point and also the cost.
  • Nothing should depend on the factory's internals. A caller that reads the renderer off the factory has reintroduced the coupling the factory removed (Information Hiding).
Misreads
  • "Never call new directly." This is the belief that produces a factory per class, most of which return new X(args) unchanged. Direct construction is correct whenever construction has no decision in it (KISS: Simplest for the Requirements You Have).
  • "A factory decouples callers from implementations." Only if there is more than one implementation. With one, callers are coupled to the same class through one more hop (Interface Versus Implementation).
  • "Factories are for dependency injection." Related but different: DI supplies dependencies, a factory encapsulates a construction decision. Conflating them is how a codebase ends up with both and needs neither (Dependency Injection).
  • "The builder pattern is the answer to long constructors." A builder makes a long parameter list tolerable to write; it does not make the object simpler, and it usually permits partially-built states the constructor forbade (Making Illegal States Unrepresentable).
Smells this explains
  • long-parameter-list
  • primitive-obsession

Testing it, and how it ages

What to test, and at which boundary
  • Test the licence rule through the factory, since that is where it lives, and assert the failure case returns a value rather than throwing (Result Types).
  • Test that the created object is valid, not that the factory called the right collaborators. Asserting on mock invocations here tests the wiring and freezes it (Mocking).
  • One test per renderer that the registry resolves it, because a missing entry is a runtime failure by design.
  • A test that no other construction path exists — in practice, a check that the constructor is not public outside the module (What to Automate Out of Review).
How this design ages
  • Factories accrete parameters. The healthy response is a request object; the unhealthy one is optional parameters that only some paths use (Introduce Parameter Object).
  • When a second family of variants appears — a test factory, a tenant-specific factory — that is the moment abstract factory becomes justified, and not before (Speculative Generality).
  • Factories are also where feature flags collect, because construction is the natural place to branch. That is convenient and it means the factory now has a lifecycle of its own, with flag cleanup as an ongoing obligation (Feature Flags and What They Cost).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALThat construction which makes a decision or enforces an invariant deserves one home is true regardless of language, because the alternative is the same decision copied to every call site.
  • LANGUAGE-SPECIFICLanguages with named constructors and default arguments (Python, Kotlin, Swift, Dart) absorb most of this in the type itself, so a separate factory type is rarely needed; Java and C# lack named constructors, which is why the static-factory idiom and then the factory class became conventions there. In Rust, impl Report { fn for_range(..) -> Result<Report, LicenceError> } is the whole pattern and the borrow checker makes the invalid intermediate state impossible anyway.
  • FRAMEWORK-SPECIFICIn a DI-container framework — Spring, .NET, NestJS — the container is already a factory, and adding your own for the same object produces two construction paths with different wiring. What the container cannot do is enforce a domain invariant at construction, which is the case where your own factory still earns its place (Wiring and the Composition Root).

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • Testing & Reliability Engineering — construction is where determinism is won or lost: a factory that resolves the clock, the locale and the id generator once is what makes a system reproducible in a test, and a constructor that reaches for now() is what makes it flaky.