Abstract factory
An abstract factory creates a whole family of related objects through one interface, so the objects always match. This page uses it to equip Pedal's stations in London and Paris, shows how a mismatched station happens without it, and where the pattern gets expensive.
1. The pain
Pedal is opening in Paris. A London station has a bolt lock and a contactless reader that both talk NFC, and an English e-ink sign. Paris buys different hardware: a magnetic lock and a chip-and-PIN pad that talk Bluetooth, and a French LCD sign. The parts only work with partners from the same family; a London lock can't hear a Paris terminal.
The setup code chooses each part with its own if city == …. Nothing ties the three choices together, so one wrong branch builds a station that installs fine and then never unlocks. With Berlin next year, every one of those branches grows again.
2. The idea in one picture
Abstract factory is an object whose job is to create a whole family of related objects. It has one creation method per part (make_lock(), make_terminal(), make_display()), and each concrete factory returns the parts of one family. Code that sets up a station receives a factory and calls its methods; it never names a part's class, so it can't mix families.
It's built from factory methods (page 01), one per product, grouped in one object. The difference is the guarantee: a single choice of factory decides every part at once.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Abstract factory | Declares one creation method per kind of part. | CityKit: make_lock(), make_terminal(), make_display() |
| Concrete factory | Creates every part of one family. | LondonKit, ParisKit |
| Abstract product | The interface for one kind of part. | Lock, Terminal, Display |
| Concrete product | One family's version of a part. | BoltLock, MagneticLock, ChipPinPad… |
| Client | Uses only the abstract factory and products. | install_station(name, kit) |
4. Before and after
The station setup both ways (Python sketch; the full program is in section 5).
Without the pattern
def install_station(name, city):
if city == "london":
lock = BoltLock()
else:
lock = MagneticLock()
if city == "paris":
terminal = ChipPinPad()
else:
terminal = ContactlessReader()
# ... and again for the sign
return Station(name, lock, terminal)With it
class CityKit(ABC):
def make_lock(self): ...
def make_terminal(self): ...
def make_display(self): ...
def install_station(name, kit):
return Station(name,
kit.make_lock(),
kit.make_terminal())
install_station("PS-03", ParisKit())| The next change | Without the pattern | With it |
|---|---|---|
| Open in Berlin | Add a branch to every part's if, in every function that builds stations | Add BerlinKit and its three parts |
| London swaps its sign supplier | Find each place London's sign is chosen | Change one line in LondonKit.make_display |
| Stop mixed-family stations | Review every branch by hand; tests per combination | Impossible by construction: one kit makes all parts |
The kit turns three separate choices into one, so they can't disagree.
5. The code
Two city kits make a lock, a terminal and a sign each. install_station() uses only the CityKit interface. The last line wires parts by hand to show the mismatch the kits prevent.
from __future__ import annotations
from abc import ABC, abstractmethod
# ---- One interface per part of a station. ----
class Lock(ABC):
@abstractmethod
def describe(self) -> str: ...
@abstractmethod
def radio(self) -> str: ...
class Terminal(ABC):
@abstractmethod
def describe(self) -> str: ...
@abstractmethod
def radio(self) -> str: ...
class Display(ABC):
@abstractmethod
def show(self, text: str) -> str: ...
# ---- London's family: everything talks NFC, signs are in English. ----
class BoltLock(Lock):
def describe(self) -> str: return "bolt lock"
def radio(self) -> str: return "nfc"
class ContactlessReader(Terminal):
def describe(self) -> str: return "contactless reader"
def radio(self) -> str: return "nfc"
class EInkSign(Display):
def show(self, text: str) -> str: return f"e-ink sign: '{text}'"
# ---- Paris's family: everything talks Bluetooth, signs are in French. ----
class MagneticLock(Lock):
def describe(self) -> str: return "magnetic lock"
def radio(self) -> str: return "ble"
class ChipPinPad(Terminal):
def describe(self) -> str: return "chip-and-PIN pad"
def radio(self) -> str: return "ble"
class LcdSign(Display):
def show(self, text: str) -> str: return f"LCD sign: '{text}'"
# ---- Abstract factory: one object that makes a whole matching family. ----
class CityKit(ABC):
@abstractmethod
def make_lock(self) -> Lock: ...
@abstractmethod
def make_terminal(self) -> Terminal: ...
@abstractmethod
def make_display(self) -> Display: ...
@abstractmethod
def welcome(self) -> str: ...
class LondonKit(CityKit):
def make_lock(self) -> Lock: return BoltLock()
def make_terminal(self) -> Terminal: return ContactlessReader()
def make_display(self) -> Display: return EInkSign()
def welcome(self) -> str: return "Welcome to Pedal"
class ParisKit(CityKit):
def make_lock(self) -> Lock: return MagneticLock()
def make_terminal(self) -> Terminal: return ChipPinPad()
def make_display(self) -> Display: return LcdSign()
def welcome(self) -> str: return "Bienvenue chez Pedal"
# ---- The client: builds a station from whichever kit it is given, naming no concrete class. ----
class Station:
def __init__(self, name: str, lock: Lock, terminal: Terminal, sign: str) -> None:
self.name, self.lock, self.terminal, self.sign = name, lock, terminal, sign
def check(self) -> str:
if self.lock.radio() != self.terminal.radio():
return f"mismatch: lock {self.lock.radio()}, terminal {self.terminal.radio()}"
return f"ok (all {self.lock.radio()})"
def install_station(name: str, kit: CityKit) -> Station:
display = kit.make_display()
return Station(name, kit.make_lock(), kit.make_terminal(), display.show(kit.welcome()))
if __name__ == "__main__":
for name, kit in [("LS-12", LondonKit()), ("PS-03", ParisKit())]:
s = install_station(name, kit)
print(f"{s.name}: {s.lock.describe()} + {s.terminal.describe()} + {s.sign} -> {s.check()}")
# Without a kit, nothing stops someone wiring a London lock to a Paris terminal.
hand_wired = Station("XX-01", BoltLock(), ChipPinPad(), "no sign")
print(f"{hand_wired.name}: {hand_wired.lock.describe()} + {hand_wired.terminal.describe()} -> {hand_wired.check()}")
// ---- One interface per part of a station. ----
interface Lock { describe(): string; radio(): string; }
interface Terminal { describe(): string; radio(): string; }
interface Display { show(text: string): string; }
// ---- London's family: everything talks NFC, signs are in English. ----
class BoltLock implements Lock { describe() { return "bolt lock"; } radio() { return "nfc"; } }
class ContactlessReader implements Terminal { describe() { return "contactless reader"; } radio() { return "nfc"; } }
class EInkSign implements Display { show(text: string) { return `e-ink sign: '${text}'`; } }
// ---- Paris's family: everything talks Bluetooth, signs are in French. ----
class MagneticLock implements Lock { describe() { return "magnetic lock"; } radio() { return "ble"; } }
class ChipPinPad implements Terminal { describe() { return "chip-and-PIN pad"; } radio() { return "ble"; } }
class LcdSign implements Display { show(text: string) { return `LCD sign: '${text}'`; } }
// ---- Abstract factory: one object that makes a whole matching family. ----
interface CityKit {
makeLock(): Lock;
makeTerminal(): Terminal;
makeDisplay(): Display;
welcome(): string;
}
class LondonKit implements CityKit {
makeLock(): Lock { return new BoltLock(); }
makeTerminal(): Terminal { return new ContactlessReader(); }
makeDisplay(): Display { return new EInkSign(); }
welcome() { return "Welcome to Pedal"; }
}
class ParisKit implements CityKit {
makeLock(): Lock { return new MagneticLock(); }
makeTerminal(): Terminal { return new ChipPinPad(); }
makeDisplay(): Display { return new LcdSign(); }
welcome() { return "Bienvenue chez Pedal"; }
}
// ---- The client: builds a station from whichever kit it is given, naming no concrete class. ----
class Station {
constructor(readonly name: string, readonly lock: Lock, readonly terminal: Terminal, readonly sign: string) {}
check(): string {
if (this.lock.radio() !== this.terminal.radio()) return `mismatch: lock ${this.lock.radio()}, terminal ${this.terminal.radio()}`;
return `ok (all ${this.lock.radio()})`;
}
}
function installStation(name: string, kit: CityKit): Station {
const display = kit.makeDisplay();
return new Station(name, kit.makeLock(), kit.makeTerminal(), display.show(kit.welcome()));
}
const cities: [string, CityKit][] = [["LS-12", new LondonKit()], ["PS-03", new ParisKit()]];
for (const [name, kit] of cities) {
const s = installStation(name, kit);
console.log(`${s.name}: ${s.lock.describe()} + ${s.terminal.describe()} + ${s.sign} -> ${s.check()}`);
}
// Without a kit, nothing stops someone wiring a London lock to a Paris terminal.
const handWired = new Station("XX-01", new BoltLock(), new ChipPinPad(), "no sign");
console.log(`${handWired.name}: ${handWired.lock.describe()} + ${handWired.terminal.describe()} -> ${handWired.check()}`);import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
// ---- One interface per part of a station. ----
interface Lock { String describe(); String radio(); }
interface Terminal { String describe(); String radio(); }
interface Display { String show(String text); }
// ---- London's family: everything talks NFC, signs are in English. ----
static class BoltLock implements Lock { public String describe() { return "bolt lock"; } public String radio() { return "nfc"; } }
static class ContactlessReader implements Terminal { public String describe() { return "contactless reader"; } public String radio() { return "nfc"; } }
static class EInkSign implements Display { public String show(String text) { return "e-ink sign: '" + text + "'"; } }
// ---- Paris's family: everything talks Bluetooth, signs are in French. ----
static class MagneticLock implements Lock { public String describe() { return "magnetic lock"; } public String radio() { return "ble"; } }
static class ChipPinPad implements Terminal { public String describe() { return "chip-and-PIN pad"; } public String radio() { return "ble"; } }
static class LcdSign implements Display { public String show(String text) { return "LCD sign: '" + text + "'"; } }
// ---- Abstract factory: one object that makes a whole matching family. ----
interface CityKit {
Lock makeLock();
Terminal makeTerminal();
Display makeDisplay();
String welcome();
}
static class LondonKit implements CityKit {
public Lock makeLock() { return new BoltLock(); }
public Terminal makeTerminal() { return new ContactlessReader(); }
public Display makeDisplay() { return new EInkSign(); }
public String welcome() { return "Welcome to Pedal"; }
}
static class ParisKit implements CityKit {
public Lock makeLock() { return new MagneticLock(); }
public Terminal makeTerminal() { return new ChipPinPad(); }
public Display makeDisplay() { return new LcdSign(); }
public String welcome() { return "Bienvenue chez Pedal"; }
}
// ---- The client: builds a station from whichever kit it is given, naming no concrete class. ----
record Station(String name, Lock lock, Terminal terminal, String sign) {
String check() {
if (!lock.radio().equals(terminal.radio())) return "mismatch: lock " + lock.radio() + ", terminal " + terminal.radio();
return "ok (all " + lock.radio() + ")";
}
}
static Station installStation(String name, CityKit kit) {
Display display = kit.makeDisplay();
return new Station(name, kit.makeLock(), kit.makeTerminal(), display.show(kit.welcome()));
}
public static void main(String[] args) {
Map<String, CityKit> cities = new LinkedHashMap<>();
cities.put("LS-12", new LondonKit());
cities.put("PS-03", new ParisKit());
for (Map.Entry<String, CityKit> e : cities.entrySet()) {
Station s = installStation(e.getKey(), e.getValue());
System.out.println(s.name() + ": " + s.lock().describe() + " + " + s.terminal().describe() + " + " + s.sign() + " -> " + s.check());
}
// Without a kit, nothing stops someone wiring a London lock to a Paris terminal.
Station handWired = new Station("XX-01", new BoltLock(), new ChipPinPad(), "no sign");
System.out.println(handWired.name() + ": " + handWired.lock().describe() + " + " + handWired.terminal().describe() + " -> " + handWired.check());
}
}Output (the same in all three languages)
LS-12: bolt lock + contactless reader + e-ink sign: 'Welcome to Pedal' -> ok (all nfc) PS-03: magnetic lock + chip-and-PIN pad + LCD sign: 'Bienvenue chez Pedal' -> ok (all ble) XX-01: bolt lock + chip-and-PIN pad -> mismatch: lock nfc, terminal ble
6. Try it
Assemble a station
Pick each part by hand, as the old setup code did, or switch to a city kit. Pedal now runs in three cities.
A station works only if all three parts come from one family: the lock and terminal share a radio, and the sign speaks the city's language.
7. When not to use it
What you get
- Families can't be mixed: one choice of kit decides every part.
- New families are additive: a new city is a new kit, no edits.
- Swapping is one line: pass a different kit to switch the whole family.
- Client code stays generic: it knows only interfaces.
What you pay
- Many classes: one factory per family plus one class per part per family.
- Adding a kind of part is expensive: a new
make_battery()changes the interface and every kit. - Overkill for one family: with one city, it's ceremony.
- Harder to read: the concrete classes are a hop away from the code using them.
| If… | Use instead |
|---|---|
| Only one kind of object varies | 01 Factory method or a simple factory |
| The parts vary independently and any mix is fine | Choose each part separately (plain dependency injection) |
| You need one complex object, not a family | 03 Builder |
| Families differ only in configuration values | One set of classes plus a config per city |
8. How it goes wrong
city == "paris" next to the kit, the family decision has leaked back out.9. Remember it and say it
How this hook works: what “Buy the set, not the pieces” means
What the slogan means, phrase by phrase
- “the set”
- A concrete factory:
LondonKit. - “not the pieces”
- The client never picks a part class itself.
- “always match”
- No London lock beside a Paris terminal.
Why this picture fits: Furniture collections keep a room consistent by removing the piece-by-piece choice.
The story → the concept
| The shop | The code |
|---|---|
| A collection (oak, walnut) | LondonKit, ParisKit |
| Table, chairs, shelves | Lock, terminal, display |
| Choosing a collection | Passing a kit to install_station |
| A new collection | BerlinKit |
Watch it on an example
install_stationgetsParisKitas aCityKit.- It asks for a lock, a terminal and a display.
- All three come back as the Paris family; the radio check passes.
The trap it saves you from
Choosing each part of a family with its own if, so the choices can disagree.
It still works when…
Yes, in UI toolkits (a light or dark set of widgets), database drivers (connection, command, reader) and cloud SDKs.
In an interview or code review
- Say what must match: "Locks and terminals only work within a city's hardware family, so a
CityKitmakes all three parts." - Contrast with factory method: factory method varies one product; abstract factory varies a family of products together, usually by grouping factory methods.
- Name the weak spot: adding a city is cheap, adding a kind of part is expensive (every kit changes). Say which one you expect to happen more.
- Real examples: Java's
DocumentBuilderFactoryand JDBC drivers, and themed widget sets in UI toolkits.
10. Check yourself
What does an abstract factory guarantee?
Pedal opens in Berlin. What changes?
Stations now need a battery pack too. What changes?
How does abstract factory relate to factory method?
When is an abstract factory the wrong tool?
11. Go deeper
| Resource | Why |
|---|---|
| 01 Factory method | The building block: one product per method. |
| Architecture styles 05: Microkernel | Per-city variation at the scale of plugins rather than classes. |
Java DocumentBuilderFactory | A standard-library factory chosen at runtime, returning a matching parser family. |
| Wikipedia: Abstract factory pattern | Structure, history and variants. |