design·lab

Real-world combination

Notification Service

One event triggers a fan-out to a user's preferred channels — each channel delivers itself through a shared send skeleton.

✗ The problem

Channels hard-coded at every call site

Sending a notification means calling each channel directly. Add SMS or Push and every call site needs editing — and nobody checks what the user actually wants.

function shipOrder(order) {
  sendEmail(order.user, msg);
  sendSMS(order.user, msg);
  // ← new channel? edit here too…
  // ← user opted out of SMS? too bad.
}
Every new channel means reopening shipOrder — and every other place a notification fires. Sound familiar? It's the Open/Closed smell again.
✓ The combination

Event triggers a service; channels are swappable strategies

An Observer subscribes the service to a domain event. Each channel is a Strategy behind send(msg). A Template Method defines the skeleton: build → deliver → log.

on('OrderShipped', e => notify(user, e));

class NotificationService {
  channels = [];              // Strategy list
  register(ch) { this.channels.push(ch); }

  notify(user, evt) {
    const msg = this.build(evt);   // Template Method
    this.channels
      .filter(c => user.prefs[c.name])
      .forEach(c => c.send(msg));  // each = Strategy
  }
}
OrderShipped
event
NotificationService
notify()
Email
SMS
Push
✓ See it live

Trigger the event — only preferred channels fire

The event fans out through the service to the user's enabled channels only. Register a brand-new channel at runtime — the service core never changes.

📦 OrderShipped
↓ NotificationService checks user prefs ↓
🔔 NotificationService
↓ fan-out to enabled channels ↓
📧
Email
📱
SMS
📲
Push
no events fired yet

Tip: click a channel to toggle the user's preference, then trigger again.

✓ Takeaway

Decoupled by event, chosen by preference

  • Decouples trigger from delivery: callers just raise an event; they never touch channels.
  • Open/Closed: register a new channel (Slack, webhook…) without editing the service core.
  • Preferences respected: each channel is filtered by user.prefs before it sends.
  • One send skeleton: build → deliver → log/retry is defined once, shared by every channel.
  • Caution: real delivery is async — plan for retries, rate limits, and per-channel failures.
🎯 Combines: Observer (event trigger) + Strategy (each channel) + Template Method (the send skeleton).