Design a Notification System, stage 3 of 11: decide
One notification per event, per channel
The event stream redelivers events after consumer restarts. Two planner instances can process the same event concurrently. Each must produce the same notifications, once.
System so far· 3 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
What you need to know
Event streams deliver at least once. A consumer that processes an event and crashes before recording its progress will see that event again after restarting. Two consumer instances can also briefly process the same event during a rebalance.
So the planner must produce the same result however many times it sees an event.
The most robust way is to give each notification an identity derived from its cause: (recipient, event id, channel). Store it under a unique constraint and insert with "ignore on conflict":
INSERT INTO notifications (dedupe_key, user_id, …) VALUES ('u88:evt991:email', 88, …) ON CONFLICT (dedupe_key) DO NOTHING;The first insert creates the row. Every replay hits the constraint and does nothing.
Check
Instead of a unique constraint, the planner first checks a Redis set of processed event ids, then inserts. Two planners get the same event at once. What can happen?Think first
Why not deduplicate by skipping notifications whose text matches one sent to the same user in the last 5 minutes?