Vibe Engines
YouTube
System Design

Design Slack

Step 1 / 9

Learn system design by building a team chat app like Slack step by step.

The whole design, in writing

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.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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.

Usersend / read
New in this step: User.

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.

What the new pieces do

Userclient
A person in a workspace sending messages to channels and expecting everyone else’s messages to appear instantly — across web, desktop and mobile.

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?

GatewayChannel ServiceMessage Store
New in this step: Gateway, Channel Service, Message Store. · swipe to pan the diagram

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

  1. Fire-and-forget loses the message for anyone offline, on another device, or joining later — and there’s no scrollback. Chat’s value is the durable, organized record.

  2. Copying a message into every recipient’s mailbox explodes storage for big channels and complicates joins/leaves and ordering. Model the channel, not N recipient copies.

  3. 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.

What the new pieces do

Gatewaybackend
Holds each client’s persistent connection and exposes the REST API. Receives messages, persists them, and pushes new ones down to connected clients.
Channel Serviceservice
Knows who belongs to each channel and routes a new message to all its members. Channels, not individuals, are the unit of delivery.
Message Storestore
Durable, ordered history per channel. Messages are saved before delivery so history is complete and clients can scroll back through everything.

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.

Gatewaystateful WS connectionsChannel Servicemembership · fan-outSession Registrywho is connected
New in this step: Session Registry.

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

  1. Polling is laggy and wasteful — most polls return nothing, and "live" still feels a second behind. To feel instant the server must push, not wait to be asked.

  2. 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.

  3. Wrong channel and far too slow for live chat, and unboundedly noisy. Out-of-band notifications are for offline users (step 6), not the real-time path.

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.

What the new pieces do

Session Registryservice
Maps each online user to the server holding their WebSocket, so the system knows exactly where to push a message for a given recipient.

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.

GatewayChannel ServiceMessage StoreMessage Bus
New in this step: Message Bus. · swipe to pan the diagram

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

  1. All-to-all gateway chatter is O(servers²) and tightly couples them — adding a server means rewiring everyone. You want one publish, not a mesh of point-to-point calls.

  2. 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.

  3. Pinning a whole channel to one server creates hotspots (a huge channel overwhelms it) and breaks when users are in many channels on different servers. Bridge servers with a bus instead.

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.

What the new pieces do

Message Busbus
Relays each message between gateway servers, so a member connected to a different server still receives it. The glue for cross-server delivery.

Back of the envelope

1 publish ⇒ all subscribed gateways
O(servers), not O(servers²) chatter
gateways = interchangeable subscribers
add servers without rewiring delivery
decouples "message happened" from "who’s where"
the bus bridges connection topology

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.

Gatewaystateful WS connectionsChannel Servicemembership · fan-outPresenceonline · typing
New in this step: Presence.

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

  1. Persisting a flood of high-frequency typing events would swamp the durable store with data nobody reads back. Presence is throwaway — it shouldn’t touch message storage at all.

  2. Mixing high-frequency disposable signals into the durable pipeline risks the thing that matters (messages) during a presence storm. Keep them on separate paths.

  3. 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.

What the new pieces do

Presenceservice
Tracks who’s active and who’s typing, broadcasting these high-frequency, throwaway signals separately from durable messages.

Back of the envelope

typing fires every keystroke
orders of magnitude more events than messages
in-memory, broadcast, never stored
a presence storm can’t touch the message store
everyone disconnects ⇒ presence gone
disposable by design

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.

Channel Servicemembership · fan-outMessage Storeper-channel historySession Registrywho is connectedSearch Indexmessages
New in this step: Search Index.

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

  1. Scanning years of history per query is far too slow. You need a structure built for retrieval, not a linear scan of the raw store.

  2. 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.

  3. Clients hold only a sliver of history and can’t search what they never received or channels they just joined. Search must run server-side over the full indexed record.

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.

What the new pieces do

