Factory method
A factory method moves the question "which class do I create?" out of the code that uses the object and into a method that subclasses override. This page builds Pedal's docks that way, adds scooters without editing a line of the old code, and compares it with the simple factory most people use day to day.
1. The pain
Pedal started with one kind of vehicle. Then came e-bikes, which need a battery reading from the dock's charger, and next spring the city wants scooters. The code that releases a vehicle, the code that moves vehicles between stations and the code that books repairs each grew the same if kind == … chain, because each of them needs to create the right object.
Every new kind now means finding and editing every one of those chains, and forgetting one is a bug that only shows up when a scooter reaches that path. The creation decision is copied around, when it should live in one place.
2. The idea in one picture
Factory method puts object creation behind a method that subclasses override. The base class runs the fixed steps and calls make_vehicle() where it needs a new object; it never names a concrete class. Each subclass answers that one question in its own way, often using its own data (the charging dock passes its charger reading to the e-bike).
A smaller cousin, the simple factory, is just a function that picks the class from data: "this database row says e-bike, so make an EBike". It isn't one of the 23 book patterns, but it's what most people mean by "a factory", and section 5 shows both side by side.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Product | The interface every created object shares; callers use only this. | Vehicle: describe(), pence_per_minute() |
| Concrete product | One real class behind the interface. | Bike, EBike, Scooter |
| Creator | Runs the shared steps and declares the factory method. | Dock with release() and make_vehicle() |
| Concrete creator | Overrides the factory method to return one kind of product. | ClassicDock, ChargingDock, ScooterBay |
4. Before and after
The same release step written both ways (Python sketch; the full program in three languages is in section 5).
Without the pattern
def release(dock, vid):
if dock.kind == "classic":
v = Bike(vid)
elif dock.kind == "charging":
v = EBike(vid, dock.charger)
else:
raise ValueError(dock.kind)
start_ride(v)
return v
# rebalance() and book_repair()
# repeat the same if/elif chainWith it
class Dock(ABC):
def release(self, vid):
v = self.make_vehicle(vid)
start_ride(v)
return v
@abstractmethod
def make_vehicle(self, vid): ...
class ChargingDock(Dock):
def make_vehicle(self, vid):
return EBike(vid, self.charger)| The next change | Without the pattern | With it |
|---|---|---|
| Add scooters | Edit release, rebalance and book_repair: three chains | Add ScooterBay; no existing class changes |
| E-bikes need a battery reading | Find every EBike( call and pass the reading | One place: ChargingDock.make_vehicle |
| Test release() without hardware | Patch the module or fake a dock kind string | A FakeDock subclass returns a test vehicle |
The pattern doesn't remove the decision; it moves it to the one class that has the information to make it.
5. The code
Three dock classes override make_vehicle(), so release() never names a vehicle class. Below them, a simple factory turns stored records into vehicles and rejects an unknown kind.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable
# ---- The products: what a dock hands to a rider. Callers only ever see Vehicle. ----
class Vehicle(ABC):
def __init__(self, vid: str) -> None:
self.vid = vid
@abstractmethod
def describe(self) -> str: ...
@abstractmethod
def pence_per_minute(self) -> int: ...
class Bike(Vehicle):
def describe(self) -> str:
return f"bike {self.vid} (pedal only)"
def pence_per_minute(self) -> int:
return 10
class EBike(Vehicle):
def __init__(self, vid: str, battery: int) -> None:
super().__init__(vid)
self.battery = battery
def describe(self) -> str:
return f"e-bike {self.vid} (battery {self.battery}%)"
def pence_per_minute(self) -> int:
return 15
class Scooter(Vehicle):
def __init__(self, vid: str, max_kmh: int) -> None:
super().__init__(vid)
self.max_kmh = max_kmh
def describe(self) -> str:
return f"scooter {self.vid} (max {self.max_kmh} km/h)"
def pence_per_minute(self) -> int:
return 20
# ---- Factory method: release() is the same for every dock; each kind of dock decides what it creates. ----
class Dock(ABC):
def __init__(self, label: str, name: str) -> None:
self.label = label
self.name = name
def release(self, vid: str) -> str:
vehicle = self.make_vehicle(vid) # the factory method: no class named here
return f"{self.label} {self.name} released {vehicle.describe()} | {vehicle.pence_per_minute()}p a minute"
@abstractmethod
def make_vehicle(self, vid: str) -> Vehicle: ...
class ClassicDock(Dock):
def __init__(self, name: str) -> None:
super().__init__("classic dock", name)
def make_vehicle(self, vid: str) -> Vehicle:
return Bike(vid)
class ChargingDock(Dock):
def __init__(self, name: str, charger_reading: int) -> None:
super().__init__("charging dock", name)
self.charger_reading = charger_reading
def make_vehicle(self, vid: str) -> Vehicle:
return EBike(vid, self.charger_reading) # uses the dock's own state
class ScooterBay(Dock): # added later: no existing class was edited
def __init__(self, name: str, speed_limit: int) -> None:
super().__init__("scooter bay", name)
self.speed_limit = speed_limit
def make_vehicle(self, vid: str) -> Vehicle:
return Scooter(vid, self.speed_limit)
# ---- A simple factory: one function that turns a stored record into the right object. ----
MAKERS: dict[str, Callable[[str], Vehicle]] = {
"bike": lambda vid: Bike(vid),
"e-bike": lambda vid: EBike(vid, 100),
"scooter": lambda vid: Scooter(vid, 15),
}
def vehicle_from_record(kind: str, vid: str) -> Vehicle:
if kind not in MAKERS:
raise ValueError(f"unknown vehicle kind '{kind}'")
return MAKERS[kind](vid)
if __name__ == "__main__":
docks: list[Dock] = [ClassicDock("D1"), ChargingDock("D7", 84), ScooterBay("S2", 15)]
for dock, vid in zip(docks, ["B-101", "E-205", "K-310"]):
print(dock.release(vid))
records = [("bike", "B-101"), ("e-bike", "E-205"), ("scooter", "K-310")]
print("loaded from records: " + ", ".join(vehicle_from_record(k, v).describe() for k, v in records))
try:
vehicle_from_record("hoverboard", "H-1")
except ValueError as e:
print(f"rejected: {e}")
// ---- The products: what a dock hands to a rider. Callers only ever see Vehicle. ----
abstract class Vehicle {
constructor(readonly vid: string) {}
abstract describe(): string;
abstract pencePerMinute(): number;
}
class Bike extends Vehicle {
describe(): string { return `bike ${this.vid} (pedal only)`; }
pencePerMinute(): number { return 10; }
}
class EBike extends Vehicle {
constructor(vid: string, readonly battery: number) { super(vid); }
describe(): string { return `e-bike ${this.vid} (battery ${this.battery}%)`; }
pencePerMinute(): number { return 15; }
}
class Scooter extends Vehicle {
constructor(vid: string, readonly maxKmh: number) { super(vid); }
describe(): string { return `scooter ${this.vid} (max ${this.maxKmh} km/h)`; }
pencePerMinute(): number { return 20; }
}
// ---- Factory method: release() is the same for every dock; each kind of dock decides what it creates. ----
abstract class Dock {
constructor(readonly label: string, readonly name: string) {}
release(vid: string): string {
const vehicle = this.makeVehicle(vid); // the factory method: no class named here
return `${this.label} ${this.name} released ${vehicle.describe()} | ${vehicle.pencePerMinute()}p a minute`;
}
protected abstract makeVehicle(vid: string): Vehicle;
}
class ClassicDock extends Dock {
constructor(name: string) { super("classic dock", name); }
protected makeVehicle(vid: string): Vehicle { return new Bike(vid); }
}
class ChargingDock extends Dock {
constructor(name: string, private readonly chargerReading: number) { super("charging dock", name); }
protected makeVehicle(vid: string): Vehicle { return new EBike(vid, this.chargerReading); } // uses the dock's own state
}
class ScooterBay extends Dock { // added later: no existing class was edited
constructor(name: string, private readonly speedLimit: number) { super("scooter bay", name); }
protected makeVehicle(vid: string): Vehicle { return new Scooter(vid, this.speedLimit); }
}
// ---- A simple factory: one function that turns a stored record into the right object. ----
const MAKERS: Record<string, (vid: string) => Vehicle> = {
"bike": (vid) => new Bike(vid),
"e-bike": (vid) => new EBike(vid, 100),
"scooter": (vid) => new Scooter(vid, 15),
};
function vehicleFromRecord(kind: string, vid: string): Vehicle {
const make = MAKERS[kind];
if (!make) throw new Error(`unknown vehicle kind '${kind}'`);
return make(vid);
}
const docks: Dock[] = [new ClassicDock("D1"), new ChargingDock("D7", 84), new ScooterBay("S2", 15)];
const vids = ["B-101", "E-205", "K-310"];
docks.forEach((dock, i) => console.log(dock.release(vids[i])));
const records: [string, string][] = [["bike", "B-101"], ["e-bike", "E-205"], ["scooter", "K-310"]];
console.log("loaded from records: " + records.map(([k, v]) => vehicleFromRecord(k, v).describe()).join(", "));
try {
vehicleFromRecord("hoverboard", "H-1");
} catch (e) {
console.log(`rejected: ${(e as Error).message}`);
}import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class Main {
// ---- The products: what a dock hands to a rider. Callers only ever see Vehicle. ----
abstract static class Vehicle {
final String vid;
Vehicle(String vid) { this.vid = vid; }
abstract String describe();
abstract int pencePerMinute();
}
static class Bike extends Vehicle {
Bike(String vid) { super(vid); }
String describe() { return "bike " + vid + " (pedal only)"; }
int pencePerMinute() { return 10; }
}
static class EBike extends Vehicle {
final int battery;
EBike(String vid, int battery) { super(vid); this.battery = battery; }
String describe() { return "e-bike " + vid + " (battery " + battery + "%)"; }
int pencePerMinute() { return 15; }
}
static class Scooter extends Vehicle {
final int maxKmh;
Scooter(String vid, int maxKmh) { super(vid); this.maxKmh = maxKmh; }
String describe() { return "scooter " + vid + " (max " + maxKmh + " km/h)"; }
int pencePerMinute() { return 20; }
}
// ---- Factory method: release() is the same for every dock; each kind of dock decides what it creates. ----
abstract static class Dock {
final String label, name;
Dock(String label, String name) { this.label = label; this.name = name; }
String release(String vid) {
Vehicle vehicle = makeVehicle(vid); // the factory method: no class named here
return label + " " + name + " released " + vehicle.describe() + " | " + vehicle.pencePerMinute() + "p a minute";
}
protected abstract Vehicle makeVehicle(String vid);
}
static class ClassicDock extends Dock {
ClassicDock(String name) { super("classic dock", name); }
protected Vehicle makeVehicle(String vid) { return new Bike(vid); }
}
static class ChargingDock extends Dock {
private final int chargerReading;
ChargingDock(String name, int chargerReading) { super("charging dock", name); this.chargerReading = chargerReading; }
protected Vehicle makeVehicle(String vid) { return new EBike(vid, chargerReading); } // uses the dock's own state
}
static class ScooterBay extends Dock { // added later: no existing class was edited
private final int speedLimit;
ScooterBay(String name, int speedLimit) { super("scooter bay", name); this.speedLimit = speedLimit; }
protected Vehicle makeVehicle(String vid) { return new Scooter(vid, speedLimit); }
}
// ---- A simple factory: one function that turns a stored record into the right object. ----
static final Map<String, Function<String, Vehicle>> MAKERS = Map.of(
"bike", Bike::new,
"e-bike", vid -> new EBike(vid, 100),
"scooter", vid -> new Scooter(vid, 15));
static Vehicle vehicleFromRecord(String kind, String vid) {
Function<String, Vehicle> make = MAKERS.get(kind);
if (make == null) throw new IllegalArgumentException("unknown vehicle kind '" + kind + "'");
return make.apply(vid);
}
public static void main(String[] args) {
List<Dock> docks = List.of(new ClassicDock("D1"), new ChargingDock("D7", 84), new ScooterBay("S2", 15));
String[] vids = {"B-101", "E-205", "K-310"};
for (int i = 0; i < docks.size(); i++) System.out.println(docks.get(i).release(vids[i]));
String[][] records = {{"bike", "B-101"}, {"e-bike", "E-205"}, {"scooter", "K-310"}};
List<String> loaded = new ArrayList<>();
for (String[] r : records) loaded.add(vehicleFromRecord(r[0], r[1]).describe());
System.out.println("loaded from records: " + String.join(", ", loaded));
try {
vehicleFromRecord("hoverboard", "H-1");
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}Output (the same in all three languages)
classic dock D1 released bike B-101 (pedal only) | 10p a minute charging dock D7 released e-bike E-205 (battery 84%) | 15p a minute scooter bay S2 released scooter K-310 (max 15 km/h) | 20p a minute loaded from records: bike B-101 (pedal only), e-bike E-205 (battery 100%), scooter K-310 (max 15 km/h) rejected: unknown vehicle kind 'hoverboard'
6. Try it
Add a vehicle kind
Pedal keeps adding vehicles. Add them one at a time and watch what each design asks you to edit.
Simplified: three functions create vehicles in the "without" design (release, rebalance, book repair); each needs one more branch per kind.
7. When not to use it
What you get
- New kinds without edits: add a creator subclass; the shared steps stay closed.
- Creation in one place: the class that has the data makes the object.
- Callers depend on the interface:
release()only knowsVehicle. - Easy fakes: a test subclass returns whatever the test needs.
What you pay
- One more class per kind: creators grow alongside products.
- Indirection: a reader must find which subclass ran.
- Inheritance for a small choice: subclassing just to pick a class is heavy when nothing else varies.
- Upfront cost: wasted if a second kind never comes.
| If… | Use instead |
|---|---|
| There is one kind, and no second is planned | Call the constructor |
| The choice depends on data, not on who creates | A simple factory: a function or a dict of makers (section 5) |
| You need a matching set of objects per city | 02 Abstract factory |
| Creation has many optional settings | 03 Builder |
| The creator does nothing else but create | Pass a maker function in: 24 Dependency injection |
8. How it goes wrong
make_vehicle() is typed to return EBike, callers start using e-bike-only methods and the decoupling is gone. Return the interface.9. Remember it and say it
How this hook works: what “Same door, different vehicle” means
What the slogan means, phrase by phrase
- “Same door”
- The base class's fixed steps (
release()). - “different vehicle”
- The product each subclass creates.
- “each dock decides”
- The overridden factory method.
Why this picture fits: Riders never ask for a class; they walk up to a dock and get what it holds.
The story → the concept
| The dock | The code |
|---|---|
| The release procedure | Dock.release() |
| What the dock holds | make_vehicle() in each subclass |
| A new scooter bay | A new subclass; old docks untouched |
| The rider | Code that only knows Vehicle |
Watch it on an example
- The rider unlocks at charging dock D7.
release()runs the shared steps and callsmake_vehicle().ChargingDockcreates an e-bike with its charger reading;release()sees only aVehicle.
The trap it saves you from
Copying the same if kind == chain into every function that creates objects.
It still works when…
Yes, even without inheritance: pass the creating function in, and the same idea holds.
In an interview or code review
- Name the decision and where it lives: "Which vehicle to create belongs to the dock, so
make_vehicle()is a factory method onDock." - Say what doesn't change: "Adding scooters adds a
ScooterBayclass;release()and the existing docks aren't edited." That is the open-closed principle at work. - Know the difference from a simple factory: a factory method is overridden by subclasses; a simple factory is one function choosing from data. Use the second when no subclass exists to own the choice.
- Point to one you've used:
iterator()on every Java collection is a factory method; each collection returns its own iterator class.
10. Check yourself
What does a factory method let a subclass decide?
Pedal adds scooters. With factory method, what changes?
A database row says kind = "e-bike". What fits best?
Why should make_vehicle() return Vehicle rather than EBike?
When is a factory method overkill?
11. Go deeper
| Resource | Why |
|---|---|
| Low-level design 04: Patterns you'll actually use | The short interview tour; this track goes deeper on each. |
| Low-level design 03b: Open-closed principle | The principle this pattern serves: add code, don't edit it. |
Java Collection.iterator() | A factory method in the standard library: each collection makes its own iterator. |
| Wikipedia: Factory method pattern | History, variants and language-by-language notes. |