Start Learning
Javaneer
Back to stage
Stage 1·Design Patterns Applied

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.

15 min readIntermediate
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 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 parts

Pick 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 returning

The 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 parts counter and a sandwich order

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.

Choose the creational pattern

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.
Was this lesson helpful?
Edit this page on GitHub