Search Indexindex
An inverted index over message history so users can find old messages by keyword, scoped to channels they can access.

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.

GatewayChannel ServiceMessage StorePresenceNotifications
New in this step: Notifications. · swipe to pan the diagram

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

  1. A raw counter drifts across devices and is hard to keep consistent (which device decremented?). It also can’t say which messages are unread, only how many — fragilely.

  2. Re-scanning a channel’s history to count unreads on every app open is wasteful at scale. There’s a cheap derivation if you track one marker instead.

  3. 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.

What the new pieces do

Notificationsservice
Computes unread counts and sends push notifications for mentions and DMs to users who are offline or away.

Back of the envelope

unread = messages after last-read
a single per-user, per-channel cursor
cheap to compute
no re-scan, no drifting counter
cursor syncs across devices
every client agrees

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.

UserGatewayChannel ServiceMessage StoreSession RegistryMessage BusPresenceSearch IndexNotifications
The system as it stands at this step. · swipe to pan the diagram

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.

You did it

You just designed Slack.

UserGatewayChannel ServiceMessage StoreSession RegistryMessage BusPresenceSearch IndexNotifications
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • 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.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. How do you keep message ordering consistent within a channel?

    Assign each message a per-channel monotonic sequence (or a time-sortable id) at persist time, before fan-out, so every recipient orders by the same key regardless of which server delivered it or network jitter. Clients reconcile on reconnect by requesting "everything after sequence N." The store is the ordering authority; the real-time push is just an accelerator.

  2. How does a client catch up after being offline?

    It reconnects, the session registry re-binds it to a gateway, and it requests messages after its last-known sequence per channel from the durable store — so anything missed while the socket was down (or the bus hiccuped) is pulled. Because messages persist before delivery, "missed a push" is always recoverable, never data loss.

  3. A #announcements channel has 50,000 members — what breaks?

    Pushing one message to 50k live sockets at once is a fan-out spike (the celebrity problem from a news feed). Fix it by fanning out lazily for giant channels: don’t push to everyone, let clients pull on read/focus, and rate-limit. Normal channels stay push; only the rare huge ones switch to pull.

  4. How are threads modeled?

    As messages linked to a parent message id, forming a sub-conversation within the channel. They persist in the same per-channel store but carry a parent reference, so a thread is a filtered view (messages where parent = X). Delivery can be narrower — thread participants and the parent’s followers — rather than the whole channel.

  5. Why shard by channel rather than by user?

    A message’s reads and writes are overwhelmingly within one channel — membership, history and fan-out all key on the channel. Channel-sharding co-locates that data (no cross-shard joins to deliver) and lets hot channels scale independently. User-sharding would scatter one channel’s messages across many shards, making every send a cross-shard fan-out.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A message is persisted before delivery so that…

    • It sends faster
    • History is always complete for latecomers and other devices
    • It uses less storage

    Persist first, deliver second — the durable per-channel record is chat’s core value.

  2. Instant delivery uses WebSockets plus…

    • Faster polling
    • A session registry mapping each user to their gateway server
    • Email

    The registry turns "deliver to user U" into "push on this socket on this server".

  3. A pub/sub message bus exists to…

    • Store messages
    • Fan a message out to channel members across all gateway servers
    • Index search

    One publish reaches members on any server; gateways are interchangeable subscribers.

  4. Presence and typing run on a separate path because they’re…

    • More important
    • High-frequency and disposable — never persisted
    • Encrypted

    Keeping ephemeral signals off the durable path means a typing storm never risks the message store.

  5. Unread counts are computed from…

    • A counter incremented per message
    • A per-user, per-channel last-read cursor
    • The search index

    Unread = messages after the last-read marker — cheap, and syncs consistently across devices.

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.

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.

The qualities that shape everything

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 deliver over 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 registry over 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 bus over 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 path over 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 cursor over 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
built to be chatted, not memorized — make the calls, cut the bus, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

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

More System Designs