System Design Prepgo pro
System design interview question

Design Slack

Real-time team messaging: channels, DMs, presence, history and search for millions of concurrent users.

Difficulty: hard. Patterns: real-time, websockets, fan-out, chat. Reported at Slack, OpenAI, Discord, Meta, Microsoft, Amazon, Google.

Study shows every answer; Practice hides them until you have produced your own.

Functional requirements

  • Workspaces with public and private channels. Users belong to one or more workspaces; channels are scoped to a workspace.
  • Direct messages and group DMs. Model as a channel with a fixed member set so one code path handles both.
  • Send and receive messages in real time. Text, mentions, emoji reactions; edits and deletes propagate.
  • Message history with pagination. Scroll back through years of history; jump to a date or a permalink.
  • Presence and typing indicators. Online / away / offline, plus "X is typing…" in the current channel.
  • Unread counts and @mention badges. Per channel per user, consistent across all of a user's devices.
  • Threads and reactions. Replies attach to a parent message; reactions are counters keyed by emoji.
  • File uploads and previews. Images, PDFs, snippets. Files are attached to messages and searchable by name.
  • Full-text search over messages and files. Scoped to channels the searcher can see.
  • Push notifications when offline. Mentions and DMs go to mobile push; channel messages honour per-channel preferences.

Non-functional requirements

  • Scale (10M DAU · 1M concurrent). Concurrency is what sizes the real-time tier; DAU sizes storage and history reads.
  • Delivery latency (p99 < 500 ms). Sender to every online recipient within one region. Cross-region can be ~1 s.
  • Ordering (total order per channel). All clients see the same order within a channel. No ordering guarantee across channels.
  • Delivery guarantee (at-least-once + dedupe). A message is never lost once the sender sees an ack. Duplicates are tolerated and deduped client side.
  • Durability (no message loss). History is the product. Acked messages survive node, AZ and region failures.
  • Availability (99.99 %). Roughly 52 minutes of downtime a year. Degraded mode (send works, presence stale) is acceptable.
  • Multi-region. Users are worldwide; a workspace is homed in one region but members connect from anywhere.
  • Security. Private channel membership enforced on every read and on every fan-out. Encryption in transit and at rest.

Back-of-envelope estimates

  • Concurrent connections: ~1 M. 10 M DAU, each online ~2.4 h/day spread over a 24 h day gives 10 M × 2.4 / 24 = 1 M average concurrent. Peak (US morning) is ~2–3× the average, so plan for ~2.5 M connections.
  • Messages sent per second: ~5 k avg · 15 k peak. Assume 40 messages per DAU per day. 10 M × 40 = 400 M/day ÷ 86 400 s ≈ 4.6 k/s. Peak 3× ≈ 15 k/s. Writes are not the hard part.
  • Fan-out deliveries per second: ~500 k–1 M. Median channel has ~10 members but the distribution is heavy-tailed: a few #general channels have 50 k. Assume an average of 100 online recipients per message: 5 k × 100 = 500 k pushes/s, peaking near 1 M/s. This is the hard part.
  • WebSocket servers: ~50–100. A tuned server holds ~50 k idle connections; budget 25 k per box to leave headroom for fan-out CPU. 2.5 M ÷ 25 k = 100 servers at peak, in a few AZs.
  • Storage per year: ~150 TB. Average message ~1 KB after metadata and indexes. 400 M/day × 1 KB × 365 ≈ 146 TB/yr. Replication ×3 → ~450 TB/yr raw. Files are far larger but go to object storage, not the message store.
  • Read / write ratio: ~50 : 1. Every message is read by every member, and history loads re-read old messages. Reads dominate, but most are served from the client cache or a hot recent-messages cache, so the DB read load is a fraction.
  • Presence updates per second: ~50 k. 1 M concurrent × heartbeat every 30 s = 33 k/s, plus explicit status changes. Each presence change can fan out to every viewer of that user, so presence is cheap to ingest and expensive to broadcast.

