Notification service architecture.
The architecture is straightforward. The reason this needs to be one service is that the rules about when not to send are the hard part, and they have to live somewhere every sender passes through.
The components
Every row below is read from the graph that produced the diagram above, so the two cannot disagree.
| Component | Tier | Why it is there |
|---|---|---|
| Producing services | Client | Supporting component |
| Notify API | Application | One entry point, many channels |
| Preferences | Application | Opt-outs and quiet hours enforced here |
| Templating | Application | Supporting component |
| Channel router | Application | Supporting component |
| Rate limiter | Application | Per user, not just per service |
| Delivery workers | Application | Supporting component |
| Kafka | Data | Supporting component |
| Dead letter queue | Data | Where undeliverables go to be inspected |
| Postgres | Data | Supporting component |
| Email provider | External | Supporting component |
| SMS / push | External | Supporting component |
Design decisions worth arguing about
A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.
One entry point, or the rules do not hold
If services can send email directly, then opt-outs, quiet hours and frequency caps are enforced only by whichever teams remembered them. Funnelling everything through one API is what makes those guarantees real rather than aspirational. The cost is a service on the critical path of many features and a migration to get everyone off their existing send calls, which is usually the harder half.
Rate limit per recipient, not per sender
Limits usually get applied per calling service, which does nothing for the user receiving twelve notifications from six services during one incident. Limiting per recipient across all channels is what actually protects the human. It requires shared state on the send path and a policy for what to do when the limit is hit, and dropping is almost always worse than collapsing several notifications into a digest.
Asynchronous, so a provider outage is not your outage
Calling the email provider synchronously ties the caller's latency and availability to a third party. Queueing means a provider outage becomes a delivery delay instead of a user-visible failure, and the queue absorbs bursts that would otherwise hit provider rate limits. The price is that senders no longer learn whether delivery succeeded, so status has to be exposed some other way.
A dead letter queue you actually read
Undeliverable notifications have to go somewhere, and the usual failure is that they go to a queue nobody has looked at in months. The design decision is not whether to have a DLQ but who is alerted when it grows and what tooling exists to replay from it, because a DLQ without a replay path is a deletion with extra steps.
How it changes with scale
Volume is bursty and correlated: everything wants to notify everyone at the same moments. Sizing for peak is wasteful and sizing for average means backlog during exactly the events that matter, so this is a queue-depth problem rather than a throughput one. Provider rate limits usually bind before your own capacity does.
Where it breaks first
A retry storm against a degraded provider. The provider slows down, workers time out and retry, the retries add load, and the provider degrades further. Without a circuit breaker and backoff the system converts a partial outage into a total one, and the queue grows past the point where catching up is possible within the notifications' useful lifetime.
Draw this yourself
Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.
When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.
Questions about this design
More templates
- URL Shortener System DesignThe canonical read-heavy system: roughly a hundred reads for every write, and a redirect that has to be fast enough that nobody notices it happened.
- Chat Application System DesignLong-lived connections change everything: the hard part is not storing messages, it is knowing which of your servers is holding the socket you need to write to.
- Payment System DesignThe only system on this list where being approximately right is indistinguishable from being wrong, and where the provider, not you, holds the truth.
- RAG Pipeline ArchitectureAlmost every RAG system that disappoints is failing at retrieval, not generation, and the architecture is what decides whether you can tell.