System Design · step by step

Design Slack

Step 1 / 9

In the interview room

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

Functional requirements

What it must do — agree on these before drawing a single box.

  • Post to a channel: send a message to a channel (not a person); persist it to per-channel history.
  • Deliver live: every member sees the message instantly over a WebSocket, across all devices.
  • Presence & typing: show who’s online and “Bob is typing…” without touching durable storage.
  • Search: find old messages by keyword, scoped to channels the user can access.
  • Notify & unread: push mentions/DMs to offline users and show accurate unread counts everywhere.

Non-functional requirements

The qualities that shape the whole design — each one names the mechanism that buys it.

History is never lost
The Channel Service persists each message to a per-channel Message Store before delivery — persist first, deliver second — so latecomers and other devices get a complete record.
Feels instant
Clients hold a persistent WebSocket and a Session Registry maps user→server, so “deliver to user U” becomes “push on this socket on this server”.
Reach members on any server
Each accepted message is published once to a pub/sub Message Bus; every gateway subscribed to the channel pushes to its local sockets — O(servers), not O(servers²).
A typing storm can’t risk messages
Presence and typing run on a separate ephemeral path — in-memory, broadcast, never persisted — so high-frequency signals never touch the durable store.
Find anything, see only what you may
Messages are indexed into a Search Index at write time and queries hit the index scoped by channel permissions, not the raw store.
Consistent unread across devices
A per-user, per-channel last-read cursor makes unread = messages after the marker — cheap to compute and synced across a user’s devices.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Persist to a per-channel store, then deliverover a copy per recipient (fan-out on write)

Copying a message into every recipient’s mailbox explodes storage for big channels and complicates joins and ordering. Model the channel, not N recipient copies — one ordered history for clean scrollback.

Persistent WebSockets + a session registryover clients polling the API each second

Polling is laggy and wasteful — most polls return nothing and “live” still feels a second behind. To feel instant the server must push, and the registry tells it which socket on which server.

One publish to a pub/sub busover each gateway querying every other gateway

All-to-all gateway chatter is O(servers²) and tightly couples them — adding a server means rewiring everyone. Publish once; gateways become interchangeable subscribers you can add freely.

A separate ephemeral presence pathover sending typing events through the durable message path

Typing fires every keystroke — orders of magnitude more events than messages. Pipelining them through the durable store risks the thing that matters during a presence storm. Keep them in-memory and disposable.

A per-user last-read cursorover an incrementing unread counter per user per channel

A raw counter drifts across devices and can’t say which messages are unread. Unread is just “messages after my last-read marker” — cheap, and syncing one cursor keeps every device in agreement.

What this teaches

Learn system design by building a team chat app like Slack step by step. An interactive guide covering channel messaging and persistence, real-time delivery over WebSockets, cross-server fan-out via pub/sub, presence and typing, message search, notifications and unread counts, and sharding by channel.

Key takeaways

  • Channel-centric messaging: persist to a per-channel store, then deliver.
  • WebSockets plus a session registry give instant, routed push delivery.
  • A pub/sub bus fans each message out to members across all servers.
  • Presence and typing run on a separate, ephemeral, never-persisted path.
  • A write-time search index (with permissions) makes history findable.
  • Per-user last-read cursors drive unread counts and offline notifications.
  • Shard by channel and pull-fan-out for huge channels to scale.

Concepts covered

  • What is Slack?
  • Post to a channel, persist
  • WebSockets & sessions
  • Cross-server fan-out
  • Presence & typing
  • Search
  • Notifications & unread
  • The sharp edges

Design Slack — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

The big idea

What is Slack?

Team chat: people post to channels, and every member sees the message in real time, across all their devices, with full searchable history. The twist versus 1:1 messaging is fan-out to groups and durable, organized history per channel.

Persist every message to a per-channel store, deliver in real time over WebSockets, and fan a message out to all channel members — bridging servers with a pub/sub bus. Then layer presence, search and notifications on top.

How to read this: We add one piece at a time, problem then fix, and the diagram grows. Hit Begin.

Step 1 · The skeleton

Post to a channel, persist

A message is sent to a channel, not a person, and it must be saved so latecomers and other devices can read it. Where does it go, and who is it for?

Design decision: A message is sent to a channel (not a person) and latecomers must read it later. What happens to it?

The call: Persist to a per-channel store, then deliver. — The channel service looks up members and writes the message to ordered per-channel history first, so latecomers and other devices always get a complete record. Persist first, deliver second.

The Gateway hands the message to a Channel Service that looks up the channel’s members and writes the message to a per-channel Message Store. Persist first, deliver second — so history is always complete.

Channels are the unit: Modeling delivery around channels (with membership) rather than individual recipients is what makes group chat, joins and history natural. The message store is ordered per channel for clean scrollback.

Step 2 · Make it instant

WebSockets & sessions

Polling for new messages is laggy and wasteful. To feel live, the server must push messages the instant they arrive — but it needs to know which server holds each recipient’s connection.

