Design a Notification System, stage 10 of 11: change it
Write the sender
Write the core loop of an email sender: claim a delivery, check it is still wanted, respect the class's quota, send, and record the outcome. Handle transient, permanent and unknown outcomes differently.
System so far· 10 parts
Select a component to see what it is responsible for and which state it owns.
- 1Product services → Event stream: Domain events via outbox
- 2Event stream → Notification planner: Events, at least once
- 3Notification planner → Notifications DB: Insert notifications under dedupe keys
- 4Notification planner → Delivery queues: Deliveries by priority class
- 5Channel senders → Delivery queues: Claim deliveries
- 6Channel senders → Email provider: Send within quota
- 7Channel senders → Push services: Send to each device
- 8Web and mobile apps → Inbox API: Inbox and read state
- 9Inbox API → Notifications DB: Read notifications
- Asynchronous
- Request / response
What you need to know
0 of 2 checks done
A delivery moves through a small set of states. Each move is a conditional update: it only happens if the row is in the expected state, so concurrent senders can't both make it.
From To When queued sending a sender claims it (with a fresh attempt token) sending sent the provider accepted it sending suppressed preferences no longer allow it sending failed a permanent error, or a deliberate give-up sending queued a transient error; try later Check
Why condition the final update on the attempt token, not just the delivery id?The order of steps inside one attempt matters:
- Claim (
queued → sending). - Re-check preferences: the last chance to cancel before something irreversible.
- Take a token from the class's rate limit.
- Call the provider.
- Record the outcome, conditioned on the attempt token.
- Claim (
Think first
A sender claims a delivery and the machine dies mid-call. The delivery stays in 'sending' forever. What needs to exist?