Design Approach: Build MVP First, Then Scale
This guide is structured as:
1) MVP (single-node, minimal working)
2) Scaling Steps 1 / 2 / 3 (Gradually add Kafka, Redis, DB sharding, and HA)
3) Entity / Data Model (核心表设计 + 关键字段)
4) End-to-End Flows (send/receive/offline/sync)
5) Tradeoffs & Interview Q&A (背诵版)
1) MVP: The Smallest Working Chat System (Single Instance)
MVP Goals
- 1:1 messaging
- Basic message history
- Basic online delivery (best effort)
- No Kafka / no Redis / no multi-gateway
MVP Architecture
- Client (Web + Mobile)
- Single Chat Server (includes WebSocket + REST)
- Single DB (Postgres / MySQL) for users + conversations + messages
MVP Endpoints
WebSocket
WS /connect(auth during handshake)sendMessage(client → server)message(server → client push)
HTTP (REST)
POST /loginGET /conversationsGET /messages?conversationId&after=cursor
MVP Flow (Online)
- Client connects via WebSocket and authenticates
- Client sends message over WS
- Server writes message into DB
- Server pushes message to recipient if recipient is connected to the same server
MVP Flow (Offline)
- Sender sends message
- Server persists message to DB
- Receiver later opens app and pulls history via REST
MVP 关键:先保证能用(正确性),延迟/HA/极致扩展后面再加。
2) Scale Step 1: Split REST vs WebSocket + Stateless Chat Service
Why
- REST 是短请求高 QPS
- WebSocket 是长连接高并发连接数
- 分开才能分别扩缩和调超时
Add Components
- API LB + API Gateway (REST)
- WS LB + WS Gateway (WebSocket)
- Chat Service (stateless: validate + enqueue-only / or validate + write DB in early stage)
At this stage, you can still keep direct DB write (no Kafka yet) but split responsibilities.
3) Scale Step 2: Add Presence Store (Redis) for Multi-Gateway Routing
Problem
Once you have multiple WS Gateways, you need to know:
- user is online?
- connected to which gateway?
Add Component
- Presence / Session Store (Redis)
userId → gatewayIdlastHeartbeat- TTL-based cleanup
Result
- Delivery component can route to the correct WS gateway.
- Gateway crash → reconnect → mapping rebuilt.
4) Scale Step 3: Add Kafka for Decoupling + Ordering + Backpressure
Why Kafka
- Decouple ingestion (send path) from delivery (push path)
- Smooth spikes (buffering)
- Enable retry/replay
- Ordering guarantee per conversation via partitioning
Add Components
- Message Queue (Kafka): partition by
conversationId - Delivery / Router (Kafka Consumer):
- persist to DB (source of truth)
- lookup Redis presence
- push to WS gateway via internal RPC
Reliability Model
- Delivery is at-least-once
- Dedup via messageId idempotency
5) Scale Step 4 (Optional Advanced): HA, Sharding, Multi-Region
HA
- WS Gateway multi-AZ behind LB
- Redis cluster/replication
- Kafka replication
- DB replication + backups
Sharding
- Cassandra/DynamoDB partition by
conversationId - Avoid hot partitions for large groups via bucketing
Multi-Region (if asked)
- Active-active with per-region Kafka + DB replication
- Read local, write with conflict strategy (complex)
Usually mention as extension unless interviewer pushes.
6) Entity Design (Hardcore, Interview-Ready)
Below is a practical data model that supports:
- conversation list UI
- message history
- ordering
- unread counts
- multi-device sync
6.1 Core Entities
User
user_id(PK)usernamecreated_atstatus(optional: active/blocked)
Conversation
conversation_id(PK)-
type(DMGROUP) created_atcreated_bytitle(group only)
ConversationMember
conversation_id(PK part)user_id(PK part)-
role(memberadmin) joined_atlast_read_seq(for read/unread & sync)muted_until(optional)
Message
conversation_id(partition key)seq(clustering key / increasing per conversation)message_id(global idempotency key)sender_idcontentcreated_attype(text, future: image/file)edited_at(optional)deleted(optional)
Key point: Ordering is by
(conversation_id, seq)not by client timestamp.
6.2 Recommended Storage Layout (Cassandra/Dynamo-style)
Table: MessagesByConversation (source of truth)
- PK:
(conversation_id) - CK:
(seq)or(created_at, message_id) - Columns:
message_id, sender_id, content, created_at, ...
Table: ConversationsByUser (inbox UI)
- PK:
(user_id) - CK:
(last_activity_time DESC, conversation_id) - Columns:
last_message_preview, unread_count, pinned, ...
Table: MembersByConversation
- PK:
(conversation_id) - CK:
(user_id) - Columns:
role, joined_at, last_read_seq, ...
Idempotency / Dedup (optional)
- PK:
(message_id) - Columns:
conversation_id, seq, status, created_at - TTL optional
Used to ensure retries don’t create duplicates.
6.3 Presence / Session Store (Redis)
Key Patterns
presence:{userId} -> gatewayId(TTL)heartbeat:{userId} -> timestamp(TTL)- Optional:
inbox:{userId} -> [messageId...](short-term cache)unread:{conversationId}:{userId} -> count
Redis is acceleration only, not correctness.
7) End-to-End Flows (Aligned with Entities)
7.1 Send Message (Online)
- Client sends
sendMessage(conversationId, messageId, content)over WS - WS Gateway forwards to Chat Service (internal API)
- Chat Service validates membership (ConversationMember) + rate limit
- Chat Service enqueues to Kafka (partition by conversationId)
- Delivery Router consumes:
- allocate
seq(monotonic per conversation) and persistMessage - update
ConversationsByUserfor sender/receiver (last_activity, preview) - lookup Redis presence for receiver
- push to receiver’s WS gateway
- allocate
7.2 Receive Message (Push)
- Router → WS Gateway: internal RPC
push(message) - WS Gateway → Client: WebSocket
messageevent
7.3 Offline Delivery + Recovery
- If receiver not online (no presence mapping):
- still persist to DB
- optionally update Redis inbox/unread cache
- optionally trigger push notification
- On reconnect:
- client sends
last_read_seqper conversation - server returns messages where
seq > last_read_seq
- client sends
7.4 Multi-Device Sync
- Each device stores per conversation
last_read_seq - On reconnect / app open:
- pull deltas from
MessagesByConversation(conversationId, seq > last_read_seq)
- pull deltas from
8) Interview “Layered” Explanation (Memorize)
8.1 MVP pitch (10s)
Start with a single chat server + DB: authenticate, persist messages, push if online, otherwise clients fetch history.
8.2 Scale pitch (20s)
Then split REST and WS gateways, add Redis presence for routing, add Kafka to decouple send/deliver and guarantee per-conversation ordering, store messages in Cassandra as the source of truth.
8.3 Correctness pitch (10s)
Cassandra is durable source of truth; Kafka provides buffering/replay; Redis accelerates presence and unread state; delivery is at-least-once with messageId idempotency.
9) Quick Q&A (Hardcore)
When is it persisted?
When written to Cassandra/DB as source of truth.
Gateway crash: message loss?
No. Client reconnects; presence rebuilt; missing messages pulled from DB.
How does router find user?
Redis
userId → gatewayIdmapping with heartbeat TTL.
How do you guarantee ordering?
Kafka partition by conversationId + per-conversation seq in DB.
Why not exactly-once?
Too complex; at-least-once + idempotency is the practical standard.
Notes (How to draw on whiteboard)
- Start with MVP boxes (Client → Chat Server → DB)
- Then evolve to: API LB/GW and WS LB/GW
- Add Redis presence mapping
- Add Kafka + Delivery Router
- Finally annotate: ordering (partition), idempotency (messageId), source of truth (DB)