The probe
Slack is WhatsApp + channels + threads + search.
The messaging core is identical to WhatsApp (WebSocket, Cassandra, delivery semantics). The probes unique to Slack: channel message fan-out (a channel with 10,000 members means one message = 10,000 deliveries), message threading (replies live in a separate namespace from channel messages), and full-text search over message history.
Step 1 — Clarify
- Direct messages only, or channels + DMs + threads? (Scope to channels + DMs) - Search over message history? (Yes — search is a major Slack differentiator) - Max members per channel? (Slack allows ~10K for standard, unlimited for Enterprise Grid)
- File sharing? (Yes — same object storage pattern as WhatsApp media) - SLO: message delivery < 200ms p99 within same workspace
Step 2 — Estimate
- 20M DAU × 200 messages/day = 4B messages/day = ~46K messages/sec - Average channel size: 50 members → fan-out 50 deliveries per message - Large channels (10K members): 1 message = 10K WebSocket pushes - Message retention: Slack keeps all history (unlimited on paid plans) → ~500 bytes/msg × 4B msgs/day × 365 days = ~730 TB/year
Step 3 — API Design
WebSocket: wss://slack.com/connect (per workspace per device)
POST /v1/messages
Body: { channel_id, text, thread_ts (optional — reply), blocks[], files[] }
Idempotency-Key: header
Response: { ts (message timestamp, also its ID), channel_id }
GET /v1/channels/:channel_id/messages
Query: cursor, limit, oldest, latest (time range)
Response: { messages[], has_more, next_cursor }
POST /v1/search
Body: { query, channel_ids[], from_user, date_range }
Response: { messages: [{ts, channel_id, text, permalink}], total }
ts (timestamp) is Slack’s actual message ID — a Unix timestamp with microsecond precision, unique per channel.
Step 4 — Data Model
- messages (Cassandra): PARTITION BY channel_id, CLUSTER BY ts DESC — identical pattern to WhatsApp but scoped to channel
- threads: thread_ts (FK to parent message ts), reply messages in same table with thread_ts as additional cluster key
- channel_members: channel_id, user_id, joined_at — indexed both ways (channel→members for fan-out, user→channels for sidebar)
- search_index (Elasticsearch): message ts, channel_id, text, user_id, timestamp — updated async via Kafka consumer
- unread_counts (Redis): user_id:channel_id → {unread_count, last_read_ts} — updated on every message and read event

