Factory & Builder
Creational patterns: factory method and abstract factory to centralize object creation, and the builder for constructing complex objects (a Report, a Transaction) step by step.
On this page
Creational patterns decouple your code from the messy details of making objects. Two earn their keep
constantly: the Factory (in its method and abstract forms) centralizes the decision of which concrete
class to create, and the Builder constructs complex objects step by step without a monstrous constructor.
Both attack real smells in ledger-legacy - scattered new statements and telescoping constructors.
The problem with scattered new
Every new PostgresTransactionDao() sprinkled through the code hard-wires a concrete class at that spot - the
Dependency Inversion problem, but for construction. When creation also involves a decision ("which fee
policy for this transaction type?"), that logic gets copy-pasted everywhere a policy is needed.
Factory Method centralizes the choice in one place:
class FeePolicyFactory {
FeePolicy forType(Type type) {
return switch (type) {
case DOMESTIC -> new DomesticFee();
case INTL -> new IntlFee();
case CRYPTO -> new CryptoFee();
};
}
}Now the "which class" decision lives in exactly one spot. Callers ask the factory and receive a FeePolicy,
never naming the concretes. (Note the tension with Open/Closed - a switch here still changes when types are
added. That's an acceptable, contained trade-off: one switch in a factory beats the same switch scattered
across the codebase, and it's the seam you'd later replace with registered strategies.)
Abstract Factory: families of related objects
Abstract Factory goes one level up: it creates families of related objects that must be used together.
ledger-legacy must export reports in different formats, each needing a matching header, row, and footer
renderer:
interface ReportComponentFactory { // a family of matching parts
HeaderRenderer header();
RowRenderer row();
FooterRenderer footer();
}
class PdfReportFactory implements ReportComponentFactory { ... } // all PDF parts
class CsvReportFactory implements ReportComponentFactory { ... } // all CSV partsPick PdfReportFactory and you're guaranteed a consistent set of PDF renderers - you can't accidentally mix a
PDF header with a CSV footer. Use it when objects come in families that must match; it's overkill for a single
object.
Builder: taming the telescoping constructor
ledger-legacy's Report object has ten fields, half optional, producing the dreaded telescoping
constructor:
new Report(accountId, from, to, null, true, false, "USD", null, 2, null); // 🚩 what do these mean?Positional args are unreadable and error-prone (swap two booleans, silent bug). The Builder names each part and reads like prose:
Report report = Report.builder()
.accountId("ACC-1")
.period(from, to)
.currency("USD")
.includeFees(true) // only set what you need; sensible defaults for the rest
.build(); // can validate invariants here before returningThe builder also gives you one place to validate invariants before the object exists, and it pairs naturally
with immutability: build a fully-formed, unchangeable Report in one expression. (Lombok's @Builder or a
record with a companion builder generates this for you.)
Reach for a builder past ~4 params or optional fields
A constructor with two or three required args is fine - a builder there is ceremony. The builder pays off when you have many parameters, several optional ones, or same-typed params that are easy to transpose (two Strings, two booleans). Its readability and its single validation point are the wins; don't builder-ify a two-field value object.
A factory is the parts counter at a garage: you don't wander the warehouse grabbing components (scattered new); you tell the clerk 'brake kit for a 2019 sedan' and they hand you the right concrete part - and an abstract factory hands you a matching set (pads, rotors, and clips that fit together). A builder is a build-your-own sandwich order: instead of a cryptic combo number (positional constructor), you say 'rye, turkey, no onions, extra mustard' - each choice named, optional bits skipped, and the kitchen checks it's a valid sandwich before making it. One centralizes which object; the other clarifies how a complex one is assembled.
Two problems in ledger-legacy: (1) an AccountStatement object has 3 required and 6 optional fields, and call
sites are full of nulls and hard-to-read positional constructors; (2) code in five places does
if (region == EU) new EuTaxRules() else new UsTaxRules() to pick a tax-rules object. Which creational pattern
fits each, and why not swap them?
When is the Builder pattern the right choice over a plain constructor?
Key takeaways
- Creational patterns decouple code from concrete construction, just as Dependency Inversion decouples from concrete collaborators.
- Factory Method centralizes the 'which concrete class' decision in one place so callers depend only on the abstraction.
- Abstract Factory creates families of related objects that must be used together (e.g. matching PDF vs CSV report parts).
- The Builder replaces telescoping constructors with named, optional steps, gives one place to validate invariants, and pairs with immutability.
- Reach for a builder past ~4 params or with optional/same-typed fields; a plain constructor is better for two or three required args.