Case study
Design a real-time chat system
You type "see you at 8" in WhatsApp, Slack, or Discord and hit send. The checkmarks flip from sent to delivered to read. To you it is one message. Under the hood the system must append a durable , push to online devices over a , and notify offline phones without ever losing the message.
Think billions of users, sub-100ms delivery when online, strict ordering per chat, and no duplicates when clients retry. The product is a durable conversation log with a fast delivery layer on top, not just WebSockets.
What happens on send?
Dedupe client id; assign the next seq.
Durable write
Append to the conversation log via the stream.
Deliver if the recipient is online.
Push + sync
Notify offline devices; fill gaps on reconnect.
Try it: chat delivery, partitions, and ordering
Messages hash to conversation_id partitions. Fan-out to online recipients; offline users catch up from durable storage on reconnect.
- Client WS
- Chat gateway
- Partition
- Message store
- Recipient WS
- In flight
- 0
- Delivered
- 0
- Ordering scope
- per-conversation
- Partition
- conversation_id
Capacity
backpressure0 messages in flight. Shed load if gateway queues grow.
Consistency
per shardTotal order across all chats is not required; per-conversation sequence suffices.
Delivery
at-least-onceClients dedupe by message id on reconnect.
Send messages through a partitioned chat service. Ordering is guaranteed per conversation, not globally.
Related building blocks
WebSockets, queues, caching, and sharding all show up in chat HLDs.
What actually happens
The gateway forwards the frame to ingest. The uses so retries do not double-write, assigns a monotonic seq, publishes to Kafka, and acks the sender that the server accepted the message. Workers persist the log and try real-time delivery. The acknowledgement is safe only after the accepted message and its stable sequence reach the promised durable boundary. If Kafka is that first boundary, its replication and retention must support recovery until the long-term store catches up. Do not acknowledge merely because an in-memory gateway accepted a frame. Sequencer failover must also preserve existing sequence assignments and reject stale ownership.
If the recipient is online, the delivery router finds their gateway and pushes the frame; flow back later. If offline, the message is already safe in storage; FCM/APNs is best-effort. On reconnect, the client syncs with . Keep and typing out of Cassandra. They are ephemeral.
How to choose the chat building blocks
1. Is the data durable or ephemeral? Messages and receipts belong in the . and typing stay in short-TTL caches.
2. How do retries stay safe? Require and a per-conversation before you talk about WebSocket fan-out.
3. What if the phone was offline? Persist first, push later, then gap-fill from on reconnect, never rely on push alone.
Simple rule of thumb
Design the ordered log first, then the delivery layer, then offline push. If a step would lose messages when Kafka or push is down, reorder the design until the log is authoritative.
Scale target
These are illustrative design targets: a very large registered population, low online delivery latency in the intended geography, and no acknowledged message loss within the stated storage failure model. At-least-once delivery with idempotency prevents duplicate logical messages; it does not by itself guarantee durability. Define where the server commits before acknowledging, how many failures that storage tolerates, and how long offline history remains available.
High-level design diagram
Real-time messaging platform: entry → connections → ingest → storage → push and sync. Match the happy path at the bottom of the image.

