Skip to content

FRONTEND SYSTEM DESIGN

Design a Notification system

Design in-app and push notifications: real-time delivery, read state, and cross-device consistency.

Notification system architecture diagram

THE INTERVIEW SEQUENCE

How to drive this discussion

01

Clarify scope

02

Set requirements

03

Draw data flow

04

Defend trade-offs

Start with the smallest coherent design. After the main path works, introduce failure, scale, accessibility, observability, and release strategy one constraint at a time.

01Concrete scale assumptions

Millions of users may receive in-app, web-push, email, or mobile notifications across several devices.

At-least-once delivery is acceptable only with deterministic deduplication and idempotent read state.

Read status can converge eventually across devices; security or payment alerts require higher delivery priority.

Permission and channel preferences are user-controlled and checked at send time.

02Architecture decisions and rationale

Separate event, delivery, and presentation

A canonical notification event fans out through preference and channel services; the frontend renders a channel-specific projection.

Use push for wake-up, fetch for truth

A minimal push can prompt an authorized inbox fetch, avoiding sensitive or stale data in push payloads.

Cursor-page durable history

An ordered inbox supports reconnect and multi-device use; a socket carries hints and fresh events, not the only copy.

Treat read as an idempotent command

Stable notification ids and monotonic state make retries safe and let devices reconcile.

03Client contracts and data boundaries

Notification envelope

Stable event identity, version, category, deep link, and timestamps support deduplication and reconciliation.

TypeScript
type Notification = { id: string; version: number; category: string;  title: string; body: string; href?: string;  createdAt: string; readAt: string | null };type ReadCommand = { notificationIds: string[]; readAt: string; mutationId: string };

04Failure-mode analysis

Socket disconnects

Impact: Fresh notifications stop appearing.

Mitigation: Reconnect with jitter, fetch after the last cursor, and expose stale/offline state without losing durable history.

Event delivered twice

Impact: The inbox duplicates an alert or unread count.

Mitigation: Upsert by stable notification id and make downstream delivery and read commands idempotent.

Permission denied

Impact: Web push cannot be used.

Mitigation: Ask only after explained user intent, preserve in-app delivery, and provide browser-specific recovery guidance.

Unread count races

Impact: Badge and list disagree across tabs or devices.

Mitigation: Derive from versioned server state, broadcast local mutations across tabs, and reconcile after reconnect.

05Observability and operations

  • Measure accepted, delivered, displayed, opened, dismissed, deduplicated, and failed events per channel and category.
  • Track socket connection health, reconnect attempts, cursor catch-up duration, push subscription churn, and preference suppression.
  • Correlate event and delivery attempt ids while excluding notification body and other private content from logs.
  • Use synthetic notifications and dead-letter monitoring to prove the entire delivery path, not only API uptime.

06Security and accessibility boundaries

  • Authorize inbox reads and deep-link destinations independently; an opaque id is not authorization.
  • Keep sensitive content out of lock-screen push payloads and escape all sender-controlled text and URLs.
  • Use polite live announcements for new items without moving focus; urgent semantics are reserved for genuinely urgent events.
  • Make permission, channel, category, digest, mute, and unsubscribe controls clear and keyboard operable.

ANSOne full answer, start to finish

Drive the discussion yourself first. This is one strong answer delivered the way you would say it in the room — a model, not the only correct design.

The key framing: the real-time channel is an accelerator, not the source of truth. A REST or GraphQL API is the durable record, and the socket just makes things feel instant. Say that early, then the rest is about read-state staying consistent across a user's devices and clients reconciling after they have been offline.

  1. Step 1 · Clarify

    Ask: 'In-app only, or push notifications too? How many devices per user typically? Do users configure what they get notified about? And how urgent — is a few seconds of delay acceptable?' Assumptions: 'Both in-app and push, two to three devices per user, per-category preferences, and a few seconds of delay is fine for most categories.' Multiple devices is what makes read-state the hard part.

  2. Step 2 · Requirements

    User-facing: see new notifications quickly, an accurate unread badge, and marking one read on your phone updates your laptop. Hidden: no duplicates even though delivery is not exactly-once, the unread count stays consistent across devices and tabs, and a client that was offline catches up correctly.

  3. Step 3 · Two layers: API of record, socket as accelerator

    'A REST or GraphQL API is the authoritative source for notification state — the list, the read flags, the count. A WebSocket, or Server-Sent Events for the simpler one-way case, pushes new notifications and read-state changes to a user's open clients so they update instantly. But if the socket drops, a fallback poll still converges the client to correct state. The socket is never the only path.'

  4. Step 4 · Delivery pipeline

    'When an event happens server-side, the delivery service checks the user's preferences — category, channel, quiet hours — to decide whether to notify in-app, via push, or both. The notification is persisted first, then pushed over the socket to any connected clients, updating their unread count immediately.'

  5. Step 5 · Duplicates and read-state

    'Every notification carries a stable event id. Clients upsert by that id rather than assuming the transport delivered it exactly once, so an echo or a replay does not create a second entry. Marking one read sends an update to the server, which broadcasts the new read state to the user's other connected devices so their badges match. The count itself is monotonic and versioned so a stale update cannot decrement it wrongly.'

  6. Step 6 · Reconnect

    'A client coming back online does not trust that the socket stream caught it up. It fetches current state from the API — the recent list and the authoritative unread count — and reconciles. Cursor-paginated history is what makes this recovery reliable.'

  7. Step 7 · Push payloads and permission

    'Push payloads are minimal and display-safe — a wake-up or a short summary, never sensitive content on a lock screen — and the app fetches the real authorized data when opened. I ask for notification permission only after the user has done something that shows they want notifications, never on first load.'

