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.prefsbefore 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).
Back to all topics →