Skip to content
FollowtheDamnCode

Abstract Factory

One object that creates a whole family of matching parts, so you can never mix pieces from two families by accident.

Big idea

An Abstract Factory is one object with a creation method per part of a family — create_storage(), create_queue(). Each concrete factory builds parts that belong together. Client code asks the factory it was given and can't accidentally combine parts from two families.

The problem it solves

A service runs on two environments: in the cloud it uses cloud blob storage and a managed queue; on a developer's laptop it uses a local folder and an in-memory queue. Code is sprinkled with:

store = CloudStorage() if env == "cloud" else LocalStorage()
queue = CloudQueue() if env == "cloud" else MemoryQueue()

Each pair of ifs is a chance to mismatch — one forgotten branch and the laptop build writes to production storage. Adding a third environment means finding every such pair.

Think of it like

Flat-pack furniture ranges in a home store. You pick a range — say, the oak range — and the bed, the wardrobe, and the bedside table all come in matching oak with matching handles. You don't pick each piece's style separately and hope they match; choosing the range picks them all.

Structure

Each factory creates a matching family

In code

Pick the family once; every part matches
class CloudStorage:
    def put(self, key, data): return f"cloud-blob://{key} ({len(data)} bytes)"
class CloudQueue:
    def push(self, msg): return f"cloud-queue <- {msg}"

class FolderStorage:
    def put(self, key, data): return f"./data/{key} ({len(data)} bytes)"
class MemoryQueue:
    def push(self, msg): return f"memory-queue <- {msg}"


class CloudFactory:
    def create_storage(self): return CloudStorage()
    def create_queue(self): return CloudQueue()

class LocalFactory:
    def create_storage(self): return FolderStorage()
    def create_queue(self): return MemoryQueue()


class UploadService:                         # only knows the factory interface
    def __init__(self, factory):
        self.storage = factory.create_storage()
        self.queue = factory.create_queue()

    def upload(self, key, data):
        return [self.storage.put(key, data), self.queue.push(f"uploaded {key}")]


FACTORIES = {"cloud": CloudFactory, "local": LocalFactory}   # chosen once, at startup
for env in ("local", "cloud"):
    for line in UploadService(FACTORIES[env]()).upload("cat.png", b"\x89PNG...."):
        print(f"{env:5} {line}")
output
local ./data/cat.png (8 bytes)
local memory-queue <- uploaded cat.png
cloud cloud-blob://cat.png (8 bytes)
cloud cloud-queue <- uploaded cat.png

Run and edit it in the app.

The environment is decided in one line at startup. UploadService can't end up with cloud storage and a memory queue, because it never chooses parts individually.

Watch it

How to implement it

  1. List the product kinds (storage, queue) and the families (cloud, local) — a grid.
  2. Give each product kind an interface.
  3. Declare an abstract factory with one create_x() per product kind.
  4. Write one concrete factory per family.
  5. Choose the concrete factory once — in configuration or main — and pass it in.

What's the main guarantee Abstract Factory gives that separate factory functions don't?

When to use it — and when not to

Use it when code must work with several families of related objects and mixing them would be a bug: UI toolkits per platform, infrastructure per environment, database drivers with matching connection/command/reader classes.

Skip it for one product kind (that's Factory Method or a plain function), or when families never actually exist — one implementation behind an abstract factory is pure ceremony.

Pros and cons

ProsCons
Families can't be mixedAdding a new product kind means editing every factory
Swapping families is a one-line changeMany interfaces and classes for the grid
Client code is free of concrete classesOverkill with only one family

The asymmetry matters: new families are cheap, new product kinds are expensive. If you expect many new kinds of part, this pattern fights you.

How it relates

  • Each create_x() is often a Factory Method.
  • Concrete factories are frequently single instances (Singleton) or chosen by configuration.
  • Builder assembles one complex object step by step; Abstract Factory returns several related objects immediately.
  • Bridge pairs well with it: the factory picks the implementation side of the bridge.

You need to add a Cache part to both the cloud and local families. What changes?

Key terms

  • Abstract factory — An interface with one creation method per product kind in a family.
  • Product family — A set of objects designed to be used together, such as all-cloud or all-local.
  • Concrete factory — An implementation that creates every product of one family.