What is it? #
An Abstract Factory creates a family of related objects that must be used together.
The difference from a plain factory is the word family. A factory picks one implementation; an abstract factory picks a matched set — and guarantees you cannot mix members of different sets.
The problem it solves is inconsistency. If your reporting system uses a cloud storage client with a local file naming scheme, or a production database with a sandbox payment gateway, things break in confusing ways.
It is the heaviest of the creational patterns and is only worth it when mismatched combinations are a genuine hazard.
Think of it like this #
Buying a matching furniture set. Order "Scandinavian" and you get a chair, table and lamp that belong together. Order "industrial" and you get a different but equally coherent set.
What you cannot do is accidentally end up with a Scandinavian table and an industrial chair. The set is chosen once, at the top.
Simple example #
An application runs against three environments. Each needs a storage client, a payment gateway and a notifier — and using the production payment gateway with test storage would be a serious mistake.
Code #
from abc import ABC, abstractmethod
# --- Product interfaces ---
class Storage(ABC):
@abstractmethod
def save(self, key, data) -> str: ...
class Payments(ABC):
@abstractmethod
def charge(self, amount) -> str: ...
class Notifier(ABC):
@abstractmethod
def notify(self, to, message) -> None: ...
# --- Production family ---
class S3Storage(Storage):
def save(self, key, data): return f"s3://prod/{key}"
class LivePayments(Payments):
def charge(self, amount): return f"LIVE charge {amount}"
class SesNotifier(Notifier):
def notify(self, to, message): print(f"[ses] {to}: {message}")
# --- Local family ---
class DiskStorage(Storage):
def save(self, key, data): return f"/tmp/{key}"
class FakePayments(Payments):
def charge(self, amount): return f"fake charge {amount}"
class ConsoleNotifier(Notifier):
def notify(self, to, message): print(f"[console] {to}: {message}")
# --- The abstract factory ---
class Environment(ABC):
@abstractmethod
def storage(self) -> Storage: ...
@abstractmethod
def payments(self) -> Payments: ...
@abstractmethod
def notifier(self) -> Notifier: ...
class Production(Environment):
def storage(self): return S3Storage()
def payments(self): return LivePayments()
def notifier(self): return SesNotifier()
class Local(Environment):
def storage(self): return DiskStorage()
def payments(self): return FakePayments()
def notifier(self): return ConsoleNotifier()
def build_environment(name: str) -> Environment:
return {"production": Production, "local": Local}[name]()
# Application code receives a whole coherent family
env = build_environment("local")
storage, payments, notifier = env.storage(), env.payments(), env.notifier()
print(storage.save("invoice.pdf", b"")) # /tmp/invoice.pdf
print(payments.charge(999)) # fake charge 999
notifier.notify("[email protected]", "done")
# There is no way to end up with DiskStorage + LivePayments by accident.
When to use it
- several objects must come from the same coherent set
- mixing members of different sets causes real damage
- the set is chosen once, high up, and used throughout
When NOT to use it
- the objects are independent and can be chosen separately
- there is only one family
- adding a new product type to the family would force edits everywhere
How it works #
Three product interfaces define what the application needs: storage, payments, notification.
Each family provides one implementation of each. The production set talks to real services; the local set touches nothing outside the machine.
Environment is the abstract factory. It has one method per product type, and each concrete environment returns its own matched implementations.
build_environment selects the family once, typically at startup from a configuration value. After that, everything downstream receives a consistent set.
The guarantee is structural. Local simply has no way to return LivePayments, so the dangerous combination cannot occur — not by a code mistake, not by a bad configuration merge.
The main cost is rigidity. Adding a fourth product type — say a search client — means editing the Environment interface and every family that implements it. That is the trade-off for the guarantee, and it is why this pattern is reserved for cases where mismatches genuinely matter.
Real-world use #
The clearest real use is environment configuration, as above. Development, staging and production each get a coherent set of clients, and no test run can accidentally charge a real card.
Cross-platform toolkits use it for UI widget families, so a single application does not end up with mismatched controls.
Database tooling sometimes uses it: a family of connection, query builder and migration runner that all speak the same dialect.
The simpler alternative worth considering first is a configuration object plus a plain factory per product. It gives most of the benefit with less structure. Reach for the full pattern when the compile-time or construction-time guarantee is what you actually need.
Common mistakes #
- Using it when the products are genuinely independent, adding structure for nothing.
- Adding product types often, forcing edits to every family each time.
- Letting one family quietly reuse another family’s product, defeating the guarantee.
- Confusing it with a plain factory — this one produces a set, not one object.
- Building it for a single family that will never have a sibling.
Practice #
Build an abstract factory for a reporting system with two families: "internal" (plain formatter, filesystem writer, console logger) and "client-facing" (branded formatter, cloud writer, audit logger). Then explain in two sentences what would break if someone mixed a branded formatter with a console logger.