Skip to main content
When an interviewer asks what kind of queue you should choose for a notification system, name these three essential properties:
  • Durable: the queue persists messages to disk (not only in RAM) so messages survive process or machine restarts.
  • Acknowledged delivery: a message is removed from the queue only when a worker confirms the work is done (acknowledgements).
  • Multi-worker support: the queue allows multiple workers to consume in parallel so you can scale out as load increases.
One property we deliberately relax is strict global ordering. If two unrelated notifications swap order (for example, Alan’s and John’s), it usually doesn’t matter. Relaxing strict ordering reduces coordination and makes scaling much easier.
A dark-themed presentation slide titled "What kind of queue?" with colorful diagrams showing three required properties—durable (on-disk vs RAM), acknowledged messages, and many workers for parallel processing. It also notes strict ordering isn't needed and includes example task boxes and worker icons.
This design separates the notification-accepting side (API) from the sending side (workers). In interviews, emphasize that this is asynchronous processing: the caller never waits for delivery; the API enqueues the notification and returns quickly. When there is a burst of notifications, durable queues absorb the load and worker pools can scale independently to catch up.
This pattern is asynchronous processing: the API enqueues work and returns immediately, while independent worker processes consume the queue and perform the actual sends.
Should all channels share a single queue (push, SMS, email, in‑app)? No — a shared queue risks head-of-line blocking. If one provider (for example, the email provider) is slow or rate-limited, workers will be tied up processing those slow messages and other channels will be effectively blocked.
A dark-themed diagram titled "One Slow Provider" showing a single shared queue feeding delivery workers for Push, SMS, Email and In‑App channels. The Email provider is highlighted in red as slow, causing tasks to get stuck and block the queue.
To avoid head-of-line blocking, isolate channels by deploying one durable queue per channel. In the example below we have four queues (Push, SMS, Email, In‑app), each with its own worker pool. Each channel scales independently — a slow email provider affects only the email queue while push, SMS, and in‑app delivery continue unhindered. The in‑app channel is the simplest to implement because it does not depend on external providers. The notification already exists in our database; the in‑app worker only needs to mark the notification as delivered/seen. Typical in‑app flow:
  • When storing notifications, set a seen flag to false.
  • When the mobile app requests unseen notifications, the server queries rows where seen == false and returns them.
  • When the user views notifications, the app calls the server to mark them as seen (set seen = true).
Introduce the term fan-out: the notification service publishes a single event and creates a copy of that event in every channel queue. Copying one event into many queues is called fan-out. A concise summary of the design so far:
  • API (for example, POST /notify) records the notification in the database with a unique notification ID and immediately returns accepted.
  • The notification fans out into multiple durable queues (one per channel), so a slow provider can’t block other channels.
  • Workers consume each channel queue, look up contact details and preferences, and call providers to deliver notifications.
A dark-themed high-level architecture diagram titled "The design so far" showing a caller POST /notify to a service that stores an ID and fans out jobs into parallel queues and processors which send notifications to Push, SMS, Email, and In‑app channels.
Worker responsibilities — what should a worker do when it receives a message? Operational notes and best practices
  • Idempotency: Use the notification ID as the deduplication key with providers where possible. For example, store a provider-specific send ID in a durable store and skip duplicate sends.
  • Retries and DLQs: Configure exponential backoff and a maximum retry count. Send persistent failures to a DLQ for inspection and reprocessing.
  • Observability: Emit structured logs, metrics, and distributed traces for each send attempt so failures are diagnosable.
  • Rate limiting and throttling: Use token buckets or leaky buckets per provider to avoid hitting provider rate limits and cascading failures.
  • Concurrency controls: Limit concurrent sends per user or per provider to reduce risk of throttling or account-level blocking.
Putting it together: durable per-channel queues + fan-out + resilient worker semantics (timeouts, retries, idempotency, DLQ, metrics) yields a notification system that is resilient, scalable, and interview-ready. Links and references

Watch Video