Course contents

Abstract Factory — families of related objects

Sign in to run this lab and save your progress

Learn

Before you start

Abstract Factory is what Factory becomes when the objects you build MUST be used together as a family. The interview trap: students confuse it with Factory ("both build things, right?"), miss the family question, and pick the wrong pattern.

The one-question test

Ask: "If I mix two products from different factories, does the system break?"

  • Yes → Abstract Factory. The family rule is real.
  • No → Simple Factory (or a plain dict). The products are independent.

This one question cuts through 80% of the Factory vs Abstract Factory confusion. Memorize it.

Where Abstract Factory shows up

Three groups of examples that come up in real design problems:

1. Third-party APIs that ship as a family:

  • Payment gateways (Razorpay / Stripe / PayPal each ship Charger + Refunder + WebhookValidator, all using the same API key)
  • Cloud providers (AWS / GCP / Azure each ship Storage + Queue + Compute clients, all using the same credentials)
  • OAuth providers (Google / GitHub / Facebook each ship Authorizer + TokenExchanger + ProfileFetcher)

2. Saving-and-loading families:

  • Formats (JSON / XML / YAML each ship a Serializer + Deserializer + SchemaValidator that must match)
  • Database drivers (Postgres / MySQL / SQLite each ship a Connection + Cursor + TypeMapper)

3. UI / rendering families:

  • Themes (Dark theme's Button + Panel + Textbox must not mix with Light theme's)
  • Platforms (iOS / Android / Web each ship their own widget versions)

If a design problem has any of these shapes, Abstract Factory is likely the answer.

How it differs from plain Factory

Same instinct — hide the "which class do I build?" question. Different mechanic — a factory that builds MULTIPLE related products from one place.

Aspect Simple Factory Abstract Factory
What it hides Which class to build Which whole family of classes
Number of products 1 2+ (a family)
Family rule Not a concern Enforced by the factory instance
Shared state Global or per-product Per-family (on the factory instance)
Signal to use it "The class depends on runtime data" "These N objects must always come from the same source"

Pick based on the signal in the problem, not based on which pattern name you remember first.

The pattern-picking checklist

Building on lesson 1's checklist:

  1. What varies in the problem?
  2. Do the varying things come in families that must match? → Yes: Abstract Factory. No: keep going.
  3. Does building the object hide complex logic? → Factory (last lesson).
  4. Does building the object take many optional inputs? → Builder (next lesson).

Same problem statement, different observation → different pattern. This is what interviewers reward — the READING skill, not the RECITING skill.

Common mistakes

  1. Using Abstract Factory when there's no "must not mix" rule. If your Chargers and Refunders happen to belong to "Razorpay" but nothing breaks when you mix them (say, you're writing a test), you don't need Abstract Factory — just build them directly or use two Simple Factories.

  2. Not moving the credentials onto the factory. A common half-refactor: Abstract Factory shape, but each product still takes its own api_key in create_* calls. The caller ends up passing the key around. Real Abstract Factory stores the shared state ON the factory so products get it silently.

A real-world callout

Django's database code is Abstract Factory in production. Each backend (postgresql, mysql, sqlite3) ships a DatabaseWrapper that produces SchemaEditorClass, Introspection, Operations, and more — all tied to one database connection. Same shape as our payment example, just bigger.

Your task

Last lesson: Factory hides the "which class do I build?" question for ONE type. Abstract Factory hides that question for a family of related types that MUST be used together.

The word "family" does all the work here. If mixing types from different families would break your system, you're looking at Abstract Factory.

The problem

Your app supports two payment gateways: Razorpay and Stripe. Each gateway needs three objects that work together:

  • Charger — charges a card
  • Refunder — refunds a transaction
  • WebhookValidator — checks signatures on incoming webhook events

The rule that makes this an Abstract Factory problem:

NEVER mix a Razorpay Charger with a Stripe WebhookValidator. The charge gives you a Razorpay transaction ID; the webhook validator only recognizes Stripe signatures. Mixing them breaks everything.

Naive first attempt — pass the gateway name around:

def checkout(gateway, amount, card):
    if gateway == "razorpay":
        txn = RazorpayCharger().charge(amount, card)
    elif gateway == "stripe":
        txn = StripeCharger().charge(amount, card)
    # ... later, in a different file:
    if gateway == "razorpay":
        RazorpayRefunder().refund(txn)
    elif gateway == "stripe":
        StripeRefunder().refund(txn)
    # ... and in the webhook handler:
    if gateway == "razorpay":
        RazorpayWebhookValidator().verify(event)
    elif gateway == "stripe":
        StripeWebhookValidator().verify(event)

Three copies of the same if/elif in three different files. And the invariant "always use the same family" is enforced by convention — nothing stops a bug from mixing them.

Questions to ask about the requirements