Components

  • Web / Mobile client: Holds a WebSocket for real-time events and uses REST for everything else. Keeps a local cache of recent messages per channel and a per-channel cursor (last seen message id) used to resync after reconnect. Generates a client message id for idempotent sends.
  • API gateway (REST · auth · rate limit): Terminates TLS, validates the auth token, enforces per-user rate limits and routes REST calls (history, search, uploads, channel management) to the right service. Stateless, horizontally scaled behind a load balancer.
  • WebSocket gateway (sticky, long-lived): Holds the long-lived connections. On connect it authenticates, registers (user, device) → server in the connection registry and subscribes to the topics for that user's channels. On send it validates, forwards to the Message service and later pushes deliveries to the sockets it owns. ~25 k connections per box.
  • Connection registry (Redis cluster): Maps user_id → set of (ws_server, connection_id) with a TTL refreshed by heartbeats. Lets any service find where a user is connected. Also backs presence (online/away with last-seen timestamps) and pub/sub channels used to route deliveries to the right WebSocket server.
  • Channel service: Channels, memberships, permissions. Answers "who is in this channel" and "can this user read it". Membership lists are cached hard because every fan-out needs them.
  • Message service: The write path. Assigns the message id, checks membership, persists to the messages store, then publishes a message-created event to Kafka. Also serves history reads with cursor pagination and edits/deletes/reactions.
  • Presence service: Ingests heartbeats and status changes, writes to the registry with TTLs and broadcasts presence deltas to interested viewers. Deliberately lossy and rate-limited: presence is a hint, not a fact.
  • Event bus (Kafka · keyed by channel_id): Durable log of message events, partitioned by channel_id so a channel's events stay ordered. Consumers: the fan-out path (WebSocket gateways via the registry), notifications, search indexing, unread counters, analytics. Decouples the write path from every downstream reader.
  • Notification service: Consumes message events, decides who should get a push (mentions, DMs, per-channel preferences, and only if no active connection saw the message within a short window), batches and sends via APNs / FCM.
  • Search indexer (Elasticsearch / OpenSearch): Consumes message and file events, indexes text with channel_id and workspace_id as filter fields. Queries always filter by the channels the searcher belongs to, so the ACL check happens inside the index query.
  • Messages store (Cassandra / Scylla): Partition key (channel_id, time_bucket), clustering key message_id descending. A history page is one partition read. Write-optimised LSM storage, linearly scalable, tunable consistency (LOCAL_QUORUM writes).
  • Metadata DB (Postgres (sharded by workspace)): Users, workspaces, channels, memberships, preferences. Relational because it needs joins and constraints; sharded by workspace_id because nothing crosses a workspace. Membership hot-set cached in Redis.
  • Object storage + CDN (S3 · CloudFront): Files are uploaded directly from the client to object storage using a pre-signed URL, so file bytes never flow through application servers. Messages reference the file by id; downloads go through the CDN with signed, expiring URLs.
  • Push providers (APNs · FCM): Apple and Google push services. Best-effort delivery, rate-limited, with device tokens that expire. Treat as unreliable: the client resyncs from the cursor on open regardless of whether the push arrived.

User flows

  1. Send a message in a channel. The core path. Sender presses enter; every online member in the channel sees the message within 500 ms, and it is durably stored before the sender sees the checkmark.
  2. Client reconnects and syncs missed messages. Phones sleep, laptops close, Wi-Fi drops. Reconnect must be cheap for the server and invisible to the user, with no gaps and no duplicates.
  3. Presence and typing indicators. Cheap to produce, expensive to broadcast, and nobody needs it to be exact. The design leans into that: TTLs, coalescing, and only telling people who are looking.
  4. @mention → push notification to an offline user. Notifications must be timely but never spammy: no push for a message the user already saw on their laptop, and no double-push across devices.
  5. Search for a message. Full-text search that respects private channel membership and returns in a few hundred milliseconds across years of history.

Deep dives

  1. Fan-out strategy for large channels. One message to a 50 000-member channel: how does it reach everyone without melting the write path?
  2. Real-time transport: WebSocket vs long polling vs SSE. Why hold a million persistent connections instead of letting clients poll?
  3. Message ordering and IDs. How do all clients agree on the order of messages in a channel, without a single global sequencer?
  4. Message storage and partition key. Which database holds years of messages, and what is the partition key so history reads stay fast as channels age?
  5. Delivery guarantees, acks and dedupe. What does "never lose a message" actually mean, and where can duplicates come from?
  6. Presence at scale. Why is presence the feature most likely to take down a chat system, and how do you make it safe?