Design problems
Messaging & collaborationHardSDE-2 35 min

Design Slack (team chat)

Asked at
AtlassianFreshworksGoogleMicrosoftSalesforceZoho

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

Persistent WebSocket gatewaysChannel servers with gateway subscriptions (two-level fan-out)Message keyed by (channel_id, ts)Sharding by workspace vs channelLast-read pointer per user per channelIdempotent send with client_msg_idAsync indexing via KafkaPermission-filtered searchMulti-tenant isolationReconnect storms and lazy loading

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

Messages sent~1B/day (~12k/s average, ~50k/s peak)20M DAU × ~50 messages/day; work hours concentrate traffic ×4
Real-time deliveries~1M/s peakAverage channel has ~20 online members per message → 50k/s × 20; big channels add bursts
Concurrent connections~15M20M DAU, many with desktop + phone online during work hours; ~100k connections per gateway → ~150-200 gateways
Message storage~1 TB/day, ~400 TB/year1B × ~1 KB (text + metadata + reactions + indexes); replicated 3x
Search index~1.5x raw textInverted index plus stored fields; per-workspace shards
Read API (history, channel switch)~200k/s peakEach channel switch loads the latest 50 messages; ~10 switches per user-hour

3 API

EndpointWhat it does
WS/rtm/connectReal-time stream: new messages, edits, reactions, typing, presence, read markers for the user's channels
POST/v1/channels/{channel_id}/messagesSend message {client_msg_id, text, thread_ts?, file_ids?}; returns message with server ts
GET/v1/channels/{channel_id}/messages?before=&limit=50Paginated history (cursor on message ts)
POST/v1/channels/{channel_id}/readMark 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/countsUnread and mention counts for all channels at startup

4 Data model

messagesSharded MySQL/Postgres (Vitess) or Cassandra
channel_idts (time-ordered ID)message_idauthor_idtextthread_root_tsedited_atdeletedclient_msg_id

Primary 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 SQL
channel_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 cache
user_idchannel_idlast_read_tsmention_count

One row per user-channel pair; unread = messages with ts > last_read_ts. Never a flag per message per user.

search indexElasticsearch / Solr
workspace_idchannel_idtsauthortextfile names

Routed by workspace_id; every query filtered by channels the user is a member of.

filesS3 + metadata in SQL
file_idworkspace_iduploadernamesizemimestorage_keypreview_keys

Accessed through signed short-lived URLs after permission check.

channel subscriptionsIn-memory on channel servers
channel_id → set of gateway_ids with online members

Rebuilt from gateways on restart; consistent hashing assigns channels to channel servers.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALsend, historyWebSocketinsert (channel, ts)publishto subscribed gatewayseventsmessage.createdindexmentions, offlinecountssigned URL uploadDesktop / mobile / webAPI gatewayReal-time gateways(WS)Message serviceSearch serviceChannel servers(fan-out)Kafka (message events)Indexer + notifierjobsMessages DB (Vitess)Redis (read state,cache)File storage (S3)Search index (ES)APNs / FCM / email
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Send a message to a channel

  1. 1Client sends POST with client_msg_id and shows the message immediately in a pending state
  2. 2Message service checks the user is a member of the channel and the channel is not archived or read-only
  3. 3It assigns a time-ordered ts (unique within the channel) and inserts into the messages table; duplicate client_msg_id returns the existing message
  4. 4It publishes the event to the channel server owning that channel (consistent hashing on channel_id)
  5. 5The channel server sends one copy to each gateway that has subscribed members online; gateways push to each local connection
  6. 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

  1. 1Client connects to a gateway; the gateway loads the user's channel list and subscribes to each channel's channel server
  2. 2Client calls /counts to get unread and mention badges for all channels from Redis-backed read state
  3. 3For the channel the user opens, it fetches the latest 50 messages; for others it fetches lazily
  4. 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

  1. 1Desktop sends /read with last_read_ts for the channel
  2. 2Service updates the read state row and cache
  3. 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

Q1How do you implement shared channels between two different companies?
Q2How does a client catch up after being offline for a week?
Q3How would you add message retention policies and legal hold?
Q4How would you design threads so that thread replies notify participants only?
Q5How do you keep typing indicators and presence cheap?
Q6How would you add an AI summary of unread messages?

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.

Start free