Thuta Learning
ProjectsProgrammingintermediate

Project: Design a Notification System

What you'll walk away with

  • Explain the core ideas behind Project: Design a Notification System
  • Study the sample diagram/code and analyze its trade-offs
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

A naive notification system has the code that detects 'course completed' directly call send_email(), then send_push(), then write_in_app_notification() in sequence. This works at first, but it couples the event source to every consumer: adding SMS notifications later means editing that same function again, and a bug in the push-notification code can block or crash the email send that follows it. The publish-subscribe pattern fixes this by inserting a layer of indirection: the event source only publishes a 'course_completed' event with a payload, and has no idea who — if anyone — is listening. Any number of independent subscriber handlers register interest in that event type ahead of time; when the event fires, each one runs on its own, unaware of the others. This is the same pattern used by production message queues (Kafka topics, RabbitMQ exchanges, SQS+SNS fan-out) at larger scale, where subscribers can even run as separate worker processes or services. The trade-off this design accepts is indirection and, in a distributed version, eventual consistency and at-least-once delivery concerns — but it wins back total decoupling between the thing that happens and the things that react to it.

Connect it to a real scenario

This is close to what the Tutorial Platform needs the moment it adds social features: a learner finishing a tutorial should notify that learner (congratulations email) and their followers (activity feed entry), without the course-completion-tracking code needing to know anything about email templates or feed formatting. Wiring completion detection straight to send_email() the way a naive version would means every future notification channel — a push notification, a Slack webhook for study groups, an in-app toast — requires re-editing and re-testing the core completion logic. With pub-sub, adding a channel is just registering a new subscriber; the publisher, and everything already subscribed, never changes.

Try the working example

python
from collections import defaultdict


class PubSub:
    def __init__(self):
        self._subscribers = defaultdict(list)  # event_type -> list of handlers

    def subscribe(self, event_type: str, handler):
        self._subscribers[event_type].append(handler)

    def publish(self, event_type: str, payload: dict):
        for handler in self._subscribers[event_type]:
            handler(payload)


def send_email(payload: dict):
    print(f"[email] Sending congratulations to {payload['user']} for finishing '{payload['course']}'")


def log_notification(payload: dict):
    print(f"[log] {payload['user']} completed '{payload['course']}' at event time")


def notify_followers(payload: dict):
    for follower in payload.get("followers", []):
        print(f"[activity-feed] Telling {follower}: {payload['user']} just finished '{payload['course']}'")


if __name__ == "__main__":
    bus = PubSub()
    bus.subscribe("course_completed", send_email)
    bus.subscribe("course_completed", log_notification)
    bus.subscribe("course_completed", notify_followers)

    bus.publish("course_completed", {
        "user": "thiha",
        "course": "System Design",
        "followers": ["mya", "kaung"],
    })
You should see
Publishing one 'course_completed' event triggers all three independently-registered handlers in order: an email line, a log line, and one activity-feed line per follower.

5-minute try-it

Add an `unsubscribe(event_type, handler)` method to `PubSub`, and add a fourth handler that only runs for courses containing the word 'System' to demonstrate handlers can apply their own filtering logic independently of the publisher.

One important caution

Letting one handler's exception stop the loop in publish(): if send_email() raises and it's called before log_notification(), the crash prevents every handler registered after it from ever running for that event.

Coupling the publisher to specific handler names or return values (e.g. checking what send_email() returned): that reintroduces the same tight coupling pub-sub was meant to remove, and breaks the moment a new, unrelated handler is added.

Wikipedia — Publish–subscribe patternSystem Design

Easy traps

  • Letting one handler's exception stop the loop in publish(): if send_email() raises and it's called before log_notification(), the crash prevents every handler registered after it from ever running for that event.
  • Coupling the publisher to specific handler names or return values (e.g. checking what send_email() returned): that reintroduces the same tight coupling pub-sub was meant to remove, and breaks the moment a new, unrelated handler is added.
  • Validate your load/traffic assumptions before applying a design decision directly to a production system.

Exercise

Add an `unsubscribe(event_type, handler)` method to `PubSub`, and add a fourth handler that only runs for courses containing the word 'System' to demonstrate handlers can apply their own filtering logic independently of the publisher.

You'll know it worked when: Publishing one 'course_completed' event triggers all three independently-registered handlers in order: an email line, a log line, and one activity-feed line per follower.

Project: Design a Notification System | Thuta Learning