Design notification service

Email, SMS, push. Easy to sketch, and the retry question is where it gets real.

standard · 45 minutes · 8 classes

Requirements

  • Send a notification over one of several channels — email, SMS, push.
  • Per-user preferences decide which channels are eligible.
  • Templates rendered with per-notification data.
  • Retry transient failures; give up on permanent ones.
  • Rate-limit per user so nobody gets twenty messages in a minute.

Say these are out of scope

  • Building the actual delivery infrastructure — assume vendor SDKs exist.
  • Analytics on opens and clicks.
  • Internationalisation of template copy.

A shape that works

One reasonable decomposition, not the only one. What matters in the round is that you can defend the boundaries you drew.

NotificationServicesend(userId, event, data)«interface»Channeldeliver(message): ResultEmailChannelSmsChannelPushChannelPreferenceschannelsFor(user, event)TemplateEnginerender(event, data)«interface»RetryPolicynextDelay(attempt)
extendsimplementsownsuses

What they'll push on

Which failures do you retry?

Transient ones only. A 500 or a timeout is worth retrying with exponential backoff and jitter; a 400, an unsubscribed address or an invalid number will fail identically forever and retrying just burns quota. The channel result has to distinguish the two — a boolean success flag isn't enough.

The send succeeds but your process dies before recording it.

You'll retry and the user gets it twice. At-least-once is the honest default, so make delivery idempotent with a key per (user, event, channel) that the sender checks. Claiming exactly-once across a network boundary is the wrong answer and interviewers listen for it.

Why is each vendor behind an adapter?

Because your code should depend on Channel, not on a vendor's SDK shape. Swapping providers becomes one new adapter rather than edits at every call site, and it gives tests a seam — otherwise every test needs the network.

Patterns in play