🎓 A1Learner
🔍
0 of 42 lessons completed

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

ChoiceGainCost
Retries up to three attemptsMore sends arriveA retry can create a duplicate
Sent-log dedupeEach event notifies onceOne lookup before every send
Quiet hoursThe user's night stays quietRoutine messages arrive late
Text message used sparinglyUrgent news reaches the userHighest cost per message

Exercise

Why does this design keep a sent-log if retries already rescue failed sends?