1. The Problem
Problem: Creating Related Objects That Must Work Together
Many systems need to create families of related objects that are designed to be used together.
Example:
- UI components for different platforms (Windows / Mac)
- Database clients (MySQL / PostgreSQL)
- Cloud resources (AWS / GCP)
Naive approach:
if platform == "windows":
button = WindowsButton()
checkbox = WindowsCheckbox()
elif platform == "mac":
button = MacButton()
checkbox = MacCheckbox()
Problems:
- Tight coupling to concrete classes
- Scattered conditionals across the codebase
- Easy to mix incompatible objects
- Violates Open/Closed Principle
We need a way to create consistent object families without hard-coding concrete types.
2. The Abstract Factory Pattern: Encapsulate Object Families
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Core idea:
- Group related object creation behind a factory interface
- Each concrete factory produces a consistent family
- Client code depends only on abstractions
Structure:
- Abstract Factory — declares creation methods for each product
- Concrete Factories — implement creation for a specific family
- Abstract Products — interfaces for products
- Concrete Products — implementations belonging to a family
What you get:
- Guaranteed compatibility between objects
- Zero conditionals in client code
- Easy swapping of entire product families
- Strong architectural boundaries
Use Abstract Factory when:
- Objects come in families and must be used together
- You want to swap entire ecosystems at runtime/config
- You want to enforce consistency across related objects
- You want to isolate creation logic completely
3. Implementation: Abstract Factory in Python
Abstract products
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str:
pass
Concrete products
class WindowsButton(Button):
def render(self) -> str:
return "Windows Button"
class MacButton(Button):
def render(self) -> str:
return "Mac Button"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "Windows Checkbox"
class MacCheckbox(Checkbox):
def render(self) -> str:
return "Mac Checkbox"
Abstract factory
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
Concrete factories
class WindowsUIFactory(UIFactory):
def create_button(self) -> Button:
return WindowsButton()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
class MacUIFactory(UIFactory):
def create_button(self) -> Button:
return MacButton()
def create_checkbox(self) -> Checkbox:
return MacCheckbox()
Client code
class Application:
def __init__(self, factory: UIFactory):
self.button = factory.create_button()
self.checkbox = factory.create_checkbox()
def render(self):
return [self.button.render(), self.checkbox.render()]
Runtime family swapping
app = Application(WindowsUIFactory())
app = Application(MacUIFactory())
Client code never changes. Only the factory does.
4. Abstract Factory vs Factory Method vs Strategy
Key distinction:
- Factory Method: creates one product
- Abstract Factory: creates multiple related products
- Strategy: varies behavior, not object families
Mental shortcut:
- “I need to choose an algorithm” → Strategy
- “I need to choose an implementation” → Factory Method
- “I need to choose a whole ecosystem” → Abstract Factory
5. When to Use Abstract Factory (and When Not To)
Use it when:
- You see repeated creation of related objects
- Mixing implementations would cause bugs
- You want to switch platforms/providers cleanly
- You want compile-time or design-time consistency
Avoid it when:
- You only need one product
- Object families are unlikely to grow
- A simple function or dict lookup is sufficient
Common pitfalls:
- Overengineering small systems
- Bloated factory interfaces
- Adding new product types frequently (harder than adding families)