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.

Why this piece earns its place

Persist-first is the right call, but notice what it costs: sending is now only as available as the Message Store. If that write is slow the send is slow, and if it fails the honest thing is to refuse the message rather than accept something we cannot keep — the client shows it unsent instead of pretending. The failure that actually bites, though, is the retry. A client sends, the write lands, the acknowledgement is lost on a flaky network, the client retries, and the same sentence appears twice in the channel. The client has no way to tell whether the first attempt succeeded, so it has to retry. The defense is that the client mints a message id before sending and the store treats it as an idempotency key: a second write carrying that id returns the first result instead of appending a second row. That id earns its keep twice over — it is also what lets a client match the optimistic bubble it already drew on screen to the message that comes back down the socket.

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.

Why this piece earns its place

The Session Registry is a directory of a fact that goes wrong quietly. A socket does not always close politely — a laptop lid, a NAT table expiring, a phone switching networks — so the registry keeps claiming a user is on a server that no longer holds them, and pushes go into a dead connection. The knob here is the heartbeat interval and the lifetime of a registry entry. Set them long and you push into sockets nobody is on, and the user sees nothing new until they refresh. Set them short and you pay in churn: every connection rewriting its entry constantly, and users evicted and reconnecting over a momentary blip, which is worse than the staleness you were fixing. Two details fall out. An entry is per connection, not per person — the same user on desktop and phone is two rows, so “deliver to user U” resolves to a set of sockets across servers. And because messages are stored before they are pushed, a stale entry costs a delayed message, never a lost one, which is what makes it safe to treat this directory as a cache at all.

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.

Why this piece earns its place

The subscribe side is where this strains. A gateway subscribes to the channels its connected users care about, but a busy gateway holds thousands of users who between them sit in most of the workspace’s channels — so its channel set creeps toward all of them and precise routing quietly becomes broadcast-and-filter. That is survivable if you plan for it: deduplicate subscriptions so a channel is subscribed once per server rather than once per socket, and accept that past some density the bus is simply telling every gateway everything. The other thing to design for now is redelivery. A bus that guarantees anything usually guarantees at-least-once, so a hiccup or a consumer rebalance will replay, and the same message will be pushed down a socket twice. So clients dedupe on message id and treat the socket as a stream of hints about a history they can always re-fetch — never as the record itself. Build it that way and the bus being lossy, late or repetitive is an annoyance rather than a correctness problem.

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.

Why this piece earns its place

Separating the path is right; the volume on that path still has to be managed, because typing is the one event a user generates continuously. Broadcast it naively and one person typing in a large channel produces more socket traffic than an actual conversation — and it scales with membership, not with how much anyone is saying. So throttle at the source: the client emits at most one typing signal every few seconds while keys are going down, and that signal carries its own short expiry so the indicator clears itself. That detail matters more than it sounds. If clearing depends on a “stopped typing” event, every dropped one leaves somebody typing forever, and the feature starts lying. Then narrow the audience — push presence only to sockets with that channel actually on screen, not to every member of it. Going too far the other way has its own cost: throttle hard enough and the indicator shows up after the message does, which is worse than not having it, since the whole point is to tell you to wait a moment.

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.

Why this piece earns its place

Indexing at write time means every message is now written to two systems with different failure modes, and they will drift. A failed index write is invisible — the message is in the channel, it simply cannot be found, and a search returning too few results looks exactly like a search with no matches. So I would not index from a best-effort call on the request path. Drive the Search Index off the durable record itself, as a consumer reading the message history with a position it can rewind, so an index outage or a bad mapping change is repaired by replaying from an offset rather than by working out what went missing. The other temptation worth resisting is folding permissions into the index — storing each message’s allowed readers beside it so a query is one cheap lookup. It reads faster and it is a maintenance trap: every join, every leave, every channel switched to private rewrites a slice of the index, and the moment that rewrite lags, someone is searching a room they were removed from.

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.

Why this piece earns its place

A cursor makes unread a derived quantity, which is the right model, but it moves the work to read time and it fans out: someone in hundreds of channels opens the app and every badge has to be produced at once, while they watch. That stays cheap only if counting is arithmetic rather than scanning — each channel’s messages carry a monotonic position, so unread is the distance between the channel’s latest position and my cursor. It is also why a badge on a very busy channel can be capped or approximate while mention counts stay exact: nobody audits whether a channel says nine hundred or nine hundred and change, and everybody notices a mention badge that is wrong. The race worth naming out loud is between the push and the read. Someone reads on their laptop and their phone buzzes a moment later, because the notification was decided before the cursor moved. Hold the push briefly and let an active session cancel it — a late notification is forgivable, a notification for something you just read is the one people turn off.

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.

Why this piece earns its place

