HomeBlog › Design Slack

Design Slack

A workspace collaboration system design walkthrough: channels and membership, fan-out to a 10,000-member channel, per-user unread state, threads, connection management, message storage, and search across years of history.

"Design Slack" sounds like a chat question, and candidates lose points by answering it as one. Designing a generic chat app is about 1:1 conversations; designing WhatsApp adds mobile delivery semantics and end-to-end encryption. Slack is a different product category: a workspace where an entire organisation talks in long-lived channels, and where the interesting engineering sits in channel fan-out, per-user read state, threading, and searching years of accumulated history.

Everything below is a reasonable design an interviewer would expect, not a description of Slack's actual internals. Where an approach is publicly known in the industry, it is called out as such.

   +-----------+   WebSocket   +-------------------+
   |  Desktop  | ============> |  Connection Tier  |
   |  / Web    |               |  (subscriptions)  |
   +-----------+               +---------+---------+
                                         |
                              +----------+----------+
                              |   Channel Service   |
                              | membership + order  |
                              +--+-------+--------+-+
                                 |       |        |
                +----------------+       |        +----------------+
                v                        v                         v
      +-------------------+   +--------------------+   +--------------------+
      |  Message Store    |   |  Read-State Store  |   |  Index Pipeline    |
      | channel log, seq  |   | cursors + mentions |   | -> search cluster  |
      +-------------------+   +--------------------+   +--------------------+
                                         ^
                                +--------+--------+
                                | Presence + Apps |
                                | webhooks, bots  |
                                +-----------------+
A workspace collaboration architecture: clients hold subscriptions on a connection tier; a channel service appends to a shared per-channel log, updates per-user read cursors, and feeds an asynchronous search index. Integrations post through the same write path.

1. Clarify the requirements

Functional requirements

Non-functional requirements

Back-of-the-envelope scale. Take 20M daily active users each sending ~50 messages a day: about 1B messages/day, ~12K writes/sec average and maybe 5× that at the European and US morning peaks. Reads dominate — channel loads, scrollback, and search easily produce 20–50× the write volume. At ~500 bytes per message that is ~500 GB/day of text before files, so history storage is the long-term cost driver and files go to blob storage.

2. API and protocol

Channel history, search, and administration are ordinary HTTPS requests. Only live delivery runs over a socket, and the socket carries subscriptions rather than raw user-to-user routing.

# HTTP
POST /v1/channels/{cid}/messages   { text, threadParentId?, clientMsgId }
GET  /v1/channels/{cid}/history    ?before=seq&limit=50
POST /v1/channels/{cid}/read       { lastSeenSeq }
GET  /v1/search                    ?q=deploy+rollback&in=cid&from=uid

# WebSocket events
-> SUBSCRIBE  { channelIds: [...] }
<- MESSAGE    { cid, seq, userId, text, threadParentId? }
<- UNREAD     { cid, unreadCount, mentionCount }
<- PRESENCE   { userId, state: "active" | "away" }

3. High-level architecture

Clients hold a persistent connection to a connection tier whose job is subscription management: which sockets care about which channels. A session registry in Redis maps userId → connectionId → gateway, and a reverse map gives channelId → connected members — the single most important structure in the design, because it is what turns a 10,000-member channel into a manageable push.

A send goes to the channel service, which authorises the write against membership, assigns a per-channel sequence number, appends to the message store, and emits an event. The connection tier pushes that event to subscribed sockets, the read-state service updates unread counters, and the indexing pipeline makes the message searchable a moment later.

4. The channel fan-out problem

Someone posts in #engineering with 10,000 members. The naive design writes 10,000 rows — one per member's inbox — and does not survive a workspace where several such channels are busy at once.

What works is write once, read many: append the message a single time to the channel's ordered log, then split delivery in two.

This is the read-heavy fan-out choice, the mirror image of fan-out-on-write for social feeds, and it works here precisely because channel membership is bounded by an organisation rather than a follower graph. The counterpart problem is the @channel storm: an @-mention to all 10,000 members must still update 10,000 badge states. Handle that asynchronously through a queue, batching counters and pushing them to connected clients as they are computed, rather than synchronously on the sender's request.

