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:
- How many families exist today? Two? Ten?
- Will more families be added? Adding PayPal → do
we edit every place, or add one class?
- Do the members of a family share state (auth
token, API key)? If yes, building them one at a time
copies that state everywhere.
- Is there any place in the system that DOES need to
mix across families? (Usually no — that's the whole
point.)
- 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:
- 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
- Abstract
PaymentGatewayFactory with three abstract
create_* methods.
- Razorpay family —
RazorpayCharger, 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.
- 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.