Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 67

    Notification System Design

    Notification system design tests Strategy + Factory + Observer + Outbox composition.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Notification system design tests Strategy + Factory + Observer + Outbox composition. Naive if/else channel dispatch breaks within a quarter. This lesson walks the scalable canonical design.

    The story

    SaaS notifications started as one function with 4 if-branches. 18 months later: 47 branches across 8 functions, 6 files to send one notification. Rebuild: Strategy per channel + Factory selection + Outbox reliability + Observer triggering. New channel = one file.

    The business problem

    Notification sprawl blocks every product launch:

    • 47 if-branches: email/SMS/push/webhook/slack duplicated.
    • 6 files per send: no single seam to extend.
    • Unreliable delivery: fire-and-forget HTTP in request path.
    • Compliance: opt-out, rate limit, audit per channel.

    The problem teams faced

    Notification system forces:

    • Multiple channels with different APIs and retry policies.
    • Trigger from many domain events — OrderPlaced, PasswordReset.
    • Reliable async delivery — not in critical request path.
    • Easy addition of new channels without editing dispatch logic.

    Understanding the topic

    Intent: Extensible multi-channel notification architecture.

    • Strategy — one impl per channel (Email, Sms, Push).
    • Factory — select strategies by user preferences + event type.
    • Observer — domain events trigger notification pipeline.
    • Outbox — reliable enqueue from transactional context.

    Internal architecture

    Notification service structure:

    ts
    interface NotificationChannel {
    send(recipient: Recipient, template: Template, data: Record<string, unknown>): Promise<void>
    }
    class NotificationDispatcher {
    constructor(private factory: ChannelFactory) {}
    async dispatch(event: DomainEvent, user: User) {
    const channels = this.factory.resolveChannels(event.type, user.preferences)
    await Promise.all(channels.map(ch => ch.send(user, event.template, event.data)))
    }
    }
    // Order service: Outbox.write(OrderPlaced) in same transaction as order save

    Visual explanation

    Three diagrams cover channel Strategy, event pipeline, before/after if-else:

    Strategy per channel
    NotificationPort
    Interface
    EmailStrategy
    SendGrid
    SmsStrategy
    Twilio
    PushStrategy
    FCM
    New channel = one new Strategy file — not new if-branch.
    Event → Outbox → dispatch
    OrderPlaced
    Domain event
    Outbox row
    Same TX as order
    Relay worker
    Poll + publish
    Notification svc
    Factory + Strategy
    Observer in-process; Outbox + bus cross-service.
    47 branches → 1 file per channel
    Before: 47 if
    Sprawl
    Strategy extract
    Per channel
    Factory registry
    Select
    New channel: 1 file
    Outcome
    18-month sprawl fixed by creational + behavioral composition.

    Informative example

    Before / after dispatch:

    ts
    // ❌ 47 if-branches across 8 functions
    function notify(user, type, data) {
    if (type === "email") { /* sendgrid */ }
    else if (type === "sms") { /* twilio */ }
    // ... 45 more
    }
    // ✅ Strategy + Factory
    class ChannelFactory {
    resolve(eventType: string, prefs: Prefs): NotificationChannel[] {
    return prefs.enabledChannels
    .map(name => this.registry.get(name)!)
    .filter(ch => ch.supports(eventType))
    }
    }

    Execution workflow

    1Notification rebuild workflow
    1 / 4

    Audit if-branches

    Count channel conditionals.

    47 branch story.

    Real-world use

    Twilio SendGrid architecture; LinkedIn notification tier; Uber multi-channel dispatch; AWS SNS + SES patterns.

    Production case study

    47 if-branch rebuild:

    • Before: 4 branches → 47 in 18 months; 6 files per send.
    • Rebuild: Strategy + Factory + Outbox + Observer.
    • Outcome: new channel in one file, one PR.

    Trade-offs

    • Pro: open-closed for channels; reliable async; testable per Strategy.
    • Con: more files; Factory registry maintenance.

    Decision framework

    • 2+ channels → Strategy per channel minimum.
    • Cross-service triggers → Outbox + notification consumer.
    • Single email only → don't build full platform yet.

    Best practices

    • Contract test each NotificationChannel Strategy.
    • Template engine shared; channel Strategy handles transport only.
    • Dead letter queue for failed sends after retries.

    Anti-patterns to avoid

    • Sync send in HTTP request path — latency + reliability risk.
    • Fat NotificationService with all channel logic inline.
    • Observer across services via direct HTTP callbacks.

    Common mistakes

    • Factory returns wrong channels — test preference matrix.
    • Duplicate notifications on Outbox relay retry — idempotent send keys.

    Debugging tips

    • Trace: event → outbox → relay → dispatcher → which Strategy failed.

    Optimization strategies

    • Batch email sends; rate limit SMS per user.

    Common misconceptions

    • Observer ≠ entire system — Observer triggers; Strategy delivers.
    • Notification ≠ email only — multi-channel from day one if roadmap says so.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionDesign notification system?+

    Answer

    Domain events → Outbox → async worker → NotificationDispatcher → Strategy per channel, Factory for user prefs. Template engine shared.

    Follow-up

    New Slack channel?
    2IntermediateQuestionWhy Outbox for notifications?+

    Answer

    Atomic with domain transaction — order saved iff notification enqueued. Relay handles publish reliably.

    Follow-up

    vs direct publish?
    3IntermediateQuestionStrategy vs if-else?+

    Answer

    Open-closed: new channel = new class, register in Factory. if-else grows unbounded — 47 branch story.

    Follow-up

    Factory role?

    Summary

    You can design extensible multi-channel notifications. Tell the 47-branch rebuild — you own Notification System Design.

    Ready to mark this lesson complete?Track your journey across the entire course.