5. Unread counts and read state

Unread state is per user per channel, which makes it the largest-cardinality data in the system — users × channels, not messages.

Model it as a cursor: for each (userId, channelId) store lastSeenSeq plus a small mentionCount. The unread badge is latestSeq - lastSeenSeq, computable without scanning messages because the channel already tracks its latest sequence number. Mentions are counted separately, since a badge for a direct @-mention carries more weight than a generic unread dot and users expect it to be exact.

The cursor table lives in a fast key-value store keyed by user, so the client's boot request — every channel and its unread state — is a single partition read. When the user reads a channel the client sends a mark-read event; the write is idempotent (cursors only move forward) so replays are harmless.

6. Threads and the message data model

Threading falls out of one nullable column. Every message row carries an optional thread_parent_id: NULL means a top-level channel message, non-null means a reply.

The channel view queries only thread_parent_id IS NULL, so replies never clutter the main timeline, and the parent row denormalises reply_count and last_reply_at so the thread summary renders without touching replies at all. Partition the message store by channel_id: history stays co-located, ordering within a partition is trivial, and workspace isolation follows from the key prefix.

7. Connection management at scale

Millions of desktop clients hold open sockets, and each is subscribed to a slice of its workspace's channels. Three details matter:

8. Search and integrations

Search cannot run against the primary store — the access pattern is the opposite of the write path. Message writes are published to a stream that an indexing pipeline consumes, building an inverted index in a search cluster partitioned by workspace. Every query is filtered by the requester's channel memberships before results return, so a private channel can never leak. Indexing is asynchronous, so a message is searchable a second or two after it is sent — an acceptable trade, worth naming as one.

Files go to blob storage; only metadata and extracted text enter the index. Integrations — incoming webhooks, bots, slash commands — post through the same authorised write path as a human message, with per-app rate limits and a scoped token, so the rest of the pipeline stays uniform.

Key trade-offs the interviewer probes

Framework reminder: every system design answer follows the same arc — requirements → estimates → API → high-level design → data model → scale → trade-offs. Keep the system design cheat sheet in mind and narrate which stage you're in.

Practice collaboration-scale design with live AI support

CoPilot Interview surfaces a structured design skeleton — requirements, API, data model, and scaling — in about 4 seconds during real Zoom and Teams calls. Free for Windows and macOS, with a private desktop window.

Try it free

FAQ

How is designing Slack different from designing WhatsApp or a generic chat app?

A generic chat app is mostly about 1:1 conversations, and WhatsApp adds mobile delivery and end-to-end encryption. Slack is a workspace product: everything is scoped to an organisation, conversation happens in long-lived public and private channels rather than pairwise threads, and the hard problems are channel fan-out at team scale, per-user unread state, threading, and searching years of history. Interviewers expect you to name that difference early.

How do you fan out a message to a 10,000-member channel?

Write the message once to the channel's log, then push it only to members who currently have an open connection and are subscribed to that channel, which is usually a small fraction of the membership. Everyone else picks the message up on their next fetch. Writing one copy per member into 10,000 inboxes is the naive answer and it does not scale, so a shared channel log plus a per-user cursor is the design an interviewer is looking for.

How do you model unread counts per user per channel?

Store a read cursor for each user and channel: the sequence number of the last message that user has seen. The unread count is then the number of messages in the channel with a higher sequence number, which can be computed on read or kept as a cached counter. Mentions are tracked separately because a badge for an at-mention matters more than a generic unread, and both are updated when the client sends a mark-read event.

How should threads be modelled in the data layer?

Give every message an optional parent identifier. A message with no parent is a top-level channel message; a message with a parent is a reply belonging to that parent's thread. The parent row keeps a reply count and last-reply timestamp so the channel view can render the thread summary without loading replies, and replies are fetched only when the user opens the thread.

How does search work across years of message history?

Message writes are published to a stream that an indexing pipeline consumes, building an inverted index in a search cluster partitioned by workspace. Queries hit that index rather than the primary message store, and every result is filtered by the channels the requesting user is actually a member of, so a private channel never leaks into someone else's results. Indexing is asynchronous, so a new message becomes searchable a moment after it is sent.