Design Slack (team chat)
The interviewer asks
“Design a team chat product like Slack: workspaces with channels, direct messages, threads, mentions, unread counts, search across message history, and file sharing, used by companies from 5 to 200,000 employees.”
What a strong answer looks like
Slack differs from WhatsApp in three ways that drive the design: the server keeps full message history (searchable, synced to every device), it is multi-tenant with workspace-level isolation, and channels can be very large (100k members in an enterprise announcement channel). A strong answer shards message storage by workspace or channel, writes each message once per channel (not per member), and uses a real-time layer where gateways subscribe to the channels their connected users belong to. Unread state is per user per channel as a last-read pointer, not per-message flags. Search is a per-workspace inverted index fed by change data capture. Ordering per channel, idempotent sends, permission checks on every read, and handling a reconnect storm after Monday 9 am are the deep-dive topics.
Concepts to mention
1 Requirements
Functional
- Workspaces with public and private channels, and direct messages (1:1 and group DM up to 9)
- Send, edit, delete messages; threads (replies to a message); reactions
- Real-time delivery to all online members and all their devices
- Unread counts and mention badges per channel, synced across devices
- Full-text search over messages and files the user has access to
- File uploads with previews; notifications (push, email digest) for mentions
Non-functional
- 20M daily active users across ~500k workspaces; largest workspace 200k members
- Message delivery to online recipients p99 under 300 ms
- Message history durable forever (or by retention policy); no loss after ack
- 99.99% availability for send and receive
- Strict tenant isolation: no cross-workspace data leaks
- Search results p95 under 1 s; new messages searchable within ~10 s
Out of scope (say it out loud)
- Huddles (voice/video calls)
- Apps marketplace and workflow builder internals
- Enterprise key management (mention only)
- Billing
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
WS/rtm/connect | Real-time stream: new messages, edits, reactions, typing, presence, read markers for the user's channels |
POST/v1/channels/{channel_id}/messages | Send message {client_msg_id, text, thread_ts?, file_ids?}; returns message with server ts |
GET/v1/channels/{channel_id}/messages?before=&limit=50 | Paginated history (cursor on message ts) |
POST/v1/channels/{channel_id}/read | Mark read up to message ts; syncs unread state to the user's other devices |
GET/v1/search?q=&in=&from=&after= | Search messages and files the caller can access |
PUT/v1/messages/{id} | Edit a message (broadcast as an update event) |
GET/v1/users/me/counts | Unread and mention counts for all channels at startup |
4 Data model
messagesSharded MySQL/Postgres (Vitess) or Cassandrachannel_idts (time-ordered ID)message_idauthor_idtextthread_root_tsedited_atdeletedclient_msg_idPrimary key (channel_id, ts) so history is one range scan. Sharded by workspace_id for small tenants and by channel_id for huge ones; Slack itself moved to Vitess for this flexibility.
channels and membershipSharded SQLchannel_idworkspace_idtypename; membership (channel_id, user_id, role, joined_at)Index both ways: user → channels (for connect) and channel → members (for fan-out and permission checks).
read stateSharded SQL + Redis cacheuser_idchannel_idlast_read_tsmention_countOne row per user-channel pair; unread = messages with ts > last_read_ts. Never a flag per message per user.
search indexElasticsearch / Solrworkspace_idchannel_idtsauthortextfile namesRouted by workspace_id; every query filtered by channels the user is a member of.
filesS3 + metadata in SQLfile_idworkspace_iduploadernamesizemimestorage_keypreview_keysAccessed through signed short-lived URLs after permission check.
channel subscriptionsIn-memory on channel serverschannel_id → set of gateway_ids with online membersRebuilt from gateways on restart; consistent hashing assigns channels to channel servers.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Send a message to a channel
- 1Client sends POST with client_msg_id and shows the message immediately in a pending state
- 2Message service checks the user is a member of the channel and the channel is not archived or read-only
- 3It assigns a time-ordered ts (unique within the channel) and inserts into the messages table; duplicate client_msg_id returns the existing message
- 4It publishes the event to the channel server owning that channel (consistent hashing on channel_id)
- 5The channel server sends one copy to each gateway that has subscribed members online; gateways push to each local connection
- 6Message service also emits message.created to Kafka; workers index it for search, increment mention counts, and schedule push notifications for mentioned users who are offline or idle
Open the app on Monday morning
- 1Client connects to a gateway; the gateway loads the user's channel list and subscribes to each channel's channel server
- 2Client calls /counts to get unread and mention badges for all channels from Redis-backed read state
- 3For the channel the user opens, it fetches the latest 50 messages; for others it fetches lazily
- 4Client tracks the last event it saw; on short reconnects it asks for events since that point instead of reloading everything
Read on desktop, badge clears on phone
- 1Desktop sends /read with last_read_ts for the channel
- 2Service updates the read state row and cache
- 3It publishes a read-marker event to the user's own other connections, so the phone clears its badge
7 Deep dives
Real-time fan-out through channel servers
Each message must reach every online member of the channel on every device. Fan-out per user (like a timeline) is wasteful here because channels are shared spaces with persisted history. Instead, Slack-style systems have stateful channel servers: channel IDs are assigned to channel servers by consistent hashing, and each channel server keeps, for its channels, the set of gateways that currently have a subscribed member. When a user connects, their gateway subscribes to all their channels (a user can be in hundreds, so subscriptions are batched). Sending a message is then one publish to the owning channel server, which forwards once per subscribed gateway, and each gateway pushes to its local connections. For a 100k-member announcement channel with 60k online, that is a few hundred gateway messages, not 60k RPCs. Channel servers keep only soft state; if one dies, its channels are re-hashed and gateways resubscribe. Slack's real architecture has this split of gateway servers and channel servers, plus admin and presence servers, and gateway regions close to users with the channel servers in the core region.
Message storage and sharding for a multi-tenant product
History reads are the main pattern: 'latest 50 messages in channel X', then page backwards. So the primary key is (channel_id, ts), which makes it a single range scan. Sharding by workspace_id is natural for tenant isolation, backups and data residency, and keeps all of a company's data together for exports and compliance. But a huge enterprise workspace then becomes a hot shard. A better long-term choice is sharding by channel_id with a routing layer (Vitess with a lookup vindex), so big workspaces spread across shards while small ones are still cheap. The ts is a per-channel ordered identifier; generating it in the database row of the channel, or using a Snowflake-like ID with a tie-break, gives a stable total order per channel, which is the only order users see. Edits and deletes are updates to the row plus an event; deletes may be soft for retention and legal hold. Enterprise retention policies (delete after 90 days) run as background jobs per workspace. Thread replies are messages with thread_root_ts set, plus a reply count and participants list on the root to render 'N replies'.
Unread counts and mentions at scale
Storing a read/unread flag per message per user would mean a write for every member on every message, which explodes for big channels. Instead store a last_read_ts per (user, channel). Whether a channel has unread messages is simply 'latest message ts > last_read_ts', which is cheap if you cache each channel's latest ts. Exact unread counts are harder; most products show a bold channel name and only count mentions exactly. Mentions are rare relative to messages, so incrementing mention_count for each mentioned user (including @here and @channel fan-out, which you must rate-limit in big channels) is affordable; it resets when the user reads up to that point. At startup the client loads counts for all its channels in one call from Redis. Read markers sync across devices through the user's own real-time connections. This 'pointer, not flags' approach is the main insight interviewers look for in any chat or inbox design.
Permission-aware search
Search must never show a message from a private channel the user is not in, and results change when users join or leave channels. Index messages in Elasticsearch routed by workspace_id so a query only touches that tenant's shards. At query time, fetch the user's channel IDs (hundreds, maybe thousands) and add them as a terms filter, so the filter is enforced inside the search engine rather than after fetching results, which avoids empty pages and leaks. For users in many thousands of channels, split into public channels (filter by 'is_public and workspace') plus the private list. Index via Kafka consumers so indexing never slows down sending; target under 10 s lag. Edits re-index the document; deletes remove it. Ranking mixes text relevance with recency and how close the author or channel is to the searcher. Large enterprises may require their own index cluster or encryption with their own keys, which tenant routing makes possible.
8 Trade-offs
Server-side history vs device-only storage
Unlike WhatsApp, Slack stores everything on the server so any device sees full history and search works. The cost is that the server can read content, so security relies on encryption at rest, access controls and optional customer-managed keys rather than E2E encryption.
Shard by workspace vs by channel
Workspace sharding gives isolation and easy per-tenant operations but creates hot shards for giant tenants. Channel sharding spreads load but makes workspace-wide operations multi-shard. A routing layer that supports both (small tenants together, big ones spread) is the practical answer.
Exact unread counts vs bold indicator
Exact counts per channel need counting messages after a pointer, which is costly for busy channels. Showing 'has unread' plus exact mention counts gives users what they need cheaply.
Stateful channel servers vs a generic pub/sub (Redis, Kafka)
Redis pub/sub works for an MVP. At large scale, custom channel servers give control over subscription batching, ordering and backpressure. Kafka is still used for durable async work like indexing and notifications.
9 Bottlenecks
Monday 9 am reconnect storm
Millions of clients connect and each subscribes to hundreds of channels and loads counts. Use jittered connection, lazy loading (only the open channel's history), cached counts, and batched subscriptions. Slack built an edge cache (Flannel) of workspace data near users to cut this boot cost.
@channel in a 100k-member channel
One message triggers 100k notifications. Restrict @channel in large channels to admins, queue notifications through workers with rate control, and collapse multiple mentions into one push.
Hot shard from one huge customer
Move the tenant's busiest channels to dedicated shards via the routing layer; isolate their search index.
10 Failure handling
Channel server dies
Consistent hashing moves its channels to other servers; gateways detect it and resubscribe. Clients may miss a few seconds of live events, so they fetch messages since their last seen ts after resubscribe. The database still has everything.
Search indexer lag
Messages are still sent and shown; only search is stale. Alert on consumer lag; the UI can show 'recent messages may not appear yet'.
Database shard failover
Sends to channels on that shard fail for seconds; clients keep messages in pending state and retry with the same client_msg_id, so no duplicates after recovery.
11 What impresses
- Contrasting with WhatsApp: server history, multi-tenancy, big channels
- Unread state as a pointer, not per-message flags
- Explaining why fan-out goes to gateways, not users
- Search permission filtering inside the engine
- Planning for the Monday-morning connect storm
- Noting enterprise needs: retention, legal hold, data residency, customer-managed keys
12 Mistakes
- Copying messages into every member's inbox for channels
- Storing a read flag per message per user
- Filtering search results after fetching them, leaking counts or returning empty pages
- No idempotency on send, causing duplicates on retry
- Treating all workspaces as the same size
- Ignoring multi-device sync of read state
13 Follow-ups
Lessons this design uses
Practice questions
- How do you load-balance WebSocket connections, and what is different compared to normal HTTP requests?
- In your group chat, users sometimes see messages in different orders, and some see the same message twice after a reconnect. How do you guarantee ordering and no duplicates?
- A user's phone was offline for 2 hours. When it reconnects, how does the client catch up on missed updates efficiently, and what if it was offline for 2 weeks?
- You deploy a new version of your WebSocket gateway, and every deploy causes a spike of errors and a CPU storm. How do you deploy stateful connection servers safely?
- In your team chat product, a company-wide announcement channel has 100,000 members. When someone posts, how do you deliver it without hurting the rest of the system?
- Your chat product runs in Mumbai and Singapore regions for latency. A user in India and a user in Singapore are in the same conversation. How do messages flow, and how do you keep ordering correct?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.