Client waits or persistent session
Kafka and background workers
Ingest, sequencer, message store
Gateway delivery to online user
ACK / read back to sender
Design highlights (from the diagram)
Push path
A WebSocket gateway plus a connection registry so online devices get frames quickly.
Log first
Make the durable log the truth; delivery is a transport layer that can retry.
Reconnect
Per-conversation sequence numbers let clients replay gaps after a drop.
Groups
Fan out asynchronously. Partition the bus by conversation_id for order.
Offline
Batch and collapse push notifications: do not fire one alert per message.
Privacy option
E2EE is optional: the server can route ciphertext without reading bodies.
Architecture layer by layer
Aligned with the numbered blocks in the HLD.
01Clients, DNS, CDN, and edge securityReach the nearest region safely before chat traffic hits origin.
Reach the nearest region safely before chat traffic hits origin.
Think of it like: Airport security and signs before you enter the terminal building.
The mobile, desktop, or browser client first resolves a service address and reaches a healthy edge. Static assets such as avatars, stickers, and application bundles can come from a CDN, while private chat traffic continues to an authenticated gateway. Geographic routing reduces avoidable travel time, but the conversation’s write owner and data location determine where a durable send must ultimately run.
The edge terminates the encrypted connection where appropriate, rejects oversized or excessive requests, and routes accepted traffic to the right service. DDoS filtering and a web application firewall reduce unwanted load; application authorization still decides whether this user may send to this conversation. Keep connection establishment separate from membership checks because permissions can change while a socket remains open.
02Connection and real-time deliveryPersistent connections, presence, typing, and routing to the right device.
Persistent connections, presence, typing, and routing to the right device.
Think of it like: A switchboard operator who knows which desk extension each person is at.
A real-time gateway holds each device’s connection, validates session state, and exchanges heartbeats to detect broken paths. A connection registry maps a user and device to the gateway currently holding its socket. Presence is an approximate view derived from those heartbeats; it can briefly remain online after a disconnection, so delivery cannot rely on presence as proof that a message arrived.
Delivery workers locate the current gateway, send a frame, and track the device acknowledgment separately from durable message acceptance. A stale registry entry or failed gateway is repaired by reconnect and history replay. Typing indicators expire and can be dropped under pressure; durable messages remain in history. Bound each device’s outgoing buffer so a slow receiver cannot exhaust gateway memory.
The registers user_id → node_id in a . Delivery services read that map before pushing.
03Message ingestion (durable path)Validate, order, and append to the log before anyone relies on delivery.
Validate, order, and append to the log before anyone relies on delivery.
Think of it like: Registered mail: get a receipt number before the post office tries front-door delivery.
Ingestion authenticates the sender, checks current conversation permissions and payload limits, and associates a stable client_message_id with the send. The conversation owner assigns an ordered sequence and records the message durably before returning a durable-acceptance acknowledgment. Deduplication must recover the same identifier and sequence on retry; simply generating another sequence number does not make a repeated send safe.
A replicated log such as Kafka or Pulsar can be the accepted-message source while persistence workers build queryable history. Route a conversation’s events to the same partition and preserve its processing order; many conversations can share that partition. Unread counts, delivery fanout, and permitted search projections consume the accepted event independently, so a slow secondary feature does not block durable ingestion.
The () defines the accepted order inside one conversation. Persist the assignment and deduplication result with the accepted event. can buffer downstream work, but its configured durable acknowledgment must precede any claim that the send is safely accepted.
04Persistence layerMessages are append-only; profiles and groups change less often.
Messages are append-only; profiles and groups change less often.
Think of it like: A filing cabinet per conversation drawer, plus a membership roster book.
Store message history by conversation and sequence, adding a time bucket when an indefinitely growing conversation would create an oversized partition. Cassandra or Scylla are possible query-oriented stores, but the chosen database must meet the required durability and pagination behavior. Keep identity, membership, roles, and settings in an authoritative catalog with a clear version for permission decisions.
Images and attachments belong in object storage, with message rows referencing their authorized object versions. A CDN can deliver permitted media without making objects public by accident. Search is a derived projection and may lag; with end-to-end encryption, the server cannot build a plaintext content index from ciphertext. Deletion and retention workflows must reconcile message metadata, attachment references, and any searchable copies.
05Push, groups, sync, and receiptsOffline users, large groups, history catch-up, and seen checks.
Offline users, large groups, history catch-up, and seen checks.
Think of it like: Voicemail when you are away, plus a read log on the envelope.
When a device is offline, a push service uses its platform token to send a bounded notification, often collapsing several updates into one conversation alert. That alert is a prompt to sync, not the durable message itself. On reconnect, the client requests messages after its last applied sequence; the server replays gaps and makes duplicates harmless. An expired history cursor requires a stated resynchronization policy.
Group management records role changes and membership revocation independently of presence. Apply the current access policy to sending, history reads, and delivery to removed members; an eventually refreshed member list must not silently extend revoked access. Delivered and displayed receipts use separate progress records, often per device. A displayed receipt reports a client action, not proof that a human read or understood the message.
() track a client-reported displayed position separately from delivery. Store monotonic progress per conversation and apply the product’s multi-device and privacy rules; never overwrite a newer position with a delayed older update. The XMPP displayed-marker specification illustrates this distinction.
06E2EE and trustOptional client-side encryption; server routes opaque blobs.
Optional client-side encryption; server routes opaque blobs.
Think of it like: Locked boxes the courier carries without a master key.
With end-to-end encryption, sending devices encrypt for the intended recipient devices, and the server stores and routes ciphertext with the metadata needed for delivery. Device key verification, key rotation, recovery, and adding or removing devices become product requirements. Use an established protocol and supported libraries rather than inventing encryption from a diagram.
Private key material stays under client control, while a service may distribute public key bundles and device-directory information. A new group membership or device change can require new encryption state to protect future messages. Server-side plaintext search and automatic content inspection are limited by this boundary; scope search, reporting, backups, and moderation according to what participants deliberately reveal.
Mention () when the problem statement says WhatsApp / iMessage / Signal. Otherwise note it as a follow-up dimension.
07Observability, admin, and disaster recoveryYou debug chat with delivery latency metrics, not CPU alone.
You debug chat with delivery latency metrics, not CPU alone.
Think of it like: Hospital monitors on heart rate and blood pressure, not just bed count.
Measure the stages users experience: send to durable acknowledgment, accepted message to device delivery, reconnect to completed catch-up, and the age of undelivered work. Trace a stable message identifier through gateway, ingestion, log, history store, and delivery without logging private message bodies. Monitor connection counts and outgoing-buffer pressure because a quiet CPU graph can hide failing real-time delivery.
Operations need controlled account restrictions, feature rollout, backups, and a tested regional recovery plan. A replicated message log and the membership catalog must recover to compatible states so failover does not grant stale permissions or forget accepted messages. Additional regions require explicit write ownership and spare capacity; active-active deployment is a design choice with conflict and operating costs, not a default checkbox.
Durable vs ephemeral
Interviewers check that you do not store typing indicators in Cassandra.
Durable (must not lose)
These survive reconnects, crashes, and region failovers. Treat them as the source of truth.
Retain message bodies or ciphertext together with identifiers and the order assigned by the . Keep media references tied to the version actually sent. An acknowledged message must remain recoverable under the stated failure model even if live delivery fails.
Group membership also needs a durable history and a defined revocation boundary. A removed member must not receive new content merely because a gateway cached an old roster. Version permission decisions, invalidate relevant subscriptions, and define historical-access rules explicitly. Replication lag may delay a roster display; it must not silently determine authorization.
Ephemeral (OK to lose or approximate)
These make chat feel alive, but rebuilding them from heartbeats or reconnect is cheaper than treating them as permanent rows.
and typing indicators can expire and be rebuilt from new heartbeats. entries also expire because a crashed gateway may never remove its mapping. Treat them as routing hints, then recover missing durable messages through history replay.
Push batching timers and other presentation hints can be recomputed without changing message ownership or permissions. Losing a timer may delay or duplicate an alert, so collapse notifications by stable conversation state. This separation protects the durable path from the much noisier stream of transient updates.
Message flows
Happy path, offline, and reconnect: three stories worth drawing.
1. Send while recipient is online
Separate three acknowledgements: the service durably accepted the message, a recipient device received it, and the recipient marked it read. They can happen far apart in time and are different user-visible states. A lost delivery acknowledgement can cause another delivery attempt, so the receiving device also deduplicates by message ID. Reading on one device can then update a monotonic per-conversation read position.
Read the diagram definition
sequenceDiagram
autonumber
participant S as Sender client
participant GW as Connection gateway
participant IN as Ingest API
participant SEQ as Sequencer
participant K as Kafka
participant DB as Message store
participant RT as Delivery router
participant RG as Recipient gateway
participant R as Recipient client
S->>GW: send message (WS)
GW->>IN: forward + auth
IN->>SEQ: assign seq (dedupe client_message_id)
SEQ->>K: message-ingested
SEQ-->>S: ack sent (server received)
K->>DB: persist worker
K->>RT: delivery worker
RT->>RG: push if online
RG->>R: deliver frame
R-->>RG: delivery ACK
RG-->>S: delivered status
Note over R: User opens message
R->>RG: read receipt (WS)
RG->>RT: forward receipt
RT->>GW: push if sender online
GW-->>S: read status update2. Recipient offline
An offline recipient still has a durable message waiting in the conversation history. A push notification is a wake-up hint that may arrive late, be collapsed, or never arrive. When the app opens, it fetches missing messages regardless of whether push succeeded. Avoid putting sensitive message content in push payloads unless that exposure matches the product’s encryption and notification policy.
Read the diagram definition
sequenceDiagram
autonumber
participant S as Sender
participant K as Kafka
participant DB as Message store
participant P as Push service
participant D as Recipient device
S->>K: message-ingested
K->>DB: append message
K->>P: recipient offline
P->>P: batch by conversation (5s window)
P->>D: FCM / APNs ("3 new messages")
Note over D: User opens app
D->>DB: GET messages after last_seq
DB-->>D: gap fill + history3. Reconnect and gap repair
The device sends the last contiguous sequence it has durably stored for each conversation. If it has 101 and 103 but not 102, acknowledging 103 as complete would hide a hole, so it reports 101 and requests the gap. Coordinate replay with live delivery using a boundary sequence, buffer later live frames, and deduplicate overlaps. For expired history, return an explicit resync requirement rather than silently skipping it.
Read the diagram definition
sequenceDiagram
autonumber
participant C as Client
participant GW as Gateway
participant SYNC as Sync service
participant DB as Message store
C->>GW: reconnect + token
GW->>GW: refresh registry entry
C->>SYNC: last_acked_seq per conversation
SYNC->>DB: fetch seq > last_acked
DB-->>C: missed messages
C->>C: merge with local cache4. Group message fan-out
Commit the group message once, then expand delivery to the relevant members and their active devices asynchronously. A 100-member group with two devices per member can create roughly 200 delivery attempts from one message. Membership and access rules decide which recipients are eligible, including what a newly joined member may read. Large groups need bounded delivery batches and backpressure so one busy group does not starve ordinary chats.
Read the diagram definition
sequenceDiagram
autonumber
participant S as Sender client
participant IN as Ingest API
participant SEQ as Sequencer
participant K as Kafka
participant DB as Message store
participant F as Fan-out worker
participant RG as Recipient gateway
participant R1 as Recipient 1
participant RN as Recipient N
S->>IN: send group message
IN->>SEQ: assign seq (once)
SEQ->>K: message-ingested
K->>DB: persist single log write
K->>F: fan-out worker
par async to N members
F->>RG: deliver to member 1
RG->>R1: push frame
and
F->>RG: deliver to member N
RG->>RN: push frame
endWhat users experience
Walk through these before naming Kafka partitions.
Send a message
You: You type "see you at 8" and hit send.
System: Gateway → ingest → sequencer assigns seq → Kafka → persist. You get an ack that the server accepted it.
Friend is online
You: They see the message almost instantly.
System: Delivery worker finds their gateway from Redis registry, pushes over WebSocket, delivery ACK flows back to you.
Friend was offline
You: They get a push notification later and catch up.
System: Message already in Cassandra. Push service batches alerts. On open, client syncs messages after last_seq.
Requirements
Functional
- 1-to-1 and group chat (e.g. up to 500 members)
- Message history with pagination
- Online presence and last seen
- Delivery and read receipts
- Media (image, video, file)
- Push when offline
Non-functional
- 500M DAU, 2B registered users (round interview numbers)
- Sub-100ms delivery when both sides online
- Strict ordering per conversation
- At-least-once delivery with idempotency
- 99.99%+ availability; E2EE if in scope
Numbers that drive the design
500M DAU × 40/day = 20B/day
5× average (460K–1.15M range)
~20% of DAU; 3.3M presence/s if central
Online path; push is seconds
Assume 500M DAU × 40 messages/day = 231K writes/sec average (1.15M/s at 5× peak). Delivery attempts multiply by ~1.5 devices (347K/s before groups). Group messages to 100 members add 1M deliveries/sec at 10K group msg/s. Presence for ~100M online users must stay in TTL-backed cache, not the profile database.
Case-study upgrade
Chat Delivery Math
A chat system stores every message durably and then delivers it to online devices. Presence is ephemeral, but messages and acknowledgements are not.
Assumptions and formulas
- Daily active users: 500M. Average messages/user/day: 40. Average recipient devices: 1.5.
- Online users for presence: 100M (~20% of DAU). Presence heartbeat interval: 30 seconds.
- Recent message cache keeps the latest messages per active conversation.
- Group messages multiply delivery attempts and can create hot conversation partitions.
Concrete math
- Workload breakdown: message writes 231K/s avg (1.15M/s peak); delivery attempts 347K/s avg (231K x 1.5 devices); read receipts ~116K/s avg (~50% of messages); media uploads ~11.6K/s (5% of messages); presence 3.3M heartbeats/s (100M online / 30 sec, aggregate regionally).
- Messages: 500M x 40/day = 20B/day = 231K messages/sec average; 5x peak is about 1.15M/sec.
- Delivery attempts: 231K messages/sec x 1.5 devices = 347K attempts/sec before groups.
- Read receipts: ~50% of messages generate receipts = 231K x 0.5 = ~116K/s average; 5x peak ~575K/s.
- Media uploads: 5% of 231K msg/s = ~11.6K uploads/s; at 500 KB average = ~5.8 GB/s ingress before replication.
- Reconnect storm: 10% of 100M online clients reconnect within 60s = 10M reconnects/60s ≈ 167K reconnects/sec plus gap-repair reads from last-acked sequence.
- Group fan-out: 10K group messages/sec x 100 recipients = 1M deliveries/sec.
- Presence: 100M online users / 30 sec = 3.3M heartbeats/sec if sent centrally; aggregate regionally instead.
- Storage: 20B/day x 500 bytes/message = 10 TB/day raw; 5 years is about 18 PB before compression and retention rules.
- Recent-message cache: 100M active conversations x 50 messages x 500 bytes = about 2.5 TB raw before cache overhead.
Design decisions forced by numbers
- Durable log is source of truth; WebSocket delivery is a transport layer.
- Shard by conversation id, but detect hot large groups and split delivery work.
- Use last-acked sequence for reconnect gap repair and multi-device sync.
Scaling to millions
- At 1M users: one connection registry cluster and conversation-sharded message store are enough.
- At 10M users: add regional routing, offline queues, and read-repair sync APIs.
- At 100M+ users: isolate presence, throttle reconnect storms, and monitor per-conversation hot partitions.
Readable flow
Interview follow-ups to expect
- What is durable and what is ephemeral?
- How do clients recover after reconnect without missing messages?
- How are duplicate sends and duplicate delivery acknowledgements deduplicated?
Operational readiness checklist
After the math, explain how this capacity plan is measured, protected, launched, and evolved.
Observability
- Attach each capacity estimate to a dashboard metric: p99 latency, queue age, cache hit rate, shard skew, error rate, and freshness lag.
- Alert on rate-of-change and saturation, not only hard failures; backlogs and hot keys usually warn before an outage.
Security and abuse
- Budget for auth checks, rate limits, quota isolation, audit logging, privacy filters, and abuse-review side paths.
- Separate trusted internal events from user-controlled input so retries and backfills cannot bypass policy checks.
Degraded modes
- Define the fallback when a dependency is slow: stale cache, partial results, read-only mode, delayed async work, or admission control.
- Protect the invariant first; reduce freshness, personalization, or analytics quality before risking duplicate or corrupt state.
Testing and rollout
- Test average, peak, and hot-object traffic; include retry storms, cold caches, lagging workers, and regional dependency loss.
- Use canaries, shadow reads, dual-write validation, feature flags, and explicit rollback criteria for risky path changes.
Rollout, backfill, and runbooks
- Make migrations and backfills idempotent, resumable, rate-limited, and visible through progress and error metrics.
- Write the runbook before launch: owner, dashboards, paging thresholds, mitigation switches, replay steps, and repair commands.
10x evolution
- For chat delivery math, state which number forces the next move: cache memory, shard count, write amplification, queue lag, egress, or failover reserve.
- Add the next layer only when the metric forces it: sharding, regional reads, async replay, hot-key isolation, or stricter backpressure.
Replay, operations, and 10x growth
Chat interviews turn on recovery after disconnects and retries.
SLOs and metrics
Online send-to-deliver p99 <100ms, sync gap repair p99 <500ms for recent chats, no lost committed messages. Track oldest consumer lag, per-conversation hot partitions, duplicate suppression, reconnect rate, and push failure class.
Degraded modes
If push is down, messages remain durable and clients catch up on open. If presence is stale, hide online badges before blocking sends. If delivery workers lag, send ACK still means stored, not delivered.
Testing and replay
Replay message-ingested logs into a shadow store, fuzz duplicate client_message_id retries, chaos test gateway loss, and load test reconnect storms with jittered client backoff.
10x evolution
Move to regional gateway routing, conversation-sharded sequencers, per-group delivery budgets, device-level sync cursors, hot-conversation isolation, and retention tiers for old attachments.
Hard-part drill: message replay after reconnect
A client reconnects after missing 12 minutes of messages on three conversations. It sends last acknowledged sequence per conversation, the sync service returns ordered gaps before live frames, duplicates are ignored by message_id, and read receipts are replayed only after the local view catches up.
APIs worth naming
WebSocket plus a few REST calls; mention client_message_id and after_seq.
Sketch a WebSocket URL plus a few REST endpoints. Call out client_message_id on send and after_seq on history fetch so reconnects can catch up cleanly.
/connect?token=Persistent session; heartbeats every ~30s/api/v1/messagesSend message (client_message_id, conversation_id)/api/v1/conversations/{id}/messagesHistory after seq cursor/api/v1/conversationsCreate 1:1 or group/api/v1/presenceOnline / last_seen (ephemeral)/api/v1/messages/{id}/readRead receiptMessage bus
Kafka between ingest, storage, delivery, and side effects.
Message bus (Kafka / Pulsar)
Separates "message saved" from "deliver to devices" and side effects.
Publishers
Ingest → Kafka
Services that append to the log after sequencing, never block on push to every group member.
message-ingestedtopicIngest / sequencer
Message assigned a seq and ready for durable work.
Emitted only after validation, sequencing, and the configured durable append. The event must retain the original client-message identity for replay. Partition key = conversation_id so all messages in one chat stay ordered in the log.
delivery-eventstopicStream processor
Recipient is online or needs push.
Downstream delivery workers look up the connection registry and push WebSocket frames or enqueue push notifications.
Consumers (workers)
Kafka → workers
Separate worker pools for storage, delivery, and side effects so one slow pool does not stall the others.
message-ingestedtopicPersistence worker
Append a row to the Cassandra message store.
Commit the Kafka offset only after the history write is durable. Use a stable message key so a crash after the write but before the offset commit repeats the same row safely. Track the applied position so reconnect reads can detect projection lag.
message-ingestedtopicDelivery router
Push to online gateways or an offline queue.
If the recipient is online: frame to their . If offline: hand to push service (batched FCM/APNs).
message-ingestedtopicSide-effect processors
Unread badges, search index, analytics.
These projections can lag without losing the accepted message. Retain their replay positions, enforce authorization for search results, and rebuild incorrect counts from message history. With E2EE, index only permitted metadata rather than unavailable plaintext.
Ordering boundary: hash conversation_id to a so messages from one conversation share an ordered log. Many conversations share each physical partition. Preserve order in processing and sequence assignment as well; routing alone does not prevent consumers from completing side effects out of order.
Data model (core tables)
messages
Store messages ordered by conversation and sequence. For long histories, add bounded time or sequence buckets so one conversation does not create an unbounded physical partition; the sync API crosses buckets in logical order.
conversations
Metadata; membership in group_members.
group_members
Fan-out list for group delivery.
users
Postgres OK for smaller relational profile data.
Caching, media, and bottlenecks
What to cache
- Connection registry: user_id → gateway node (TTL ~5 min)
- Recent messages per conversation (last 50, ~1h TTL)
- User profiles and conversation list on login
- Processed message IDs retained for the supported retry window; durable uniqueness must still protect older retries if the product permits them
Common bottlenecks
- Reconnect storm after outage → client backoff + rate limits
- Large group fan-out → async via Kafka, batch push
- Hot conversation partition → dedicated cache, read replicas
- Media transcoding → async workers, not send path
Beginner vs advanced answers
Beginner (5 min)
WebSockets for online users, messages table, push for offline, group membership table.
Advanced (deep dive)
The deeper design makes the ownership and recovery rules explicit. A conversation owner assigns sequence numbers; durable IDs make retries harmless; a connection registry locates current devices but can be stale. Reconnect uses an ordered sync API to repair gaps, and group delivery fans out under a budget. End-to-end encryption changes search, moderation, key recovery, and multi-device synchronization because the server cannot freely interpret message content.
Choosing and explaining Real-time chat
A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.
Explain the core idea
Durable log first; WebSocket is delivery transport.
Order per conversation_id with monotonic sequence numbers.
Separate ephemeral presence/typing from message storage.
When it belongs
Design WhatsApp, Messenger, Slack, or Discord.
They ask about offline, read receipts, or group size.
They probe message loss vs delivery latency.
How it fails
Duplicate messages when client retries without idempotency key.
Global ordering attempted across all chats (unnecessary).
Synchronous fan-out to 500 group members on send path.
Trade-offs to weigh
Pull vs push for group messages.
E2EE vs server-side search and moderation.
At-least-once + dedupe vs exactly-once complexity.
Common misunderstandings
Storing typing indicators in the message log.
Updating last_seen in Postgres every 30s heartbeat.
No reconnect replay API (users see holes after network blip).
Deeper walkthrough
Chat system deeper walkthrough
Chat is durable conversation logs, online routing, offline replay, push fallback, and ephemeral presence, not WebSockets alone.
Requirement refinements
Clarify 1:1 vs max group size, read receipts, typing, media, edits/deletes, E2EE.
Separate durable messages from ephemeral presence and typing.
API design details
client_message_id on send; getMessagesAfter(conversation_id, last_seq) on reconnect.
WebSocket for real-time; REST for history and uploads.
Data model and storage
Partition messages by conversation_id; monotonic seq per conversation.
Read receipts and delivery state separate from message body rows.
Scaling strategy
Connection registry in Redis; shard gateways horizontally.
Large groups: write once to log, async delivery workers with backpressure.
Reliability, recovery, and operations
Replay from last acked seq on reconnect before showing synced UI.
Monitor send-to-deliver latency, consumer lag, connections per node, push failure rate.
Deepening reference
Chat System: realistic production notes
Beginner mental model: Chat is an ordered message log plus online delivery, offline delivery, and presence.
Production scope: A production chat system must separate durable messages from ephemeral state. Messages, read receipts, media, typing indicators, and presence have different consistency and storage needs.
Choose what the first version must guarantee
Start by deciding which chat system capabilities are essential. Describe the main read and write operations before choosing infrastructure. For each operation, identify the stored records it uses and the condition that makes its result correct. A fast response is useful only if it satisfies that condition.
Next, separate work the user must wait for from work that can finish later. Estimate requests per second, storage growth, and the busiest object or customer. Use those estimates to choose one bottleneck to investigate. The calculations below are example workloads with stated assumptions; they are not measured limits of a particular product.
Concrete capacity calculations
Messages: 500M DAU x 40/day = 20B/day = 231K messages/sec average; 5x peak is about 1.15M/sec.
Delivery attempts: 231K messages/sec x 1.5 devices = 347K attempts/sec before groups. Group fan-out: 10K group messages/sec x 100 recipients = 1M deliveries/sec.
Storage: 20B/day x 500 bytes/message = 10 TB/day raw; 5 years is about 18 PB before compression and retention rules.
Presence: 100M online users / 30 sec = 3.3M heartbeats/sec if sent centrally; aggregate regionally instead.
Connections: 100M concurrent sessions at 100K connections per gateway node needs about 1,000 gateway nodes plus headroom.
Recent-message cache: 100M active conversations x 50 messages x 500 bytes = about 2.5 TB raw before cache overhead.
Scaling to millions of users
Partition durable messages by conversation_id; ordering is per conversation, so avoid global ordering requirements.
Route WebSocket sessions through a connection registry keyed by user_id to current gateway node. Keep registry entries short-lived with heartbeat refresh.
For large groups, write once to the group log and fan out unread markers or notifications asynchronously. Do not synchronously push to every offline member.
Store media in object storage and send only metadata plus signed download references through the message path.
On reconnect, clients send last_seen_sequence and replay missed messages from the conversation log.
Sequence and data-flow diagram
Walk this path after the high-level design so it is clear what is synchronous, what is asynchronous, and where the system can degrade safely.
Read the diagram definition
sequenceDiagram
participant S as Sender
participant G as Chat gateway
participant M as Message service
participant D as Durable message log
participant R as Recipient gateway
participant N as Push worker
S->>G: send message with client_message_id
G->>M: Validate and assign sequence
M->>D: Append message
D-->>M: Stored
alt Recipient online
M->>R: Deliver over active session
else Recipient offline
M->>N: Queue offline notification
end
M-->>S: Ack with server sequenceCore flow to walk through
Send: authenticate, assign conversation sequence number, persist message, publish delivery event.
Deliver online: route through connection registry to the recipient WebSocket server.
Deliver offline: store unread state and use push notifications without exposing sensitive message content.
Production readiness
Chat System: ship, operate, and evolve
Now examine the same request flow during failures and deployments. Tie each operational control to the user behavior it protects.
Observability
Track chat system p95/p99 latency, error rate, saturation, and user-visible freshness separately for read, write, and async paths.
Alert on leading indicators such as queue age, hot-key skew, cache miss storms, reconciliation mismatches, and error-budget burn.
Security and abuse
Enforce authentication, authorization, rate limits, input validation, audit trails, and privacy checks at the service boundary.
Treat abuse controls as a first-class path: throttling, quota isolation, moderation hooks, revocation, and safe handling of sensitive data.
Degraded modes
Name what still works if cache, search, ranking, stream processing, object storage, or a regional dependency is slow.
Prefer stale-but-safe reads, partial results, async retry, admission control, and feature disablement over corrupting core state.
Testing and rollout
Load test average, peak, and hot-object traffic; add failure-injection for retries, timeouts, backlog growth, and dependency outages.
Ship with canaries, shadow reads or dual writes when useful, rollback switches, schema compatibility checks, and replay validation.
Runbooks
Document dashboards, paging thresholds, owner actions, rollback steps, data repair commands, and customer-impact language.
Keep backfills and replays idempotent, resumable, rate-limited, and observable so repair work does not create a second incident.
10x evolution
Say which metric forces the next architecture change: shard count, cache footprint, write amplification, queue lag, p99 latency, or regional failover reserve.
Evolve one bottleneck at a time: partition by access pattern, isolate hot tenants, regionalize reads, add replayable async pipelines, then harden multi-region behavior.
Hard parts worth deepening
Ordering should be scoped to a conversation, not globally across all chats.
Presence is ephemeral and should use TTL heartbeats, not permanent database writes.
Large groups need controlled fan-out and backpressure.
End-to-end encryption changes server capabilities: search, moderation, and previews become constrained.
Failure modes and recovery
Client retries send after timeout; dedup with client_message_id per conversation.
Recipient reconnects after missed messages; replay from last seen sequence number.
Push provider is down; keep unread state durable and retry with backoff.
Large group sends overload fan-out; shard delivery workers by conversation_id.
Signals to monitor
Measure send-to-ack latency at the durable acceptance boundary and send-to-deliver latency at the receiving device. Their difference helps separate storage delay from recipient connectivity or delivery backlog.
Track offline push acceptance separately from actual delivery, since provider acceptance does not prove the device displayed a message. Watch connection count for gateway capacity and replay lag for how long reconnecting devices need to catch up.
| Choice | Best when | Cost |
|---|---|---|
| WebSocket | Bidirectional low-latency messaging is core. | Stateful connection management. |
| Long polling | Infrastructure simplicity matters. | Higher latency and connection churn. |
| Conversation sequence numbers | Per-chat ordering is required. | One hot sequence per very active group. |
Try this scenario
Explain how a mobile client reconnects after being offline for 6 hours and receives exactly the messages it missed.
What must never become false
Once a message is committed to the conversation log with a sequence number, it must not be lost, and retries with the same client_message_id must not create a second committed message. Per-conversation order is required; global order across all chats is not.
Failure timeline: retry without idempotency
T+0 Client sends msg A; gateway times out after Kafka ack but before 200 T+2s Client retries without client_message_id → second commit, seq+1 duplicate text T+10s Recipient sees two bubbles; read receipts attach to wrong seq T+1m Reconnect sync from last_seq still cannot merge the semantic duplicate T+5m Ops "fixes" with delete, but tombstones race with offline devices Outcome At-least-once delivery without idempotency breaks the no-duplicate invariant
Exercises
- 500M DAU × 40 msgs/day ≈ average write QPS; at 5× peak what is message ingest/sec? If groups add 10k msg/s to 100 members, how many deliveries/sec appear?
- Design reconnect: client has last_seq=40, server has 41–45. What API restores gaps before UI claims synced?
- Argue pull vs push for a 500-member group while preserving per-conversation order and backpressure on gateways.
Primary references
Realtime messaging and durable log delivery sources.
Key takeaway
A chat system is an ordered message log per conversation plus delivery to online devices and push + sync when offline. Draw the HLD, separate durable from ephemeral data, walk the happy path (ingest → Kafka → store → gateway push), and explain reconnect with last_seq. Name Cassandra (messages), Redis (registry + presence), Kafka (buffer), and FCM/APNs (offline).
Practice and further reading
Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.
Focus for this lesson: Design conversation ordering, durable delivery, dedup/resume, multi-device sync, membership races, encryption keys, presence, and regional recovery.
Explain the decision
What ordering guarantee is practical for chat?
Read the reasoning
A monotonic sequence within a conversation or partition, not a global order across unrelated conversations.
Apply it to a production scenario
A user sends on phone while offline and edits the same draft on laptop. What resolves?
Read one approach
Assign client operation IDs, order accepted messages server-side, model edits explicitly, and surface conflicts rather than infer a global device clock.
Work through the numbers
Five million concurrent sockets at 30 KiB each consume how much connection memory?
Read the worked calculation
About 143 GiB before TLS, buffers, indexes, and headroom; shard gateways and plan reconnect capacity.
Reason through a failure
What must group membership changes preserve with end-to-end encryption?
Read the recovery reasoning
Removed members cannot decrypt future epochs, new members cannot silently read unauthorized history, and membership/key events are authenticated and ordered.
Sources and further study
Continue with Design Video Streaming.