Design decision: Messages must appear the instant they’re sent. How does the server reach the right client live?

The call: Persistent WebSockets + a session registry mapping user→server. — Each client holds a long-lived socket to a gateway; a registry records which server holds each user’s connection, so "deliver to user U" becomes "push on this socket on this server" instantly.

Clients hold a persistent WebSocket to a gateway. A Session Registry records which server each online user is connected to, so the system can route a message straight to the right connection and push it down immediately.

Stateful connections: Unlike stateless HTTP, chat gateways hold long-lived connections. The session registry is the directory that turns “deliver to user U” into “push on this socket on this server”.

Step 3 · Members are everywhere

Cross-server fan-out

A channel’s members are spread across many gateway servers. The server that received a message can only push to its own connected clients — everyone else would miss it.

Design decision: A channel’s members are spread across many gateway servers. The receiving server only knows its own clients. How does everyone get the message?

The call: Publish each message to a pub/sub Message Bus that all gateways subscribe to. — The sending gateway publishes once; every gateway subscribed to that channel pushes to its local sockets. One publish reaches members on any server, and gateways become interchangeable subscribers you can add freely.

Publish each accepted message to a Message Bus (pub/sub). Every gateway subscribes to the channels its connected users care about and pushes the message to those local sockets. One publish reaches members on any server.

Pub/sub bridges servers: The bus decouples “a message happened” from “who is connected where”. Gateways become interchangeable subscribers, so you can add servers without rethinking delivery.

Step 4 · Who’s around?

Presence & typing

The green dots and “Bob is typing…” are core to chat, but they’re extremely high-frequency and disposable — running them through the durable message path would swamp it.

Design decision: Green dots and "Bob is typing…" fire constantly and are disposable. How do you handle them?

The call: A separate ephemeral path: in-memory presence, broadcast, never persisted. — Track online/away/typing in fast ephemeral storage and broadcast over the same sockets, but never store it. If everyone disconnects, presence simply vanishes — and a typing storm can’t risk the message store.

Handle Presence on a separate channel: track online/away/typing in fast, ephemeral storage and broadcast updates over the same WebSockets, but never persist them. If everyone disconnects, presence simply disappears.

Ephemeral vs durable: Messages are sacred and stored; presence is throwaway and in-memory. Keeping them on separate paths means a storm of typing events never risks the message store.

Step 5 · Find old messages

Search

Teams accumulate millions of messages, and the value of chat is partly the searchable record. Scanning the message store for a keyword across years of history is far too slow.

Design decision: Teams accumulate millions of messages and need to find old ones by keyword. How?

The call: Build an inverted Search Index at write time, enforce permissions on read. — Index messages as they’re written so queries hit the index, not the raw store — and scope results to channels the user can access. Permissions must be part of the query, not an afterthought.

Index messages into a Search Index (inverted index) as they’re written, scoped by channel and permissions. Queries hit the index, not the raw store, and only return messages the user is allowed to see.

Index for retrieval: The same inverted-index idea behind web search applies here: build it at write time, enforce access on read. Permissions must be part of the query, not an afterthought.

Step 6 · Pull them back

Notifications & unread

People aren’t always watching. Mentions and DMs need to reach them via push, and every client must show accurate unread counts and badges — consistently across devices.

Design decision: Every device must show accurate unread counts and badges, consistently. How do you compute "unread"?

The call: Store a per-user, per-channel last-read cursor; unread = messages after it. — Unread is just "messages after my last-read marker" — cheap to compute, and syncing that one cursor across devices keeps every client in agreement automatically.

A Notifications service tracks each user’s last-read position per channel to compute unread counts, and sends push notifications for mentions/DMs to offline users. Read state syncs across a user’s devices via the gateway.

Read state per user: Unread is just “messages after my last-read marker”. Storing a per-user, per-channel cursor makes counts cheap and keeps every device in agreement.

Step 7 · Scale & big channels

The sharp edges

A huge company workspace, and an announcement channel with tens of thousands of members, both strain fan-out and connection counts. Threads add another dimension to ordering and delivery.

Shard by channel (and workspace) so a channel’s membership and messages live together, and run many stateless gateways for the connection load. For giant channels, fan out lazily (members pull on read) rather than pushing to everyone at once; model threads as messages linked to a parent.

Push small, pull huge: Just like a news feed’s celebrity problem: push to normal channels, but for massive ones let clients pull, so one announcement doesn’t trigger tens of thousands of simultaneous pushes.

You did it

You just designed Slack.

  • Channel-centric messaging: persist to a per-channel store, then deliver.
  • WebSockets plus a session registry give instant, routed push delivery.
  • A pub/sub bus fans each message out to members across all servers.
  • Presence and typing run on a separate, ephemeral, never-persisted path.
  • A write-time search index (with permissions) makes history findable.
  • Per-user last-read cursors drive unread counts and offline notifications.
  • Shard by channel and pull-fan-out for huge channels to scale.
built to be chatted, not memorized — make the calls, cut the bus, run the gauntlet.
Finished this one? 0 / 62 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.