Deep Dive
low level designdesign patternsoop

Factory Method: Let the Subclass Decide What to Build

A switch(type) buried inside your shared workflow is a design smell. Factory Method moves the 'new' to a subclass so the logic that uses an object never learns which one it got.

·17 min read
Medium

A logistics startup ships its first product with one method that plans a delivery: pack the cargo, dispatch a truck, hand back a receipt. Clean. Then the company wins a port contract and has to move freight by sea. Someone opens planDelivery(), adds if (mode == "sea") new Ship() next to the truck, and ships it. A year later there’s air freight, cold-chain, and last-mile bikes — and planDelivery() is a switch with five arms wrapped around three lines of logic that were the same the whole time.

The workflow never changed. Loading cargo and dispatching it and writing a receipt is identical whether the thing that moves is a truck or a ship. The only thing that varies is the one line that decides which vehicle to build — and that line is stuck in the middle of the code everyone is afraid to touch. Every new transport is a diff to the one method that must never break.

The Gang of Four’s fix is to take that single decision — which class do I instantiate? — and hand it to a subclass.

The intuition: the recipe is fixed, the ingredient is chosen at the last step

Think of a delivery dispatcher who follows the same checklist for every shipment — weigh it, label it, load it, log it. Nowhere on that checklist does it say truck or ship. There’s just one line: “load onto the assigned vehicle.” Which vehicle shows up at the dock is decided by whoever is running that route — the road team sends a truck, the sea team sends a ship. The dispatcher’s checklist never changes when a new route opens; a new team just brings a new vehicle.

The checklist is the Creator’s shared method. “Load onto the assigned vehicle” is the factory method. The truck and the ship are Products. The road team and the sea team are concrete Creators — each one answers the single question the checklist left open.

Why it exists: the smell it removes

Here’s the delivery method before the pattern — the shared steps we actually care about are trapped behind a switch that must be reopened for every new transport.

Before: the switch is welded to the workflow
java

Two things are wrong, and they’re the same thing. First, Logistics knows the names of every concrete transport — it can’t be reused or unit-tested without dragging Truck and Ship along. Second, the decision and the workflow change for different reasons but live in one method: adding air freight (a creation concern) forces you to edit the delivery steps (a workflow concern), and one slip breaks routes you never meant to touch.

Factory Method separates the two. The workflow moves into a base class and freezes. The new moves into subclasses, one per product. Watch it happen.

See it live

Below, the client code and the Creator’s planDelivery() are frozen — they never change as you click. Switch the Creator subclass and only one thing moves: which concrete Product the factory method returns, and therefore how the cargo travels. Adding a third transport (air) is just a third button — nothing on the left had to change to make room for it.

Pick a Creator subclass. Notice what stays frozen and what changes.
Client code · frozen
Logistics logistics = pickLogistics(config);
logistics.planDelivery(cargo);
Creator base · planDelivery() · frozen
product = createTransport() // factory method → Truckproduct.drive(cargo) // deliver via the Product interfacereturn receipt
Only the highlighted factory-method line resolves to a different class.
RoadLogistics
overrides createTransport()
↓ returns
«Product» Truck
Move cargo by road in a truck.
Truck.drive(cargo)
A fourth transport = one more Creator subclass. Nothing on the left changes.

That’s the whole promise made tangible. The caller says logistics.planDelivery(cargo) and gets truck behavior or ship behavior depending only on which subclass it’s holding — with zero conditional logic in the method doing the work.

The structure

Four roles, and one arrow that carries the pattern: the Creator’s workflow points at the Product interface, never at a concrete product. The concrete Creators sit on the bottom row, free to multiply.

usesextendsextendsimplementsimplementscreatescreatesLogistics«abstract» · planDelivery() + createTransport()«interface»Transport · load() · move()RoadLogisticscreateTransport() → TruckSeaLogisticscreateTransport() → ShipTruckconcrete ProductShipconcrete Product
Logistics (the Creator) owns planDelivery() and calls the abstract createTransport(). It depends only on the Transport interface. Each concrete Creator overrides createTransport() to return its own Product. Adding air freight adds one box on each row — the base class stays frozen.

The uses arrow from Logistics to Transport is the one that matters. Because it points at the interface, the concrete-Product column on the right can grow without end, and the planDelivery() logic above never learns a single one of their names.

The code

Three parts, in the order you’d write them: the Product contract, the abstract Creator that owns the shared logic and defers the new, and the concrete Creators that override only the factory method.

«interface» · ProductTransport
load(cargo: Cargo) → void
move() → void
«abstract» · CreatorLogistics
createTransport() → Transport «abstract»
planDelivery(cargo: Cargo) → Receipt
The contract, the abstract Creator, and its subclasses
java

Read it top to bottom and the discipline is visible. Transport is the entire vocabulary the Creator is allowed to know. Logistics.planDelivery() is written once and calls createTransport() — an abstract method it can’t itself answer. Each concrete Creator supplies exactly one line: which class to build. There is no switch, because there is nothing to branch on — the base class doesn’t know which subclass it is, and that ignorance is the feature.

Now the payoff at the call site. The only place that names a concrete Creator is the wiring line; everything downstream is blind to the transport.

Wiring it once, then staying blind
java

Note where the switch went: it didn’t vanish, it shrank and moved to the edge. Instead of a conditional in the middle of the workflow that runs on every delivery, there’s a one-time selection at the boundary that picks a subclass. The valuable logic in the center is now conditional-free.

When (and when not) to reach for it

In the wild

You’ve called Factory Methods for years without naming them.

The tradeoffs

Factory Method buys extensibility with a hierarchy. Name the cost honestly.

What you gain
  • Open/Closed: a new product is a new subclass, not an edit to the workflow
  • The creator depends only on the Product interface — decoupled and testable
  • No conditional in the shared logic; the choice moves to the boundary
  • Frameworks can ship the workflow and let you fill in the concrete type
What it costs
  • A parallel class hierarchy: one Creator subclass per product
  • More classes and indirection than a plain switch for a single product
  • You must already have (or want) a Creator subclass axis to hang it on
  • Overkill when a simple factory or a supplier/lambda would do

Factory Method vs Abstract Factory vs Simple Factory

This trio is where interviews go to separate the memorizers from the understanders. They rhyme; their intent is different.

The one-line tell: Factory Method makes one product via a subclass; Abstract Factory makes a whole family via an object you pass around. If you’re overriding a method to change one product’s class, it’s Factory Method. If you’re swapping an object to change a coordinated set of products, it’s Abstract Factory.

Interview corner

A cross-platform dialog framework renders a window, then news up a concrete button by platform — with the platform switch buried inside the shared render() loop. Same smell as the logistics method, different domain.

Refactor this
java

Check yourself

References

Books

  • Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software, the Factory Method chapter (and its comparison to Abstract Factory).

Official documentation

  • Java SE — java.util.Iterator and java.util.Collection.iterator().
  • Java SE — java.util.Calendar.getInstance().
Go Premium

Enjoyed this post?

Unlock every deep-dive on system design & distributed systems, and keep your reading streak alive.

View plans

Related Articles