How to close

'So: in-app plus push, multiple devices, per-category preferences. A durable API is the source of truth; the socket accelerates delivery and a poll is the fallback. Stable event ids and upserts handle duplicates. Read-state changes broadcast to all of a user's devices, and the unread count is monotonic and versioned. A reconnecting client refetches rather than trusting the stream. First reliability metric after launch: end-to-end delivered-vs-suppressed outcome by category and channel, plus the dead-letter rate.'

QAInterview questions and model answers

The socket reduces latency; cursor-paginated history is the durable source that recovers missed events after disconnect.

Primary references

DESIGN CHECKLIST

Build your answer from first principles

Use this checklist to rehearse the case without reading the worked answer above.

CLARIFYING QUESTIONS

Do not design yet. Use these questions to make hidden assumptions explicit and prevent solving the wrong problem.

  1. 01

    In-app only, or also browser push / mobile push?

  2. 02

    Do notifications need guaranteed delivery/ordering, or is best-effort acceptable?

  3. 03

    Is there a per-user preference system (mute categories, digest vs. instant)?

  4. 04

    How is 'read' state synced across a user's multiple open devices/tabs?

FUNCTIONAL REQUIREMENTS

These are the user-visible capabilities the design must support. They define the first version's scope.

  1. 01

    Show a real-time (or near-real-time) unread count and notification list in-app.

  2. 02

    Mark notifications as read individually or in bulk, syncing that state across open sessions.

  3. 03

    Respect per-category user preferences (e.g. muted notification types).

  4. 04

    Deliver push notifications when the user isn't actively in the app, without duplicating an in-app notification the user already saw.

NON-FUNCTIONAL REQUIREMENTS

These qualities shape the architecture even though users do not click them directly: latency, resilience, accessibility, consistency, and scale.

  1. 01

    Read/unread state must converge correctly across multiple simultaneously open tabs/devices.

  2. 02

    The system should not notify a user twice for the same event through two channels in a confusing way (e.g. push after they already read it in-app).

  3. 03

    Notification delivery should degrade gracefully if the real-time channel is temporarily unavailable (fall back to polling).

ARCHITECTURE

Now assign responsibilities to clear boundaries. Each part should have one reason to change and an explicit contract with the next part.

  1. 01

    A real-time channel (WebSocket, or Server-Sent Events for a simpler one-directional case) pushing new notifications and read-state changes to all of a user's open clients.

  2. 02

    A REST/GraphQL API as the source of truth for notification state, with the real-time channel treated as an optimization/acceleration layer, not the sole delivery mechanism.

  3. 03

    A preferences service the delivery pipeline consults before fanning a notification out to in-app, push, or both.

  4. 04

    A fallback polling mechanism so clients without a live connection (or during a reconnect) still converge to correct state.

DATA FLOW

Trace one important user action from input to rendered result. This exposes ownership, race conditions, retries, and stale-data paths.

  1. 01

    An event occurs server-side and is checked against the user's preferences to decide which channels to notify through.

  2. 02

    The notification is persisted, then pushed over the real-time channel to any currently connected clients for that user, updating their unread count instantly.

  3. 03

    Marking as read on one device sends an update to the server, which then broadcasts the new read state to the user's other connected devices so their badge counts stay consistent.

  4. 04

    A client reconnecting after being offline reconciles by fetching current state rather than assuming the real-time stream caught it up completely.

TRADE-OFFS

There is no perfect architecture. State what each choice optimizes, what it costs, and the signal that would make you revisit it.

  1. 01

    A live socket connection gives the most immediate experience but adds real infrastructure and reconnection-handling complexity; polling is simpler and often good enough for less time-sensitive notification types.

  2. 02

    Deduplicating across in-app and push channels well requires shared read-state awareness at send time, adding coordination cost, but skipping it produces a genuinely annoying double-notification experience.

  3. 03

    Persisting every notification durably before pushing it live adds a small latency cost but guarantees a client that reconnects can still recover the full history.

SCALING

Scale the measured bottleneck rather than every box. Frontend scaling includes payload size, main-thread time, rendering work, cacheability, and release safety.

  1. 01

    Fan-out delivery (one event to potentially many connected clients for high-following-count accounts) should be handled by a dedicated fan-out service, not computed synchronously in the request path that created the event.

  2. 02

    Batch/digest lower-priority notification types instead of pushing each one individually to reduce channel and UI noise at scale.

  3. 03

    Partition real-time connections by user shard so reconnection storms (e.g. after a deploy) don't overwhelm a single node.

COMMON PITFALLS

Interviewers often probe these failure modes. Name them before being prompted and explain the guardrail you would add.

  1. 01

    Treating the real-time channel as the only source of truth, so a missed message during a brief disconnect is simply lost forever.

  2. 02

    Not deduplicating across channels, notifying a user by push for something they already read in-app seconds earlier.

  3. 03

    Computing unread count purely client-side from a list that can silently fall out of sync with the server's authoritative state.

Your closing summary

Restate the critical user flow, the most important quality target, the architecture boundary that protects it, and the trade-off you accepted. Then name the first metric you would watch after launch.

Back to all cases →