Courses › System Design › Question Breakdowns

Design Slack

Gateway and channel servers, two-level fan-out, workspace sharding, read markers and huge channels.

Design a team messaging app like Slack: workspaces with channels (public and private), direct messages, threads, real-time delivery to every member's open clients, unread counts and search. It's close to WhatsApp but with different shape: channel fan-out to many members, workspaces as tenants, persistent searchable history, and multi-device sync of read state. Slack's own architecture — stateful channel servers and gateway servers routed by consistent hashing — is a great model to borrow.

Requirements

Functional

  • Users in a workspace can post messages to channels and DMs, and reply in threads
  • Messages appear in real time on every member's connected clients (desktop, mobile, web)
  • Users see unread indicators and mentions, synced across devices
  • Users can search message history
  • Users see presence (online/away) and typing indicators

Out of scope: calls/huddles, file storage internals (presigned uploads to S3), integrations/bots, enterprise grid federation.

Non-functional

  • Low latency: a message reaches online members in < 500ms globally
  • Scale: ~20M daily active users, ~1B messages/day, channels up to ~100k members
  • Durable and ordered: an acknowledged message is never lost; messages in a channel have one consistent order
  • Tenant isolation: one workspace can never read another's data; large customers can't degrade small ones
  • Highly available — this is many companies' main communication tool

[!TIP] Size by connections and fan-out, not storage 1B messages/day is ~12k/s — modest. But an average channel has dozens of members, so delivery fan-out is hundreds of thousands of pushes per second, all over ~20M persistent WebSocket connections. Connection management and fan-out are the real scaling problems.

Capacity estimates

Core Entities

  • Workspace — tenant: members, settings
  • Channel — workspace, name, type (public/private/DM), members
  • Message — channel, ID (time-sortable), author, text, thread parent, edited/deleted flags
  • Membership — user, channel, last read message ID, notification preferences

API

POST /v1/channels/{channelId}/messages   { "text": "Ship it 🚀", "threadTs": null, "clientMsgId": "c-7f2" }
→ 201 { "messageId": "1760190000.000200" }

GET  /v1/channels/{channelId}/messages?before=...&limit=50
POST /v1/channels/{channelId}/read       { "lastRead": "1760190000.000200" }
GET  /v1/search?q=deploy+in:#eng+from:@ana

WS   /v1/realtime    ← { "type": "message", "channel": "C1", ... } | presence | typing

clientMsgId makes sends idempotent: a retried send doesn't create a duplicate.

High-Level Design

c = client "Clients\n(desktop, mobile)" @0,1
gs = service "Gateway Servers\n(WebSockets, per region)" @1,1
api = service "Web API" @1,0
cs = service "Channel Servers\n(consistent hashing)" @2,1
db = store "Message DB\n(sharded by workspace)" @2,0
q = queue "Job Queue" @3,0
search = store "Search Index" @4,0
push = service "Mobile Push" @3,2
pres = cache "Presence" @2,2
c -> api : send message
api -> db : persist
api -> cs : publish
cs -> gs : fan out
gs -> c : push
api -> q
q -> search : index
q -> push : offline members
gs -> pres
  • Send: the client posts through the web API, which checks membership, persists the message, then publishes it to the channel server that owns that channel.
  • Fan-out: the channel server forwards it to every gateway server with a subscriber for that channel; each gateway pushes it down the users' WebSockets.
  • Async work: a job queue handles indexing for search, mobile push for offline users, link unfurling and integrations.
  • Connect: on startup, a client fetches recent state via the API, connects to the nearest gateway, and the gateway subscribes to the channel servers for the user's channels.

Deep Dives

1. Channel servers and gateway servers

  • Channel servers (CS) are stateful: each owns a subset of channels via consistent hashing, keeping recent history and the set of subscribed gateways in memory. A host can serve millions of channels.
  • Gateway servers (GS) hold the WebSocket connections, deployed in many regions close to users. They know which channels each connected user belongs to and subscribe to the matching CSs.
  • A message published to a channel's CS goes once to each subscribed GS, not once per user; each GS then fans out to its local connections. That's two levels of fan-out and it keeps cross-region traffic small.
  • If a CS dies, its channels move to other CSs on the ring (seconds); gateways resubscribe and clients refetch any gap from the API.

2. Ordering and IDs

Each message gets a time-sortable ID assigned on persist (Slack uses a channel-scoped timestamp ts). Within a channel, order by ID; clients insert by ID, so late-arriving pushes still land in the right place. Since all writes to one channel go through one owner, assigning monotonic IDs per channel is straightforward.

3. Storage and tenancy

  • Shard the message store by workspace (Slack historically used MySQL shards per workspace, later Vitess). Most queries are workspace-scoped, and one workspace's data stays together for isolation, export and data residency.
  • Very large workspaces outgrow one shard: re-shard by channel within the workspace (what Vitess enabled).
  • Recent messages per channel are cached for fast channel switching.

4. Unread counts and multi-device sync

Store last read message ID per (user, channel). Unread count = messages in the channel after that ID (cheap with an index, or a maintained counter). When a user reads on desktop, the API updates the marker and pushes a channel_marked event to their other devices, so mobile badges clear. Mentions are tracked separately per user so "@you" survives a muted channel.

5. Huge channels

#general in a 100k-person company: one message → 100k deliveries and, if everyone's client marks it read, 100k writes.

  • Gateways already batch fan-out per GS; most members are offline anyway.
  • Don't push to inactive clients — just bump the channel's latest-message ID; they'll fetch when they open it.
  • Rate-limit @channel/@here and push notifications in large channels.
  • Presence in huge workspaces is subscribe-on-view: clients only get presence updates for users currently visible on screen.

[!WARNING] Persist before you fan out If you push first and the database write fails, people see a message that doesn't exist on reload. Persist, then publish; clients that miss the push catch up from the API using their last-seen ID.

6. Search

Index messages asynchronously into a search cluster (Elasticsearch / Solr), partitioned by workspace. Every query is filtered by the channels the searcher can access, so private channel content never leaks. Freshness of a few seconds is fine.

What's expected at each level

  • Mid-level: WebSockets for real-time, a messages table, channel membership, sending persisted messages to online members.
  • Senior: gateway vs. channel servers with consistent hashing, two-level fan-out, time-sortable IDs, last-read markers synced across devices, async indexing and push.
  • Staff: workspace sharding and re-sharding for giant tenants, huge-channel optimizations, failure recovery of stateful servers, multi-region gateways, and access-filtered search.