AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Design Patterns: Factory and Observer Patterns

📚 Programming & Coding⏱️ 21 min read🎓 Grade 11
✍️ AI Computer Institute Editorial Team Updated: September 2026 CBSE-aligned · Reviewed for accuracy · 21 min read
Mapped to the CBSE/NCERT syllabus and reviewed for accuracy in a separate pass. Spotted an error? Tell us on the contact page.

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:

  1. match = Match("IND", "AUS")score_a = 0, score_b = 0, _observers = [].
  2. match.attach(scoreboard)_observers = [scoreboard].
  3. match.attach(sms)_observers = [scoreboard, sms] (attachment order matters — notify() will visit them in this order).
  4. match.update_score("IND", 6): "IND" == self.team_a is true, so score_a = 0 + 6 = 6. Then notify() runs: first scoreboard.update(match) prints [Scoreboard] IND 6 - 0 AUS; then sms.update(match) computes total = 6 + 0 = 6, which is not >= 10, so it does nothing.
  5. match.update_score("AUS", 4): "AUS" != self.team_a, so the else branch runs: score_b = 0 + 4 = 4. notify(): scoreboard.update prints [Scoreboard] IND 6 - 4 AUS; sms.update computes total = 6 + 4 = 10, which is >= 10 and sent is still False, so it prints [SMS] Combined score crossed 10! and sets sent = True.
  6. match.update_score("IND", 4): score_a = 6 + 4 = 10. notify(): scoreboard.update prints [Scoreboard] IND 10 - 4 AUS; sms.update computes total = 10 + 4 = 14 >= 10, but sent is already True, 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

Two Structures, One Idea: Decoupling via Indirection FACTORY METHOD OBSERVER «abstract» OrderProcessor + create_payment_processor() + process_order(amount) extends extends UPIOrder create_payment_processor() CardOrder create_payment_processor() creates creates UPIProcessor pay(amount) CardProcessor pay(amount) implements implements «abstract» PaymentProcessor + pay(amount) Client code depends only on OrderProcessor + PaymentProcessor — never on concrete classes. Match (Subject) - _observers: list[Observer] + attach(obs) / notify() notify() notify() ScoreboardApp update(match) SMSAlert update(match) attach() adds CommentaryFeed (future) update(match) New observers register via attach(). Match never imports ScoreboardApp or SMSAlert.

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.

  1. Using the Simple Factory registry code above, what happens when you call checkout("wallet", 100)?
  2. In the Factory Method example (OrderProcessor / UPIOrder / CardOrder), what exactly must you add to support a new NetBankingOrder, and what must you not need to touch?
  3. In the cricket Observer example, if you had called match.attach(sms) before match.attach(scoreboard) (reversed order), would the final score values differ? Would the order of printed lines differ?
  4. Across the 3 calls to update_score with 2 observers attached, how many total calls to some observer's update() method occur, and how many of those produce visible printed output?
  5. 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.
  6. 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

  1. The registry lookup cls._registry.get("wallet") returns None because "wallet" is not a key in _registry. The if processor_class is None branch then raises ValueError("Unknown payment method: wallet"), so checkout never reaches the print line — the program terminates with that exception (unless caught upstream).
  2. You add exactly one new class, NetBankingOrder(OrderProcessor), overriding only create_payment_processor to return NetBankingProcessor(). You do not touch OrderProcessor, UPIOrder, or CardOrder at all — the base class's process_order method is inherited unchanged and works immediately because it calls self.create_payment_processor() polymorphically. This is the Open/Closed Principle in action.
  3. The final score values would be identical — update_score mutates score_a/score_b before calling notify(), and observers never write back to the match, so state is unaffected by attachment order. But the printed order would change, because notify() iterates _observers in attachment order: with sms attached first, its check (which only prints once, when the combined total first reaches 10) would be evaluated before scoreboard'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.
  4. 3 calls × 2 observers = 6 total update() invocations. Only 4 produce visible output: all 3 calls to scoreboard.update print, but only 1 of the 3 calls to sms.update prints (the one where the threshold is first crossed) — the other 2 execute the condition check and do nothing externally visible.
  5. 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).
  6. 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.

← Database Indexing: Optimize Query PerformanceAI Art Ethics: Ownership, Bias, and Indian Cultural Context →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn
Share