InterviewKata
Design patterns · 02 of 26 · Making objects

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

A furniture shop sells rooms as collections: the oak set, the walnut set. You pick one collection and everything you get matches: table, chairs, shelves. You never pick an oak table and walnut chairs by accident, because you never choose the pieces one by one.

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

RoleIts jobIn Pedal
Abstract factoryDeclares one creation method per kind of part.CityKit: make_lock(), make_terminal(), make_display()
Concrete factoryCreates every part of one family.LondonKit, ParisKit
Abstract productThe interface for one kind of part.Lock, Terminal, Display
Concrete productOne family's version of a part.BoltLock, MagneticLock, ChipPinPad…
ClientUses 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 changeWithout the patternWith it
Open in BerlinAdd a branch to every part's if, in every function that builds stationsAdd BerlinKit and its three parts
London swaps its sign supplierFind each place London's sign is chosenChange one line in LondonKit.make_display
Stop mixed-family stationsReview every branch by hand; tests per combinationImpossible 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 varies01 Factory method or a simple factory
The parts vary independently and any mix is fineChoose each part separately (plain dependency injection)
You need one complex object, not a family03 Builder
Families differ only in configuration valuesOne set of classes plus a config per city

8. How it goes wrong

Families that don't need to matchIf any lock works with any terminal, the factory forbids nothing useful. Inject each part on its own.
Adding a part type oftenEvery new kind of part changes the abstract factory and every concrete one. If parts come and go, the pattern fights you.
Passing the city string around anywayIf clients still check city == "paris" next to the kit, the family decision has leaked back out.
A kit that holds stateA factory that caches or counts parts quietly becomes a service locator. Keep kits as plain creators.

9. Remember it and say it

Memory hook: "Buy the set, not the pieces"One kit makes every part, so the parts always match.
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 shopThe code
A collection (oak, walnut)LondonKit, ParisKit
Table, chairs, shelvesLock, terminal, display
Choosing a collectionPassing a kit to install_station
A new collectionBerlinKit

Watch it on an example

  1. install_station gets ParisKit as a CityKit.
  2. It asks for a lock, a terminal and a display.
  3. 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

10. Check yourself

What does an abstract factory guarantee?

One kit makes every part, so they can't be mixed.

Pedal opens in Berlin. What changes?

New families are additive.

Stations now need a battery pack too. What changes?

Adding a kind of part is the pattern's expensive direction.

How does abstract factory relate to factory method?

Factory method varies one product; abstract factory varies a set together.

When is an abstract factory the wrong tool?

Without a matching rule, inject each part on its own.

11. Go deeper

ResourceWhy
01 Factory methodThe building block: one product per method.
Architecture styles 05: MicrokernelPer-city variation at the scale of plugins rather than classes.
Java DocumentBuilderFactoryA standard-library factory chosen at runtime, returning a matching parser family.
Wikipedia: Abstract factory patternStructure, history and variants.