cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 214 of 218
Ideas this builds on
High-level design214 / 218

Design a notification pipeline

Picture

A saved reminder might become email, push, or both. Preferences and channel capabilities decide which jobs should exist.

Predict

Does one dedup key per event distinguish email from push for two different users?

Walk through the small example

No, include recipient and channel. Identity must match the individual delivery being protected.

Mental model and runnable Python model

Separate event ingestion, preference/template resolution, delivery-job creation, channel workers, and provider adapters. Store stable delivery IDs and bounded retry status. Use an outbox for reliable creation from a business event where needed. Rate-limit per provider and tenant, respect quiet hours and unsubscribe policy, and dead-letter persistent failures with an operational path. End-to-end exactly-once delivery may be impossible if a provider offers no idempotency or status query after an uncertain timeout; document the duplicate-versus-loss tradeoff.

Requirements and data

Define transactional versus promotional notifications, channels, latency, quiet hours, preferences, and retention. Store template version, recipient, channel, event ID, delivery ID, status, attempts, and next attempt time without unnecessarily retaining sensitive content.

Pipeline

Ingest durable events, resolve current policy and template, create deduplicated jobs, and route them to channel workers. Workers recheck relevant preferences, call provider adapters with idempotency where supported, and persist the outcome or uncertain state.

Retries and limits

Use bounded exponential backoff with jitter, provider-specific rate limits, and deadlines. Separate invalid recipients from transient provider failures. Dead-lettered work needs inspection and safe replay; it is not a place to silently abandon the user’s promise.

Tradeoffs and observation

Measure queue age, attempts, provider acceptance, delivery reports, and opt-out violations separately. Acceptance is not necessarily inbox delivery. For providers without idempotency or status lookup, state the unavoidable uncertain-outcome policy.

What the function promises: Planner creates unique local (event,user,channel) jobs for currently permitted channels. It does not send messages or implement durable scheduling.

class Planner:
    def __init__(self):
        self.jobs = set()
    def plan(self, event, user, channels, permitted):
        created = []
        for channel in dict.fromkeys(channels):
            key = event, user, channel
            if channel in permitted and key not in self.jobs:
                self.jobs.add(key)
                created.append(key)
        return created

planner = Planner()
assert planner.plan("e1", "Ana", ["email","push"], {"email"}) == [("e1","Ana","email")]
assert planner.plan("e1", "Ana", ["email"], {"email"}) == []
assert planner.plan("e1", "Bo", ["email"], {"email"}) == [("e1","Bo","email")]

Work and storage: Expected O(requested channels) planning and O(retained delivery IDs) storage.

When to recognize it: Queue work by channel, pace calls, retry suitable failures, and record provider responses. A timeout may occur after the provider accepted the message.

Check before applying it: Delivery identity and provider guarantees determine whether duplicate suppression is actually achievable.

Change one rule

A provider times out after accepting an email but has no idempotency support. Can the client guarantee both no duplicates and no losses?

See what changes

Not generally under that uncertainty. Use provider status lookup if available or choose and document a bounded retry/reconciliation policy appropriate to the notification.

Connect

Let work wait without losing its identity: Each queued delivery needs retries, deduplication, and a failure policy.

Design a chat service: Distinguish notification acceptance from durable chat-message storage.