Notifications: Trade-Offs and Wrap-Up
The trade-offs of a notification system are the choices where helping one goal hurts another, like retries that rescue sends but risk duplicates. This lesson weighs those trade-offs and wraps up the design.
What extra attempts buy
One event fails twice before it would succeed. The printout shows a single attempt giving up where three attempts deliver.
def try_send(failures, max_attempts):
attempt = 0
while attempt < max_attempts:
attempt += 1
if attempt > failures:
return attempt
return 0
result = try_send(2, 1)
if result == 0:
print("one attempt only: not sent - the send gives up")
else:
print("one attempt only: sent on attempt", result)
print("three attempts allowed: sent on attempt", try_send(2, 3))What the sent-log buys
The same event is delivered three times in a row. The printout counts the notifications with and without a sent-log check.
deliveries = ["e-1", "e-1", "e-1"]
without_log = 0
for event_id in deliveries:
without_log += 1
sent_log = {}
with_log = 0
for event_id in deliveries:
if event_id not in sent_log:
sent_log[event_id] = True
with_log += 1
print("without a sent-log:", without_log, "notifications for one event")
print("with a sent-log:", with_log, "notification for one event")Channels trade speed for cost
Each channel trades speed against cost per message. The printout lines the three channels up for comparison.
channels = [
("push", "fast", "lowest"),
("email", "slower", "low"),
("text message", "fast", "highest"),
]
for name, speed, cost in channels:
print(name, "- speed:", speed, "| cost per message:", cost)Example
sent_log = {}
held = []
dead_letter_queue = []
hour = 23
plan = {"e-1": 0, "e-2": 99}
events = [
("e-1", True),
("e-2", True),
("e-1", True),
("e-3", False),
]
for event_id, urgent in events:
if event_id in sent_log:
print("event", event_id, "duplicate - blocked")
continue
if not urgent and (hour >= 22 or hour < 7):
held.append(event_id)
print("event", event_id, "held by quiet hours")
continue
attempt = 0
sent = False
while attempt < 3:
attempt += 1
if attempt > plan[event_id]:
sent = True
break
if sent:
sent_log[event_id] = True
print("event", event_id, "sent on attempt", attempt)
else:
dead_letter_queue.append(event_id)
print("event", event_id, "parked in the dead-letter queue")
print("sent:", len(sent_log), "| held:", len(held), "| dead-letter:", len(dead_letter_queue))Key points
- Retries: more attempts rescue more sends, and each retry is another chance to send a duplicate.
- Dedupe cost: the sent-log check adds one lookup before every send, and buys freedom from duplicate notifications.
- Quiet hours cost: held messages respect the user's night, and arrive later than they were created.
- Channel cost: a text message costs the most per message, so the design saves it for messages that matter.
- Dead-letter cost: parking failed sends needs a human to read the queue, and silent dropping would need none.
- Wrap-up: the design clarifies first, queues events, checks preferences, retries with dedupe, and respects limits and quiet hours.
In this example, the finished policy runs one night batch with retries, dedupe and quiet hours together. One event sends, one hopeless event is parked, one duplicate is blocked, and one routine event waits for the morning.
Note
every trade-off here was chosen for the user's trust, which a notification system spends fast when it spams.
Try it Yourself
Edit the code, then press Run.
Running is simulated
The main trade-offs
| Choice | Gain | Cost |
|---|---|---|
| Retries up to three attempts | More sends arrive | A retry can create a duplicate |
| Sent-log dedupe | Each event notifies once | One lookup before every send |
| Quiet hours | The user's night stays quiet | Routine messages arrive late |
| Text message used sparingly | Urgent news reaches the user | Highest cost per message |
Exercise
Why does this design keep a sent-log if retries already rescue failed sends?