Sharding by channel fixes the general case and does nothing for the specific one that hurts: a giant channel is a single shard’s hot key, and you cannot split it without splitting the thing that makes it a channel. Its recent history wants read replicas and its writes want a rate limit, because the announcement everyone reacts to is a write storm as well as a read one. The pull model has an edge worth saying too — if a huge channel stops pushing entirely, members only discover the announcement when they next look, so you still send something, just something cheap: a coalesced marker saying this channel changed, rather than the message body per member. And the operational failure at this size is the deploy, not the outage. Gateways hold every connection in the product, so restarting them together drops every socket at once and the whole workspace reconnects, re-authenticates and re-subscribes in the same few seconds. Stagger the restarts and put randomized backoff in the client, or the recovery is a bigger event than whatever it was recovering from.

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…

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

  2. Instant delivery uses WebSockets plus…

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

  3. A pub/sub message bus exists to…

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

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

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

  5. Unread counts are computed from…

    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.

The answer, out loud

What a strong answer to “Design Slack” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    What I’m actually building

    Let me make sure I’m building the right thing. Slack is team chat, so the unit is a channel rather than a person — I post to one, and everyone in it should see it right away on every device they’re signed into. Four things matter: the history is complete and findable, delivery feels instant, presence and typing are there, and people who aren’t watching get a push and an honest unread badge. The interesting part isn’t one-to-one messaging, it’s fan-out to a group and one organized record per channel.

  2. 3–9 min

    Save it before anyone sees it

    So the skeleton. A client sends to a gateway, the gateway hands it to a channel service that knows the membership, and that service writes the message into a per-channel store before anybody receives it. Persist first, deliver second. The alternative people reach for is a copy per recipient, a mailbox each, and that falls apart on a big channel — you multiply storage by membership and then owe an answer every time somebody joins or leaves. One ordered history per channel gives me scrollback for free, and it means a device that was closed isn’t missing anything, it’s just behind.

    Built in step 1: Post to a channel, persist
  3. 9–15 min

    Making it feel live

    Now it has to feel instant, and polling can’t do that — most polls come back empty and live still lags. So every client holds a persistent WebSocket to a gateway and the server pushes. That makes the gateway stateful, which is genuinely new: it isn’t an HTTP box any more, it’s holding connections. Which means I need a registry of which server a given user is connected to. That’s the piece that turns deliver to user U into push on this socket on this server.

    Built in step 2: WebSockets & sessions
  4. 15–22 min

    Reaching the people on other servers

    The problem with that is a channel’s members are scattered across all the gateways, and the server that took my message can only push to its own clients. I don’t want gateways calling each other to find recipients — that goes up with the square of the server count. Instead the gateway publishes the accepted message once to a pub/sub bus, and every gateway subscribed to that channel pushes it to its local sockets. One publish, work proportional to the number of servers, and gateways become interchangeable subscribers I can add freely. And because I persisted before I published, the bus is an accelerator, not the source of truth — if it drops, messages are still saved and people catch up.

    Built in step 3: Cross-server fan-out
  5. 22–28 min

    Green dots stay off that path

    Presence and typing I’d deliberately keep off the message path. They’re a different kind of data — typing fires on every keystroke, so orders of magnitude more events than real messages, and none of it is worth anything a second later. It lives in memory, it broadcasts over the same sockets, and it is never written to the store. If everyone disconnects, presence vanishes, and that’s the correct behaviour. I’m separating them for blast radius, not tidiness: I don’t want a storm of typing events anywhere near the thing holding messages.

    Built in step 4: Presence & typing
  6. 28–34 min

    Finding old messages

    Search is next, and it’s a lot of why the history is worth keeping. Scanning the store for a keyword across years of it isn’t going to work, so messages are indexed as they’re written and queries hit an inverted index instead of the raw store. The part I’d flag in the room is permissions. Search is the easiest place in the whole product to leak a private channel, so the access check belongs inside the query — results scoped to the channels this person can actually see — not a filter applied afterwards by whoever remembered.

    Built in step 5: Search
  7. 34–39 min

    Unread, and pulling people back

    Then unread and push. I wouldn’t keep a counter per user per channel and increment it, because as soon as somebody has two devices you have to decide which one decrements it, and it drifts. Instead each user has a last-read position per channel and unread is everything after that marker. One cursor, synced across devices, and every client agrees without any reconciliation. The same cursor tells the notifications service who is genuinely behind, so mentions and DMs go out to the people who aren’t looking.

    Built in step 6: Notifications & unread
  8. 39–43 min

    Where it strains

    At scale two things strain. Connections, which is just capacity — run a lot of stateless gateways. And data, where I’d shard by channel, and by workspace above that, so a channel’s membership and its messages sit together and delivering never means a cross-shard join. The nastier case is an announcement channel with tens of thousands of members, which is the celebrity problem from a news feed: one message shouldn’t become tens of thousands of simultaneous pushes. For channels that big I’d fan out lazily and let clients pull on read, and keep pushing for everything normal.

    Built in step 7: The sharp edges
  9. 43–45 min

    The trade, and what’s left

    So: channels as the unit, persisted before delivery, sockets and a registry for the live push, a bus to cross servers, presence kept ephemeral beside it, an index for history and a cursor for unread. The trade all the way through is durability first and real time second — every fast path can fail without anyone losing a message, but sending is only as available as the store behind it. With more time I’d work through threads, which add a second ordering inside a channel and a narrower audience than the channel itself, and exactly what a client does on reconnect to catch up.

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