Design Slack

HardMessaging & CollaborationWebSocketsFan-outSearchPresenceMulti-Tenancy

Problem

Design a team chat product like Slack. Companies create workspaces, and members talk in public and private channels and direct messages. Every message is stored permanently and is searchable by members who can see it. New messages appear in real time for everyone in the channel, including channels with 100,000 members, and users see unread counts and mentions across all their channels. A presence indicator shows who is active. The service hosts 1 million workspaces, 20 million daily active users, and 5 billion messages per day.

  • What is the largest workspace and the largest channel we must support?
  • Do enterprise customers need data residency or their own encryption keys?
  • Should search include file contents and attachments, or only message text?
  • How are threads and reactions handled: as messages, or as separate entities?
  • Must clients work offline and sync on reconnect, and on how many devices per user?
  • What message retention policies can admins configure?

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • Create workspaces with public channels, private channels, and direct messages.
  • Send messages that are delivered in real time to all channel members.
  • Store message history permanently and let members scroll back through it.
  • Search messages the user is allowed to see.
  • Track unread counts and mentions per channel per user.
  • Show presence (active or away) for workspace members.
  • Support threads, reactions, edits, deletes, and file uploads.
  • Let admins set retention policies and export data.

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • 5 billion messages per day is about 60,000 messages per second on average and 200,000 at peak.
  • Delivery latency under 500 ms to online channel members.
  • Average channel fan-out is small, but a 100,000 member announcement channel turns one message into 100,000 deliveries.
  • Strong ordering within a channel, so every member sees the same sequence.
  • Storage: 5 billion messages at 1 KB including metadata and indexes is 5 TB per day, or nearly 2 PB per year before files.
  • Strict tenant isolation: a bug must never show one workspace's data to another.
  • 99.99% availability for sending and reading; search can degrade more gracefully.

Clients load state over a REST API and receive live updates over a WebSocket connected to a gateway. Messages are written to a channel-ordered message store sharded by workspace, then published to a channel fan-out layer that delivers to the gateways where members are connected, while an indexing pipeline adds them to a per-workspace search index. Unread state is tracked as per-user read cursors compared against each channel's latest sequence number, which avoids writing a counter for every member on every message.

Loading diagram...

Startup. The client calls GET /client.boot, which returns the user's channels, each channel's latest sequence number, and the user's read cursor per channel, so the sidebar can show unread markers immediately. The client then opens a WebSocket to a gateway, which registers the user's interest in each of their channels.

Send a message. The client posts POST /channels/:id/messages with a client message ID for idempotency. The message service checks membership, assigns the next sequence number for the channel, writes the message to the message store, and appends an event to the channel event log. It returns the stored message so the sender's client can replace its optimistic copy.

Real-time delivery. The fan-out service consumes the event, looks up which gateways have members of that channel connected, and sends each gateway one copy. Each gateway pushes the event to its local sockets for that channel. Offline members are handled by the notification service, which sends mobile push for mentions and direct messages only.

Unread and mentions. When a user views a channel, the client sends POST /channels/:id/mark with the latest sequence it displayed. Unread is simply channel.latest_seq > cursor.last_read_seq. Mentions are counted separately because they need exact numbers.

Search. The indexer consumes the same event log and writes to the workspace's search index. A query runs against that index and filters results to channels the user belongs to.

Holds WebSocket connections and delivers channel events to connected members.

  • Java or Go gateway servers, each holding tens of thousands of connections.
  • A consistent-hash ring of channel servers that track which gateways care about each channel.

Diagram

Loading diagram...

Scalability

Each channel is owned by one channel server on the hash ring, which keeps an in-memory list of subscribed gateways. A 100,000 member channel might span 500 gateways, so one message costs 500 internal sends, and each gateway multiplies to its local sockets. Gateways subscribe lazily, only for channels with at least one connected member, so idle channels cost nothing.

Tradeoffs

Pushing to gateways (fan-out on write) gives low latency, which chat needs. The cost is bursty load on very large channels, so huge channels get rate limits on posting and gateways coalesce events. Per-user fan-out (writing every message to every member's inbox) was rejected: it multiplies storage by channel size and is unnecessary because history is read per channel.

Validates, orders, and durably stores messages, threads, reactions, and edits.

  • MySQL sharded by workspace ID through Vitess, which matches how almost all queries are scoped.
  • Kafka as the channel event log, partitioned by channel ID to preserve order.

API

  • POST /channels/:id/messages with { text, clientMsgId, threadTs? } returns the stored message with its sequence number.
  • GET /channels/:id/history?before=&limit= returns a page of messages.
  • PATCH /messages/:id and DELETE /messages/:id edit or delete.
  • POST /messages/:id/reactions with { emoji }.

Data Model

  • messages: workspace_id, channel_id, seq (primary key is channel_id, seq), message_id, user_id, text, thread_root_seq, edited_at, deleted_at.
  • channels: channel_id, workspace_id, name, is_private, latest_seq.
  • channel_members: channel_id, user_id, joined_at.

Scalability

Sharding by workspace keeps tenant data together and makes isolation easy to reason about. The largest enterprise workspaces get dedicated shards, and a very large workspace can be split further by channel ID. Old history moves to cheaper storage tiers but stays readable.

Tradeoffs

A relational store gives transactions for sequence assignment and membership checks, and Vitess handles resharding. A wide-column store would scale writes more easily but makes features like edits, threads, and admin exports harder. Assigning sequence numbers per channel (not globally) keeps ordering cheap.

Builds a full-text index of messages and files that respects channel permissions.

  • Elasticsearch or OpenSearch, with indexes grouped by workspace.

Scalability

Small workspaces share index shards with routing by workspace ID, while large ones get dedicated indexes. Indexing lags writes by a few seconds, which users accept.

Tradeoffs

Permissions are applied at query time by filtering on the user's channel IDs rather than indexing an ACL per message, because membership changes constantly and reindexing history on every join would be prohibitive. Users in thousands of channels are handled by caching their channel set per query session.

Tracks how far each user has read in each channel and how many mentions are pending.

  • A sharded key-value store (Redis with persistence, or a MySQL table cached in Redis).

Data Model

  • read_cursors: user_id, channel_id, last_read_seq, mention_count.

Tradeoffs

Comparing a cursor with the channel's latest sequence makes unread a cheap read with no write per message per member. Exact unread counts are computed only when needed (a range count in the message store), which is why many chat apps show a bold channel instead of a number for regular messages.

Tracks whether users are active and pushes changes to people who can see them.

  • An in-memory, sharded store keyed by user ID, fed by gateway heartbeats.

Scalability

Broadcasting every presence change to a whole workspace is quadratic. Clients instead subscribe only to users currently visible on screen (the open channel's members, the direct message list), and the service batches updates every few seconds.

Tradeoffs

Presence is soft state that can be rebuilt from heartbeats after a failure, so it is kept out of the durable store. Accepting a few seconds of staleness saves a large amount of traffic.

Related questions