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

Design Patterns: Factory, Singleton, Observer

📚 Software Engineering⏱️ 20 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 20 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open the source code of any real app — a quiz app, a chat app, a food-delivery app — and you will find the same three headaches, over and over, no matter who wrote it. Somewhere, several different files each contain their own copy of the logic that decides "which class do I create here?" Somewhere else, one particular object must exist exactly once for the whole program to behave correctly, and a stray second copy of it quietly causes bugs that are maddening to trace. And somewhere, one object changes, and five unrelated parts of the app all need to find out about it without being wired together into a tangled mess.

Design patterns are not new syntax, and they are not a new language feature. They are named, battle-tested solutions to problems like these — problems general enough that thousands of programmers before you have hit them and converged on similar fixes. Learning a pattern gives you two things: a working solution you don't have to reinvent, and a shared vocabulary — when you tell another programmer "I used a Factory here," they instantly understand the shape of your code without reading every line of it.

This chapter builds three of the most useful patterns — Factory, Singleton, and Observer — from a single running example: a quiz app, the kind of project you might actually build once you know how to write classes in Python. Each pattern starts from a genuine bug or mess that shows up in that app, then formalizes the fix.

Problem 1: the same decision, copied everywhere

Suppose your quiz app supports three question types: multiple-choice, fill-in-the-blank, and true/false. Each type is its own class, because each behaves differently when checking an answer:

class Question:
    def __init__(self, text):
        self.text = text
    def check_answer(self, given):
        raise NotImplementedError

class MCQQuestion(Question):
    def __init__(self, text, options, answer):
        super().__init__(text)
        self.options = options
        self.answer = answer
    def check_answer(self, given):
        return given == self.answer

class FillBlankQuestion(Question):
    def __init__(self, text, answer):
        super().__init__(text)
        self.answer = answer
    def check_answer(self, given):
        return given.strip().lower() == self.answer.lower()

class TrueFalseQuestion(Question):
    def __init__(self, text, answer):
        super().__init__(text)
        self.answer = answer
    def check_answer(self, given):
        return bool(given) == self.answer

Now your quiz bank is stored as data — a list of dictionaries loaded from a file, each with a "qtype" field. Three separate parts of your app need to turn that data into real question objects: the screen that loads the quiz, the screen that reviews wrong answers, and the code that emails a results summary. Written naively, each of those three places contains its own copy of the same decision:

# inside quiz_loader.py
if qtype == "mcq":
    q = MCQQuestion(text, options, answer)
elif qtype == "fillblank":
    q = FillBlankQuestion(text, answer)
elif qtype == "truefalse":
    q = TrueFalseQuestion(text, answer)

# the SAME if/elif, copied, inside review_screen.py
# the SAME if/elif, copied again, inside results_email.py

This works, but it is fragile in a specific way: the moment you add a fourth question type — say, MatchTheFollowing — you must remember to update the if/elif chain in all three files. Forget one, and that file silently mishandles the new type. This is not a hypothetical risk; "I added the feature in one place and forgot the other two" is one of the most common bug sources in student and professional projects alike, precisely because nothing forces you to remember.

The fix is to put the decision in exactly one place: a function whose only job is to look at the type and hand back the right object.

def create_question(qtype, text, **kwargs):
    if qtype == "mcq":
        return MCQQuestion(text, kwargs["options"], kwargs["answer"])
    elif qtype == "fillblank":
        return FillBlankQuestion(text, kwargs["answer"])
    elif qtype == "truefalse":
        return TrueFalseQuestion(text, kwargs["answer"])
    else:
        raise ValueError(f"Unknown question type: {qtype}")

Now quiz_loader.py, review_screen.py, and results_email.py each just call create_question(qtype, text, **fields) and never mention MCQQuestion or the other class names at all. Trace one call to be sure: create_question("mcq", "Capital of India?", options=["Mumbai", "Delhi", "Chennai"], answer="Delhi") enters the function, matches the first branch, and returns MCQQuestion("Capital of India?", ["Mumbai", "Delhi", "Chennai"], "Delhi") — a fully built, ready-to-use object. When MatchTheFollowing arrives later, you add one new elif inside create_question, and all three call sites automatically pick it up with zero changes to their own code.

This is the Factory pattern: a function (or, in bigger systems, a class) whose only responsibility is deciding which class to instantiate and handing back a ready object, so that the rest of the program never repeats that decision itself.

Common misconception: students often assume any function that returns an object is "using the Factory pattern," since every constructor already does that. It isn't. What defines a Factory is that it centralizes a decision — a branch on type — that would otherwise be duplicated at multiple call sites. If your app only ever creates one class, wrapping its constructor in a function like def make_mcq(text, options, answer): return MCQQuestion(text, options, answer) adds a layer of indirection without removing any duplicated decision — it is not the Factory pattern, just an unnecessary wrapper. The pattern earns its name only when it replaces a repeated if/elif that would otherwise live in more than one place.

Problem 2: an object that must exist exactly once

