Design Patterns Series #3 – Creational Patterns
The feature request seemed simple: “We’re adding drone delivery next quarter.” You opened the order processing class, found seventeen places with new Truck() scattered across the logic, and realized that “adding” drones actually means hunting down every constructor call, wrapping it in a condition, and hoping you didn’t miss one.
This is the problem Factory Method solves. Not the complexity of creating an object, but the fact that the decision of which object to create is buried inside business logic that shouldn’t care about it at all.
What Is the Factory Method Pattern?
Factory Method is a creational design pattern that defines an interface for creating objects, but lets subclasses decide which class to instantiate. The calling code works with the abstract product and it never sees the concrete type.
Four components make up the pattern:
- Product interface – defines what every created object can do.
- Concrete products – the actual implementations of the interface.
- Creator (abstract) – declares the factory method that returns a Product. May contain shared business logic that uses the product.
- Concrete creators – each one overrides the factory method to return a specific product type.
The key: the creator’s business logic calls the factory method and never calls new ConcreteProduct() directly. Subclasses handle that detail.
The Problem in Code
A logistics platform handles deliveries. Initially only trucks were supported. Over time, the codebase grew around that assumption:
// OrderService.java - before Factory Method
public class OrderService {
public void processOrder(Order order) {
// Hardcoded. Every method that needs transport does this.
Truck truck = new Truck();
truck.assignOrder(order);
truck.dispatch();
log.info("Order {} dispatched by truck.", order.getId());
}
public void estimateDelivery(Order order) {
Truck truck = new Truck(); // again
int days = truck.estimateDays(order.getDestination());
order.setEstimatedDelivery(days);
}
}
Drone delivery arrives. You now need to find every new Truck(), wrap it in an if-else based on order weight or destination, and do the same when ships get added next year. The transport decision is tangled into business logic that should have nothing to do with it.
The Solution, Step by Step
1. Define the product interface
Every transport type must fulfill the same contract. The business logic will only ever talk to this interface.
// Transport.java
public interface Transport {
void assignOrder(Order order);
void dispatch();
int estimateDays(String destination);
}
2. Implement concrete products
Each transport type gets its own class with its own delivery logic. They share nothing except the interface.
// TruckTransport.java
public class TruckTransport implements Transport {
@Override
public void assignOrder(Order order) {
log.info("Truck assigned to order {}.", order.getId());
}
@Override
public void dispatch() {
log.info("Truck dispatched via road network.");
}
@Override
public int estimateDays(String destination) {
return 3;
}
}
// DroneTransport.java
public class DroneTransport implements Transport {
@Override
public void assignOrder(Order order) {
log.info("Drone assigned to order {}.", order.getId());
}
@Override
public void dispatch() {
log.info("Drone launched. ETA calculated by GPS.");
}
@Override
public int estimateDays(String destination) {
return 1;
}
}
// ShipTransport.java
public class ShipTransport implements Transport {
@Override
public void assignOrder(Order order) {
log.info("Cargo ship assigned to order {}.", order.getId());
}
@Override
public void dispatch() {
log.info("Ship departed from port.");
}
@Override
public int estimateDays(String destination) {
return 14;
}
}
3. Define the abstract creator
The abstract creator declares the factory method. It also contains the shared business logic that uses the transport, and notice it never calls new on a concrete class.
// Logistics.java
public abstract class Logistics {
// The factory method - subclasses decide what to return
public abstract Transport createTransport();
// Shared business logic - works with any Transport
public void processOrder(Order order) {
Transport transport = createTransport();
transport.assignOrder(order);
transport.dispatch();
}
public void estimateDelivery(Order order) {
Transport transport = createTransport();
int days = transport.estimateDays(order.getDestination());
order.setEstimatedDelivery(days);
log.info("Estimated delivery: {} days.", days);
}
}
4. Implement concrete creators
Each concrete creator overrides the factory method and returns its specific product. That’s the only thing they need to do, as the rest is inherited.
// RoadLogistics.java
public class RoadLogistics extends Logistics {
@Override
public Transport createTransport() {
return new TruckTransport();
}
}
// DroneLogistics.java
public class DroneLogistics extends Logistics {
@Override
public Transport createTransport() {
return new DroneTransport();
}
}
// SeaLogistics.java
public class SeaLogistics extends Logistics {
@Override
public Transport createTransport() {
return new ShipTransport();
}
}
5. Wire it together
The calling code picks a creator based on business rules: order weight, destination, priority. After that, it never mentions a concrete type again.
// OrderController.java - one decision point, clean delegation
public class OrderController {
public void handleOrder(Order order) {
Logistics logistics = resolveLogistics(order);
logistics.processOrder(order);
logistics.estimateDelivery(order);
}
private Logistics resolveLogistics(Order order) {
if (order.getWeightKg() > 500) {
return new SeaLogistics(); // heavy freight -> ship
} else if (order.isUrgent() && order.getWeightKg() < 5) {
return new DroneLogistics(); // small + urgent -> drone
} else {
return new RoadLogistics(); // everything else -> truck
}
}
}
The selection logic lives in one place: resolveLogistics(). processOrder() and estimateDelivery() stay untouched regardless of how many transport types are added.
Why This Actually Works
Open/Closed Principle, again. Adding rail freight next year means writing RailTransport and RailLogistics, then adding one case to resolveLogistics(). The abstract Logistics class, processOrder(), estimateDelivery() – none of it changes.
Business logic is decoupled from object creation. The code that processes orders doesn’t know whether it’s talking to a truck or a drone. It calls dispatch() on a Transport and moves on. That’s the point: the what (deliver the order) is separated from the how (which vehicle does it).
Testability. You can test Logistics.processOrder() by injecting a mock Transport through a test subclass, with no real trucks, drones, or ships required. Each concrete transport is also testable in isolation.
When to Use It and When Not To
Reach for Factory Method when:
- Your code needs to create objects but shouldn’t depend on their concrete types.
- You anticipate adding new product types without touching existing logic.
- Object creation involves logic that belongs in one place, not scattered across the codebase.
Leave it on the shelf when:
- You only have one product type and no plans for more, so a constructor is fine.
- The subclassing hierarchy adds complexity without adding flexibility.
- A simple map of type to supplier lambda would do the job with less ceremony. If the only difference between your creators is the object they return, inheritance is overkill. That’s composition over inheritance in practice and it’s often the right call.
Factory Method shines in frameworks and libraries. Spring’s BeanFactory, JDBC’s DriverManager.getConnection() are good examples, where the framework defines the creation interface and you provide the concrete implementation. In application code, evaluate whether the abstraction earns its weight.
Wrapping Up
Factory Method is not about making object creation fancy. It’s about keeping the decision of what to create in one place, so the rest of your code can focus on what to do with it. When a new transport type arrives, you write two classes and touch one method. Nothing else breaks, because nothing else knew what it was working with.
That’s three patterns down: Strategy, Builder, Factory Method. Each one solves a different kind of problem. What algorithm to run, how to build an object, which object to create. Understanding where one ends and another begins is where the real knowledge starts.
Where have you seen Factory Method in the wild? Spring, JDBC, your own codebase, drop a comment below. I’m also curious whether you’ve ever reached for Factory Method and later simplified it back to a plain constructor. Sometimes the pattern is the answer. Sometimes it’s overkill. Either experience is worth sharing.