Open a UPI app — PhonePe, Google Pay, or the government's own BHIM, all riding on NPCI's switch — and look at any checkout screen. You can pay by UPI, by debit card, by credit card, or by net banking. The app doesn't know in advance which one you'll tap. Somewhere behind that button tap, a small piece of code has to decide, at the exact moment you choose, which concrete object to build and hand off to the rest of the payment flow — an object that knows how to talk to NPCI's switch if you chose UPI, or to a bank's card-auth network if you chose card. That decision — "build the right object without hard-coding which one" — is exactly the problem the Factory pattern exists to solve.
Now switch to a completely different screen: a cricket score app during an IPL over. Every legal delivery has to update a scoreboard widget, possibly fire a threshold SMS alert ("6 wickets down!"), and refresh a ball-by-ball commentary feed — all triggered from one place, the match state, without that match object needing to know the names of the three (or three hundred) things currently watching it. That is exactly the problem the Observer pattern exists to solve.
Both patterns come from the same source: the 1994 "Gang of Four" (GoF) book, which catalogued 23 recurring object-oriented solution shapes and grouped them into three families — creational patterns (how objects get built — Factory, Builder, Singleton), structural patterns (how objects get composed — Adapter, Decorator), and behavioral patterns (how objects communicate — Observer, Strategy). Factory is creational. Observer is behavioral. A design pattern is not a library you import — it is a reusable shape you re-implement, in whatever language, whenever the same structural problem recurs.
The problem Factory solves: creation glued into the client
Here is the checkout code most beginners write first — direct instantiation, hard-wired by an if/elif chain:
def checkout(method, amount):
if method == "upi":
processor = UPIProcessor()
elif method == "card":
processor = CardProcessor()
elif method == "netbanking":
processor = NetBankingProcessor()
else:
raise ValueError(f"Unknown payment method: {method}")
print(processor.pay(amount))
This works. It also has a specific, nameable defect: every time the product team adds a payment method (say, a new "BNPL" — Buy Now Pay Later — processor), you must reopen and edit checkout. In a real payment app this function is not written once — it is copy-pasted, in slightly different forms, into the checkout screen, the subscription-renewal screen, the refund screen, and the retry-after-failure screen. One new payment method now means four edits across four files, each a chance to typo a string or miss a branch. This is a violation of the Open/Closed Principle: code should be open to extension but closed to modification. The Factory pattern's entire job is to move the "which class do I build" decision into exactly one place, so the rest of the codebase only ever asks a factory for an object and never says ClassName() directly.
Simple Factory: centralizing the decision
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount):
pass
class UPIProcessor(PaymentProcessor):
def pay(self, amount):
return f"Paid Rs.{amount} via UPI (VPA debited, NPCI switch routed)"
class CardProcessor(PaymentProcessor):
def pay(self, amount):
return f"Paid Rs.{amount} via Card (bank auth code generated)"
class NetBankingProcessor(PaymentProcessor):
def pay(self, amount):
return f"Paid Rs.{amount} via NetBanking (redirected to bank portal)"
class PaymentProcessorFactory:
_registry = {
"upi": UPIProcessor,
"card": CardProcessor,
"netbanking": NetBankingProcessor,
}
@classmethod
def create(cls, method):
processor_class = cls._registry.get(method)
if processor_class is None:
raise ValueError(f"Unknown payment method: {method}")
return processor_class()
def checkout(method, amount):
processor = PaymentProcessorFactory.create(method)
print(processor.pay(amount))
checkout("upi", 499)
checkout("card", 12999)
Trace this line by line. checkout("upi", 499) calls PaymentProcessorFactory.create("upi"). Inside create, cls._registry.get("upi") looks up the key "upi" in the class-level dictionary and returns the class object UPIProcessor (not yet instantiated). Since it isn't None, the method returns processor_class() — an actual UPIProcessor instance. Back in checkout, processor.pay(499) runs the f-string substitution and returns the string "Paid Rs.499 via UPI (VPA debited, NPCI switch routed)", which gets printed. The second call follows the identical path through the "card" key. So the program prints exactly:
Paid Rs.499 via UPI (VPA debited, NPCI switch routed)
Paid Rs.12999 via Card (bank auth code generated)
Now if the product team adds BNPL, you write one new BNPLProcessor class and add one line to _registry. checkout — and every other screen that calls PaymentProcessorFactory.create — needs zero edits.
Factory Method: the real GoF pattern, and the misconception
Misconception: a great many students, having seen exactly the code above, conclude that "the Factory pattern is a function (or classmethod) that returns different objects based on a flag." Reality: that is a widely used idiom usually called the Simple Factory, and it is genuinely useful — but it is not what the GoF book calls the Factory Method pattern. The GoF definition is specifically about polymorphism: "define an interface for creating an object, but let subclasses decide which class to instantiate." The decision is not made by an if/elif or a dictionary lookup inside one function — it is made by overriding a method in a subclass. Watch the difference:
from abc import ABC, abstractmethod
class OrderProcessor(ABC):
@abstractmethod
def create_payment_processor(self):
pass
def process_order(self, amount):
processor = self.create_payment_processor()
return processor.pay(amount)
class UPIOrder(OrderProcessor):
def create_payment_processor(self):
return UPIProcessor()
class CardOrder(OrderProcessor):
def create_payment_processor(self):
return CardProcessor()
print(UPIOrder().process_order(499))
Trace it: UPIOrder() builds an instance whose create_payment_processor is the overridden version returning UPIProcessor(). Calling .process_order(499) runs the parent class's process_order, which calls self.create_payment_processor() — Python's method resolution order finds UPIOrder's override, not some default — getting a fresh UPIProcessor instance, then returns processor.pay(499), the same string as before: "Paid Rs.499 via UPI (VPA debited, NPCI switch routed)", now printed by the outer print.
Here is the structural payoff this buys you that the Simple Factory does not: to add NetBanking support, you write a new NetBankingOrder subclass overriding create_payment_processor — and you touch zero existing classes, not even a shared registry dictionary. The Simple Factory still requires one shared edit point (the _registry dict); the true Factory Method requires none, because "which class to build" is now delegated entirely to subclass polymorphism. In practice, engineering teams often accept the Simple Factory's one shared edit point because it's simpler to read — but you should be able to name which one you're using, and why, rather than calling both "the Factory pattern" interchangeably.
The Observer pattern: fan-out without polling
Consider the naive alternative to Observer: every consumer — the scoreboard widget, the SMS alerter, the commentary feed — runs its own loop that checks match.score_a and match.score_b every second, comparing against the last value it saw. This works, but it wastes cycles checking state that hasn't changed (most seconds between deliveries, nothing happens), and it can be laggy: if you poll once a second, a boundary can sit un-displayed for up to a second. The Observer pattern flips the direction of dependency: the object holding the state (the Subject) keeps a list of interested parties (Observers) and pushes a notification to all of them the instant its state changes — no consumer ever has to ask "did anything change yet?"
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, match):
pass
class Subject(ABC):
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
class Match(Subject):
def __init__(self, team_a, team_b):
super().__init__()
self.team_a = team_a
self.team_b = team_b
self.score_a = 0
self.score_b = 0
def update_score(self, team, runs):
if team == self.team_a:
self.score_a += runs
else:
self.score_b += runs
self.notify()
class ScoreboardApp(Observer):
def update(self, match):
print(f"[Scoreboard] {match.team_a} {match.score_a} - {match.score_b} {match.team_b}")
class SMSAlert(Observer):
def __init__(self, threshold):
self.threshold = threshold
self.sent = False
def update(self, match):
total = match.score_a + match.score_b
if total >= self.threshold and not self.sent:
print(f"[SMS] Combined score crossed {self.threshold}!")
self.sent = True
match = Match("IND", "AUS")
scoreboard = ScoreboardApp()
sms = SMSAlert(threshold=10)
match.attach(scoreboard)
match.attach(sms)
match.update_score("IND", 6)
match.update_score("AUS", 4)
match.update_score("IND", 4)
Note the coupling direction: Match only ever calls observer.update(self) — it never imports or names ScoreboardApp or SMSAlert. It depends on the abstract Observer interface only. That is the whole pattern in one sentence: a one-to-many dependency where the "one" knows nothing about the concrete types of the "many."
Worked trace: every state change, every notification
Step through the six lines at the bottom exactly as Python executes them:
match = Match("IND", "AUS")→score_a = 0,score_b = 0,_observers = [].match.attach(scoreboard)→_observers = [scoreboard].match.attach(sms)→_observers = [scoreboard, sms](attachment order matters — notify() will visit them in this order).match.update_score("IND", 6):"IND" == self.team_ais true, soscore_a = 0 + 6 = 6. Thennotify()runs: firstscoreboard.update(match)prints[Scoreboard] IND 6 - 0 AUS; thensms.update(match)computestotal = 6 + 0 = 6, which is not>= 10, so it does nothing.match.update_score("AUS", 4):"AUS" != self.team_a, so theelsebranch runs:score_b = 0 + 4 = 4.notify():scoreboard.updateprints[Scoreboard] IND 6 - 4 AUS;sms.updatecomputestotal = 6 + 4 = 10, which is>= 10andsentis stillFalse, so it prints[SMS] Combined score crossed 10!and setssent = True.match.update_score("IND", 4):score_a = 6 + 4 = 10.notify():scoreboard.updateprints[Scoreboard] IND 10 - 4 AUS;sms.updatecomputestotal = 10 + 4 = 14 >= 10, butsentis alreadyTrue, so it silently does nothing — the alert fires exactly once, not on every subsequent call.
The complete printed output, in order, is:
[Scoreboard] IND 6 - 0 AUS
[Scoreboard] IND 6 - 4 AUS
[SMS] Combined score crossed 10!
[Scoreboard] IND 10 - 4 AUS
A useful sanity check: across 3 calls to update_score, with 2 observers attached, notify() triggers 3 × 2 = 6 total calls to some observer's update method — but only 4 lines got printed. The other 2 calls (both to sms.update, in steps 4 and 6) executed fully and did real work (computing total, checking the condition) without producing any visible side effect. This is the point: the Subject doesn't know or care what each Observer decides to do with a notification — that decision is entirely internal to the Observer.
Why push beats poll: an order-of-magnitude estimate
Take a concrete, if illustrative, comparison. A T20 innings bowls 20 overs of 6 legal deliveries each — 120 deliveries. Suppose 90 of those deliveries actually change match state (a run scored or a wicket) and the rest are true dot-balls with no state change relevant to our two observers. With Observer, total observer invocations across the innings are 90 × 2 = 180. Now suppose instead every client polled the match object once per second for the innings' real-time duration — a T20 innings typically runs close to 75 minutes, i.e. 75 × 60 = 4500 seconds. With 2 polling clients, total checks are 4500 × 2 = 9000, almost all of them against unchanged state. That's 9000 ÷ 180 = 50 times more work for the polling design to deliver the same information, and it still lags behind the true delivery-by-delivery timing that push notification gives for free. This is not a claim about any specific broadcaster's engineering — it is the general shape of the trade-off between event-driven push and fixed-interval poll, and it is why Observer (and its cousins — publish/subscribe, event buses, the DOM's own addEventListener/dispatchEvent, which is Observer built into every browser) dominates polling wherever events are rarer than the polling interval would need to be to catch them promptly.
A second misconception worth naming: Observer is not the same as pub/sub
Because both are about "one change, many reactions," students often use "Observer" and "publish/subscribe" interchangeably. They are related but structurally different. In the classic GoF Observer pattern shown above, the Subject holds direct references to its Observers and calls their methods synchronously and directly — Match literally has a Python list containing the ScoreboardApp and SMSAlert objects. In a publish/subscribe system, publishers and subscribers never hold references to each other at all; a broker or event bus sits between them, and a publisher doesn't even know whether zero or a thousand subscribers exist. Observer is tightly coupled through a shared interface (loose on type, but the Subject still holds live references); pub/sub is loosely coupled through a broker (neither side holds a reference to the other). Both are legitimate — but reach for a broker only when publishers and subscribers genuinely need to run in different processes or be added without either side changing code, since a broker adds real infrastructure (a queue, a topic registry, delivery guarantees to reason about) that a plain in-process observer list does not need.
Diagram: where the indirection actually lives
Where the two patterns meet
They compose naturally in real systems. Consider a notification-preferences feature: a user picks "SMS" or "push" as their alert channel, and a ObserverFactory.create(channel) call builds the right kind of Observer object (an SMSAlert or a PushAlert) at subscription time — that's Factory deciding what gets created. That object is then attach()-ed to the Match subject, which from then on only ever calls the abstract update() — that's Observer deciding how it later gets used, without the Subject ever needing to know which factory branch produced it. Python's own standard library shows a plain version of the Simple Factory idiom in logging.getLogger(name), which returns a cached or freshly built Logger instance based on the name string — client code never calls Logger() directly. And every addEventListener call in JavaScript is a live instance of the Observer pattern: the DOM element is the Subject, your callback function is the Observer, and dispatchEvent is notify().
Active recall
Attempt these before reading the answers below.
- Using the Simple Factory registry code above, what happens when you call
checkout("wallet", 100)? - In the Factory Method example (
OrderProcessor/UPIOrder/CardOrder), what exactly must you add to support a newNetBankingOrder, and what must you not need to touch? - In the cricket Observer example, if you had called
match.attach(sms)beforematch.attach(scoreboard)(reversed order), would the final score values differ? Would the order of printed lines differ? - Across the 3 calls to
update_scorewith 2 observers attached, how many total calls to some observer'supdate()method occur, and how many of those produce visible printed output? - Give a complexity-style argument for why the Observer design beats a fixed-interval polling design for the cricket scoreboard, using the 120-delivery T20 estimate from this chapter.
- True or False: "A Simple Factory function that returns different objects based on a string is the same thing as the GoF Factory Method pattern." Justify your answer.
Answers
- The registry lookup
cls._registry.get("wallet")returnsNonebecause"wallet"is not a key in_registry. Theif processor_class is Nonebranch then raisesValueError("Unknown payment method: wallet"), socheckoutnever reaches theprintline — the program terminates with that exception (unless caught upstream). - You add exactly one new class,
NetBankingOrder(OrderProcessor), overriding onlycreate_payment_processorto returnNetBankingProcessor(). You do not touchOrderProcessor,UPIOrder, orCardOrderat all — the base class'sprocess_ordermethod is inherited unchanged and works immediately because it callsself.create_payment_processor()polymorphically. This is the Open/Closed Principle in action. - The final score values would be identical —
update_scoremutatesscore_a/score_bbefore callingnotify(), and observers never write back to the match, so state is unaffected by attachment order. But the printed order would change, becausenotify()iterates_observersin attachment order: withsmsattached first, its check (which only prints once, when the combined total first reaches 10) would be evaluated beforescoreboard's print on that same call, so the[SMS]line would appear one line earlier in the output than it does in the original trace. - 3 calls × 2 observers = 6 total
update()invocations. Only 4 produce visible output: all 3 calls toscoreboard.updateprint, but only 1 of the 3 calls tosms.updateprints (the one where the threshold is first crossed) — the other 2 execute the condition check and do nothing externally visible. - With Observer, work scales with actual events: roughly 90 state-changing deliveries × 2 observers = 180 total update calls across the innings. With fixed-interval polling by 2 clients once per second over a ~4500-second innings, total checks are 4500 × 2 = 9000, nearly all against unchanged state. That's roughly 9000 ÷ 180 = 50 times more operations for the polling design to deliver the same information, with worse latency besides (poll interval delay vs. instant push).
- False. The Simple Factory idiom centralizes the creation decision in one function or method using conditionals or a lookup table — it is a useful idiom but not the formal GoF pattern. The Factory Method pattern specifically requires an abstract Creator class with a factory method that concrete Creator subclasses override via polymorphism to decide which Product class gets instantiated — no conditional dispatch table exists anywhere, and new creators are added purely by subclassing, touching no existing code.
Think About It
Think about this: How would you explain design patterns: factory and observer patterns to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind design patterns: factory and observer patterns, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.