Questions to ask before picking a design:

  1. How many families exist today? Two? Ten?
  2. Will more families be added? Adding PayPal → do we edit every place, or add one class?
  3. Do the members of a family share state (auth token, API key)? If yes, building them one at a time copies that state everywhere.
  4. Is there any place in the system that DOES need to mix across families? (Usually no — that's the whole point.)
  5. How is the family chosen — user config, tenant setting, A/B test? Usually from a single "which gateway are we on today" decision.

If the answers are: 2+ families, more coming, shared state, never-mix, chosen once → this is the Abstract Factory shape.

Different designs, compared

Design A — pass gateway name + if/elif everywhere

The naive version above.

  • Good: Works. No new classes.
  • Bad: Breaks OCP three times over. The "always use the same family" rule is only a convention — one wrong call quietly mixes families. Shared state (API key) gets rebuilt in every place.
  • When it wins: Never for this problem. Only for one-off scripts.

Design B — one big Factory class PER PRODUCT

Three separate factories: ChargerFactory, RefunderFactory, WebhookValidatorFactory. Each is a normal registry-backed Simple Factory (like lesson 1).

  • Good: Familiar pattern, each factory is small.
  • Bad: Doesn't enforce the family rule. A caller can still ask ChargerFactory.create("razorpay") + RefunderFactory.create("stripe"). Three call sites still need to agree on the gateway name. Doesn't help with shared state.
  • When it wins: When the products are truly independent — no "must use together" rule. Not our problem.

Design C — one big "gateway god class"

One RazorpayGateway class with charge(), refund(), verify_webhook() methods. Same for Stripe.

  • Good: Family rule is automatic — one object owns all three operations.
  • Bad: Breaks SRP. As the gateway API grows (recurring charges, disputes, payouts, per-event webhooks), the class becomes a monster. Hard to test one operation on its own. Can't reuse a lone Charger somewhere else.
  • When it wins: When the gateway has 3-5 operations total AND you'll never test them alone AND you'll never grow it. Common as a first draft. Usually gets refactored later.

Design D — Abstract Factory

An abstract PaymentGatewayFactory with three abstract create methods:

class PaymentGatewayFactory(ABC):
    @abstractmethod
    def create_charger(self) -> Charger: ...
    @abstractmethod
    def create_refunder(self) -> Refunder: ...
    @abstractmethod
    def create_webhook_validator(self) -> WebhookValidator: ...

class RazorpayFactory(PaymentGatewayFactory):
    def create_charger(self): return RazorpayCharger(self._api_key)
    def create_refunder(self): return RazorpayRefunder(self._api_key)
    def create_webhook_validator(self):
        return RazorpayWebhookValidator(self._webhook_secret)

class StripeFactory(PaymentGatewayFactory):
    # ... same shape

Callers hold ONE PaymentGatewayFactory (picked once at startup) and ask it for whichever family member they need. Mixing families becomes impossible — a RazorpayFactory can only produce Razorpay-family objects.

  • Good: The family rule is enforced by the type system, not by convention. Shared state (API key) lives on the factory instance, and every product it builds inherits it. Adding a new gateway (PayPal) means one new factory class — zero changes to callers.
  • Bad: More classes to write upfront. Overkill when you'll only ever have one gateway (then you don't need any of this).
  • When it wins: Multiple families, "must not mix" rule, shared state per family. Our problem exactly.

The locked-in choice: Design D

Our assumed answers:

  • Multiple families, more coming (PayPal is on the roadmap). → Rules out A (edit every place) and B (three separate registries still don't stop family mixing).
  • The "no mixing" rule is real — mixing produces broken signatures. → Rules out A and B clearly.
  • Shared state (API key, webhook secret) is per family. → Rules out A and B which would copy the state everywhere.
  • Operations will grow. → Rules out C (god-class problem).
  • We want to test one product on its own (e.g. just the Charger). → Rules out C again.

Design D is the only choice that survives every requirement.

Mechanics for Design D

from abc import ABC, abstractmethod


class Charger(ABC):
    @abstractmethod
    def charge(self, amount, card): ...

class Refunder(ABC):
    @abstractmethod
    def refund(self, transaction_id): ...

class WebhookValidator(ABC):
    @abstractmethod
    def verify(self, signature, body): ...


class PaymentGatewayFactory(ABC):
    @abstractmethod
    def create_charger(self) -> Charger: ...
    @abstractmethod
    def create_refunder(self) -> Refunder: ...
    @abstractmethod
    def create_webhook_validator(self) -> WebhookValidator: ...


# Razorpay family
class RazorpayCharger(Charger):
    def __init__(self, api_key):
        self.api_key = api_key
    def charge(self, amount, card):
        return f"razorpay:charged {amount} on {card}"

# ... other Razorpay products ...

class RazorpayFactory(PaymentGatewayFactory):
    def __init__(self, api_key, webhook_secret):
        self._api_key = api_key
        self._webhook_secret = webhook_secret
    def create_charger(self):
        return RazorpayCharger(self._api_key)
    # ... etc

Your task

Build the whole system in main.py:

  1. Three product ABCs: Charger, Refunder, WebhookValidator with abstract methods:
    • Charger.charge(amount, card) → returns str
    • Refunder.refund(transaction_id) → returns str
    • WebhookValidator.verify(signature, body) → returns bool
  2. Abstract PaymentGatewayFactory with three abstract create_* methods.
  3. Razorpay familyRazorpayCharger, RazorpayRefunder, RazorpayWebhookValidator, RazorpayFactory. Each product's methods return:
    • Charger: f"razorpay:charged {amount} on {card} [key={key}]"
    • Refunder: f"razorpay:refunded {tx} [key={key}]"
    • WebhookValidator: verifies via signature == f"rzp:{body}:{secret}", returns True/False. Products take API key / secret in __init__; factory stores them and passes them in.
  4. Stripe family — same pattern with "stripe:..." prefix and signature format f"stp:{body}:{secret}".

Constraints (structural tests enforce):

  • No client code (only the factory) constructs Chargers / Refunders / WebhookValidators directly. Callers get them through the factory.
  • Adding a PayPal factory must work with zero changes to callers or to the abstract factory.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
02-abstract-factory-starter