Design Patterns, Explained Simply · Capstone Projects

Capstone: A Notification System with Undo

Design a notification system that reacts to events, sends through vendors with different APIs, and lets admins undo a broadcast, using Observer, Adapter and Command.

Scenario

Example company: a project-management app needs notifications. When a task is assigned, the assignee should get an email and a chat message. Admins can broadcast announcements to all users, and they want an undo button for broadcasts sent by mistake (the message is retracted from the in-app inbox).

The email vendor's SDK has send_mail(address, subject, body); the chat vendor's SDK has post(channel_id, payload_dict). The team expects to add SMS and push notifications later.

Requirements

  • Task events must not depend on email or chat code: new channels are added without touching task code.
  • The notification code should talk to one Channel interface, whatever the vendor's API looks like.
  • Broadcasts must be undoable; undo removes the message from users' in-app inboxes and records a retraction.
  • Everything must be testable without real vendors.

Constraints

  • No frameworks or message brokers; in-process only for this exercise.
  • Emails and chat messages already sent can't be "unsent". Undo must be honest about that.

Deliverables

  1. A design naming the patterns used and the role of each class.
  2. Code that demonstrates: a task assignment notifying through two vendors, and a broadcast followed by undo.
  3. A note on how the design changes if notifications move to a separate service.

Hints

  • Events → reactions: Observer (in-process) now; Pub/Sub if it becomes a separate service.
  • Vendor SDKs with different shapes: one Adapter per vendor behind a Channel interface.
  • Undo: a Command that stores what it changed, with a compensating action for things that can't be reversed.

One possible solution

Observer, Adapter and Command working togetherTry it yourself
class EmailSdk:  # vendor SDKs with their own shapes
    def send_mail(self, address, subject, body):
        print(f"  [email] to {address}: {subject}")


class ChatSdk:
    def post(self, channel_id, payload):
        print(f"  [chat] #{channel_id}: {payload['text']}")


class EmailChannel:  # ADAPTER: vendor API -> Channel.send(user, text)
    def __init__(self, sdk):
        self.sdk = sdk

    def send(self, user, text):
        self.sdk.send_mail(f"{user}@example.com", text, text)


class ChatChannel:
    def __init__(self, sdk):
        self.sdk = sdk

    def send(self, user, text):
        self.sdk.post(f"dm-{user}", {"text": text})


class Events:  # OBSERVER: tasks publish, notifiers subscribe
    def __init__(self):
        self.handlers = []

    def subscribe(self, handler):
        self.handlers.append(handler)

    def publish(self, event):
        for handler in self.handlers:
            handler(event)


class Broadcast:  # COMMAND: execute + undo (compensating for what can't be reversed)
    def __init__(self, inboxes, users, text):
        self.inboxes, self.users, self.text = inboxes, users, text

    def execute(self):
        for user in self.users:
            self.inboxes.setdefault(user, []).append(self.text)
        print(f"broadcast sent to {len(self.users)} inboxes")

    def undo(self):
        for user in self.users:
            self.inboxes[user].remove(self.text)
        print(f"broadcast retracted; correction logged for: {self.text}")


channels = [EmailChannel(EmailSdk()), ChatChannel(ChatSdk())]
events = Events()
events.subscribe(lambda e: [c.send(e["assignee"], f"You were assigned '{e['task']}'") for c in channels])

print("task assigned:")
events.publish({"task": "Fix login bug", "assignee": "ada"})

inboxes, history = {}, []
command = Broadcast(inboxes, ["ada", "bob"], "Office closed tomorrow")
command.execute()
history.append(command)
print("ada's inbox:", inboxes["ada"])
history.pop().undo()
print("ada's inbox:", inboxes["ada"])

Output

task assigned:
  [email] to [email protected]: You were assigned 'Fix login bug'
  [chat] #dm-ada: You were assigned 'Fix login bug'
broadcast sent to 2 inboxes
ada's inbox: ['Office closed tomorrow']
broadcast retracted; correction logged for: Office closed tomorrow
ada's inbox: []

Undo removed the broadcast from the inboxes the app controls and logged a correction. It doesn't pretend to unsend emails. Adding SMS is one more adapter in channels.

Deliverables

Which patterns does your design use, and what role does each class play?

Model answer

Observer: task code publishes events and notifiers subscribe. Adapter: one per vendor SDK behind a Channel interface. Command: broadcasts as objects with execute() and undo() kept in a history.

Rubric

Look for: task code independent of channels, vendor details isolated in adapters, undo implemented as a command with an honest compensating action. Misses: task code calling vendors directly, undo that claims to unsend emails.

How does undo work for notifications that have already left the system?

Model answer

Undo can only reverse what the app controls (in-app inbox entries). For emails and chat messages already delivered, the command records a retraction and can send a correction, a compensating action rather than a true reversal.

Rubric

Strong answers distinguish reversible internal state from irreversible external side effects and design compensations for the latter.

What changes if notifications move to a separate service?

Model answer

In-process Observer becomes Pub/Sub through a broker (topics like task.assigned), subscribers must be idempotent because of at-least-once delivery, failures go to retries and dead-letter queues, and the command history must be stored durably.

Rubric

Look for: broker/topics, idempotency, retries and dead letters, durable command history. Weak answers keep synchronous in-process calls across services.

Open in the app to mark lessons complete, take notes and track your progress.