The same quiz app needs a running scoreboard — current score, questions answered so far — and several screens need to read and update it: the question screen (adds points on a correct answer), the results screen (displays the final total), and the review screen (shows which answers were wrong). Written naively, each screen creates its own scoreboard:

class ScoreBoard:
    def __init__(self):
        self.score = 0
        self.answered = 0

# question_screen.py
board = ScoreBoard()
board.score += 10

# results_screen.py, opened later
board = ScoreBoard()   # a BRAND NEW object, score = 0!
print(board.score)     # prints 0, not 10 — the real score is lost

The bug here is subtle precisely because the code runs without crashing: results_screen.py gets a perfectly valid ScoreBoard object, it's just a different object from the one question_screen.py updated. The fix is to guarantee that only one ScoreBoard can ever exist during a single quiz attempt, and to give every screen a way to fetch that one shared object instead of building a new one.

class ScoreBoard:
    _instance = None

    def __init__(self):
        if ScoreBoard._instance is not None:
            raise RuntimeError("Use ScoreBoard.get_instance(), not ScoreBoard()")
        self.score = 0
        self.answered = 0

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

Trace it carefully. First call, from question_screen.py: board1 = ScoreBoard.get_instance(). Inside get_instance, the class variable ScoreBoard._instance is still None, so the if is true: it runs cls(), which calls __init__; inside __init__, ScoreBoard._instance is still None at that exact moment (the assignment in get_instance hasn't happened yet), so the guard passes, score and answered are set to 0, and the new object is stored in cls._instance and returned. board1.score += 10 makes board1.score equal 10. Later, from results_screen.py: board2 = ScoreBoard.get_instance(). This time ScoreBoard._instance is not None — it already holds the object from the first call — so get_instance skips creating anything new and simply returns that same stored object. print(board2.score) prints 10, and board1 is board2 evaluates to True: they are literally the same object in memory, just reached from two different files.

This is the Singleton pattern: a class engineered so that at most one instance of it can exist for the life of the program, with a single access point — here, get_instance() — that every part of the code uses to reach that one shared object.

Common misconception: a Singleton is not just "a global variable." A plain module-level variable like board = ScoreBoard() sitting at the top of a file is created immediately the moment that file is imported, whether or not anything ever needs it, and nothing stops a second line elsewhere from writing board = ScoreBoard() again, silently replacing it. A true Singleton is lazy — it is built only the first time get_instance() is actually called — and it is self-enforcing: the guard inside __init__ actively raises an error if any code tries to sidestep get_instance() and construct a second object directly. It is worth adding a caution here too: Singletons should be reached for only when "exactly one, for the whole program" is a genuine requirement of the problem — one active quiz attempt, one shared print spooler, one configuration loaded from disk. Using it as a default habit for "a class I happen to only need once right now" creates hidden shared state that makes code harder to test, because every test now secretly depends on whatever the one shared object currently contains.

Problem 3: one change, many things that must react

The quiz app's scoreboard now needs to trigger several reactions whenever the score changes: the on-screen number must refresh, a "ding" sound should play, and a leaderboard widget should update. The naive way is to hardcode all three reactions directly inside ScoreBoard:

class ScoreBoard:
    def __init__(self):
        self.score = 0

    def update_score(self, points):
        self.score += points
        ui_display.refresh(self.score)        # ScoreBoard now needs
        sound_player.play_ding()               # to know about UIDisplay,
        leaderboard_widget.refresh(self.score) # SoundPlayer, Leaderboard...

This works until the app needs a fourth reaction — say, a haptic buzz on a phone. That means opening up ScoreBoard's own code again and adding another hardcoded call, even though buzzing a phone has nothing conceptually to do with tracking a score. ScoreBoard has become tightly tangled with every feature that happens to care about it.

The fix: ScoreBoard keeps a list of interested objects and calls one common method on all of them, without knowing or caring what each one actually does.

class ScoreBoard:
    def __init__(self):
        self.score = 0
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def update_score(self, points):
        self.score += points
        self._notify_all()

    def _notify_all(self):
        for observer in self._observers:
            observer.update(self.score)

class UIDisplay:
    def update(self, new_score):
        print(f"UI: showing score {new_score}")

class SoundPlayer:
    def update(self, new_score):
        print("Sound: playing ding!")

class LeaderboardWidget:
    def update(self, new_score):
        print(f"Leaderboard: refreshing with {new_score}")

Wiring it up and tracing it: board = ScoreBoard() starts with score = 0 and an empty _observers list. Three board.attach(...) calls append a UIDisplay(), a SoundPlayer(), and a LeaderboardWidget() to that list, in that order. Now board.update_score(4) runs: self.score becomes 0 + 4 = 4, then _notify_all() loops over the three observers in the order they were attached, calling update(4) on each — printing UI: showing score 4, then Sound: playing ding!, then Leaderboard: refreshing with 4. Later, a harder question is answered correctly and board.update_score(46) runs: self.score becomes 4 + 46 = 50, and the same loop fires again, this time printing UI: showing score 50, Sound: playing ding!, and Leaderboard: refreshing with 50.

This is the Observer pattern: a Subject (here, ScoreBoard) keeps a list of Observers and calls one shared method — by convention, update() — on every one of them whenever its own state changes, without the Subject's code ever needing to know what any individual observer does with that call.

The diagram below traces exactly this call: update_score(46) changes the score to 50, then notify_all() fans that single change out to three independent observers, each reacting in its own way to the identical update(50) call.

ScoreBoard (Subject) update_score(46) called score becomes 50 -> notify_all() update(50) update(50) update(50) UIDisplay shows "score 50" SoundPlayer plays a ding LeaderboardWidget refreshes rank list Every observer receives the identical update(50) call. ScoreBoard's code never names UIDisplay, SoundPlayer, or LeaderboardWidget directly.

Comparing the three patterns

  • Factory — problem solved: the same "which class do I build?" decision was duplicated across multiple files. Mechanism: one function centralizes the decision and returns a ready object. Memory hook: a Factory answers the question "which one do I make?"
  • Singleton — problem solved: an object that must exist exactly once was accidentally being created more than once, causing different parts of the program to hold inconsistent copies of the same data. Mechanism: the class guards its own constructor and hands out one shared instance through a single access method. Memory hook: a Singleton answers the question "is there already one of these?"
  • Observer — problem solved: one object's change needed to reach several unrelated parts of the program without hardwiring them together. Mechanism: a Subject keeps a list of Observers and calls one shared method on all of them when its state changes. Memory hook: Observer answers the question "who else needs to know?"

Notice that the three patterns are not competitors — they solve different problems and often appear together in the same piece of code. In fact the quiz app above already combines two of them naturally: ScoreBoard could be built as a Singleton (so every screen shares the same score) that is also a Subject in the Observer pattern (so every screen's display stays in sync automatically whenever that shared score changes). create_question stays a separate, unrelated Factory, because deciding which question class to build has nothing to do with how many scoreboards exist or who gets notified when the score changes.

Where this connects to what you're already learning

By this point in your Software Engineering coursework you already know how to write a class with an __init__ method, add methods to it, and create objects from it. These three patterns are the natural next step once you have several classes that need to work together in a real project rather than sitting alone in a single script. If you have ever submitted a school project and lost marks for "duplicated code" or "poor modularity," that criticism maps directly onto the problems this chapter solved: repeated if/elif blocks across files is exactly what the Factory pattern removes, and logic for one feature leaking into a class that shouldn't need to know about it is exactly what the Observer pattern untangles. When you next build a multi-file Python project — a quiz app, a simple inventory tracker, a library-book manager — try naming, on purpose, which of your classes is a Factory, which object (if any) truly needs to be a Singleton, and which relationships between your classes are really an Observer link in disguise. Recognizing the pattern before you write the code, rather than backing into a mess and fixing it afterward, is the actual skill this chapter is teaching — the three code examples are just the vehicle for practicing it.

Check your understanding

Work through each of these using the exact classes defined above; the point is to trace the code by hand, the same way you traced create_question, get_instance, and update_score in the worked examples.

  1. A new question type, NumericQuestion (answer must match within 0.01), is added to the quiz bank. List every place in the Factory-pattern version of the code that must change, and every place in the three-separate-if/elif version that must change. What does the difference tell you about why the Factory pattern reduces bugs, not just typing?
  2. Suppose a programmer writes board = ScoreBoard() directly instead of ScoreBoard.get_instance(), on a line that runs after get_instance() has already been called once elsewhere. Trace through __init__ line by line and state exactly what happens.
  3. In the Observer example, board.attach(SoundPlayer()) is called before board.attach(LeaderboardWidget()). Does swapping that order change what gets printed when update_score runs, and why or why not?
  4. A programmer adds a fourth class, HapticBuzzer, with an update(self, new_score) method, and calls board.attach(HapticBuzzer()). Which existing class's source code, if any, needs to be edited for this to work? Compare your answer to what the naive hardcoded version from Problem 3 would have required.
  5. A classmate says: "I wrote _score_instance = ScoreBoard() once at the top of my file, so I've used the Singleton pattern." Explain, using the two concrete differences named in this chapter, why this is not the same guarantee that the get_instance() version provides.

Summary

Factory, Singleton, and Observer are three separate answers to three separate recurring problems in object-oriented code. Factory centralizes the decision of which class to construct, so that decision is written once instead of duplicated at every call site. Singleton guarantees that a class produces at most one instance for the program's lifetime and forces every part of the code to reach that same instance through one controlled access point, rather than each part accidentally building its own. Observer lets one object (the Subject) broadcast a change to a list of other objects (the Observers) through a single shared method, without the Subject ever needing to know what those observers actually do — so new observers can be added later without touching the Subject's code at all. None of the three is "better" than the others; each exists because it fixes a specific, identifiable kind of mess, and recognizing which mess you're looking at is what tells you which pattern to reach for.

Think About It

Think about this: How would you explain design patterns: factory, singleton, observer 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.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where design patterns: factory, singleton, observer is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting design patterns: factory, singleton, observer to at least 3 other topics you have studied.
← Inheritance and Polymorphism Deep DivePython Dataclasses and Type Hints →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn