Skip to content
BytePatterns

Factory Pattern Explained: One Place Decides What to Build

7 min readBytePatterns

The factory design pattern explained with Python: a registry mapping keys to classes, self-registering products, alternative constructors, and when to skip it.

Every object has to be built somewhere. The factory pattern is a decision about where: instead of each caller naming a concrete class, callers ask one function for "a van" or "an email sender", and that function decides which class to instantiate. It sounds like ceremony. In low-level design interviews it is often the difference between a design that takes a new vehicle type, payment method or notification channel with one added class, and one that needs edits in a dozen places.

The problem it solves

Construction logic tends to leak. The first caller writes Van(). The second needs to pick a class from user input and writes an if/elif chain. By the fifth, the same chain exists in three files, one of them has forgotten the compact, and adding a hybrid means finding all of them.

A factory fixes three things at once:

  • One place knows the mapping from a key to a class. Adding or retiring a product changes that place only.
  • Callers depend on a shared shape, the attributes and methods every product has, not on concrete classes.
  • Bad input fails in one place, with one clear message, instead of wherever the chain happened to fall through.

The intuition

In Python, classes are values. A dictionary from keys to classes is a factory: look up the key, call what you found. There is no need for a CarFactory class with a create method unless it holds state; a function and a module-level registry do the job.

Three variants carry the name, and it helps to keep them apart:

  • Simple factory: a function that takes a key and returns an instance. The registry version below is this.
  • Factory method: a base class calls an overridable method to create something, and each subclass decides which class that is. The framework owns the workflow; subclasses own the choice.
  • Abstract factory: one object that creates a whole family of related products, such as a light theme's buttons and dialogs, so the products always match.

A decorator makes the registry self-maintaining: each product registers itself where it is defined, so adding a product is literally adding a class. Alternative constructors, classmethods like from_text or from_json, are small factories as well: they hide parsing and choice behind a name.

Watch it run

The animation is the lesson's car hire desk. It opens on the problem: scattered Van() calls hard-wire every caller to a concrete class. A factory takes a key and hands back an object with the shape you asked for, and the registry is the only place that knows which key means which class. You book a class of car, estate, van or compact, never a model. The key is looked up and one row matches. That class is built, and only that class is named anywhere. You get seats and a boot back without ever touching a constructor. An unknown key fails here, with a clear message, instead of anywhere. Then the fleet changes: the depot retires the compact and one registry line leaves; a hybrid arrives, one new class and one new registry entry. The change stops at the seam, and every caller is exactly as it was.

Factory Pattern

Step 1 of 11

Scattered Van() calls hard-wire every caller to a concrete class.

The same interactive animation as the lesson — step through it with the controls.

The code

A registry filled by a decorator, and a hire function that is the only caller-facing way to get a car:

FLEET = {}                               # key -> class: the only place that knows

def fleet(key):                          # decorator: a class registers itself
    def register(cls):
        FLEET[key] = cls
        return cls
    return register

@fleet("estate")
class Estate:
    seats, boot = 5, 550

@fleet("van")
class Van:
    seats, boot = 3, 3000

@fleet("compact")
class Compact:
    seats, boot = 4, 300

def hire(car_class):                     # callers name a class of car, never a constructor
    try:
        return FLEET[car_class]()
    except KeyError:
        raise ValueError("no such class: " + car_class) from None

car = hire("van")
print(type(car).__name__, car.seats, car.boot)      # Van 3 3000
try:
    hire("tank")
except ValueError as err:
    print(err)                                      # no such class: tank

Adding a product touches nothing that already exists:

@fleet("hybrid")                         # a new product: one class, zero caller edits
class Hybrid:
    seats, boot = 5, 420

print(type(hire("hybrid")).__name__, sorted(FLEET))
# Hybrid ['compact', 'estate', 'hybrid', 'van']

An alternative constructor is the same idea at a smaller scale: parsing and choosing live behind one name, and it reuses the factory rather than repeating the mapping:

class Booking:
    def __init__(self, car, days):
        self.car, self.days = car, days

    @classmethod
    def from_text(cls, text):            # an alternative constructor is a factory too
        kind, days = (part.strip() for part in text.split(","))
        return cls(hire(kind), int(days.split()[0]))

b = Booking.from_text("estate, 3 days")
print(type(b.car).__name__, b.days)                 # Estate 3

A refactor must not change behaviour. The old if/elif version is kept as a reference and both are called with 5,000 seeded random keys, including unknown, empty, wrongly capitalised and padded ones; the class, the attributes and the error message must all match:

import random

def hire_old(car_class):                 # before: the choice was spelled out here
    if car_class == "estate":
        return Estate()
    elif car_class == "van":
        return Van()
    elif car_class == "compact":
        return Compact()
    raise ValueError("no such class: " + car_class)

def outcome(make, key):
    try:
        car = make(key)
        return type(car).__name__, car.seats, car.boot
    except ValueError as err:
        return "ValueError", str(err)

random.seed(28)
keys = ["estate", "van", "compact", "tank", "", "Van", "estate "]
ok = all(outcome(hire_old, k) == outcome(hire, k)
         for k in (random.choice(keys) for _ in range(5_000)))
print(ok)                                           # True

The complexity

  • Lookup: one dictionary access, O(1) on average, against an if chain that is O(p) in the number of products.
  • Adding a product: one class and one registration, and no caller changes. That is the open/closed principle from SOLID in practice.
  • Cost: one level of indirection. Reading hire("van") no longer tells you which constructor runs; you have to look at the registry.

Where it goes wrong

  • A factory for one product. If there is one class and no realistic second one, Van() is clearer.
  • Registration by import side effect. Self-registering classes only register if their module is imported. A product in a module nobody imports silently does not exist.
  • Returning products with different shapes. The point is that callers rely on a shared interface. If the caller checks isinstance afterwards, the factory has not hidden anything.
  • Confusing it with strategy. A factory decides which object to build; strategy decides which behaviour to run. They often meet: a factory returns the strategy.
  • Swallowing unknown keys. Returning None or a default moves the failure somewhere far from its cause.

When it shows up in interviews

In low-level design rounds, whenever the prompt contains types: vehicle and spot sizes in a parking lot, channels in a notification system, pieces in a chess model, payment methods at checkout. Interviewers look for construction in one place and callers that only see the interface. A common follow-up is "add a new type": the good answer is one new class and one registry entry. It pairs naturally with the observer pattern when the created objects also need to be notified.

How to say it in an interview

"I'll keep construction in one place. A registry maps each key to its class, and a small factory function looks up the key and returns an instance; unknown keys raise a clear error right there. Callers only depend on the shared interface. New types register themselves with a decorator, so adding one is a new class and nothing else, which keeps the design open for extension and closed for modification. If there were only one type, I would skip the factory."