Design PatternsAdvanced 12 min Lesson 3 of 13

Abstract Factory — Families That Match

Create groups of related objects that must be used together, so a mismatched combination becomes impossible.

Design Patterns · Lesson 3 of 13
0/13 done(0%)

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 #

PYTHON
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.
TEXT
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.

Quick quiz

  1. 1. What distinguishes an abstract factory from a plain factory?

  2. 2. What guarantee does it provide?

  3. 3. What is the main cost of this pattern?

  4. 4. Which is a good real use?

  5. 5. What simpler alternative should you consider first?

Summary

  • An abstract factory produces a matched family of related objects.
  • It makes incompatible combinations structurally impossible.
  • Choose the family once, high up, then use it throughout.
  • The cost is rigidity when new product types are added.
  • Use it only where mismatched sets would cause real damage.