Chapter 53: Design Slack/Discord
Real-time team collaboration platform supporting channels, DMs, threads, presence, and file sharing at scale.
1. Requirements
Functional Requirements
| Feature |
Description |
Priority |
| Real-time messaging |
Send/receive messages instantly (<100ms latency) |
P0 |
| Channels |
Public/private channels, channel creation/deletion, membership management |
P0 |
| Direct messages |
1:1 and group DMs (up to 9 participants) |
P0 |
| Threads |
Reply to messages in thread context, thread followers |
P0 |
| Reactions |
Emoji reactions on messages, reaction counts |
P1 |
| File sharing |
Upload files (up to 1GB), preview generation, inline display |
P0 |
| Presence |
Online/away/DND status, typing indicators |
P1 |
| Search |
Full-text search across messages, filters (from, in, before/after) |
P1 |
| Message history |
Infinite scroll, pagination, message editing/deletion |
P0 |
| Notifications |
Push notifications, @mentions, channel-wide alerts |
P1 |
Non-Functional Requirements
| Requirement |
Target |
Justification |
| Availability |
99.9% uptime |
Team productivity depends on availability |
| Latency |
<100ms for message delivery |
Real-time collaboration requires instant feedback |
| Consistency |
Eventual consistency (acceptable for chat) |
Message ordering per channel, eventual across channels |
| Scalability |
20M DAU, 100K concurrent per server |
Support enterprise scale |
| Durability |
Zero message loss |
Messages are critical business records |
Scale Targets
| Metric |
Value |
| Daily Active Users (DAU) |
20 million |
| Concurrent connections |
5 million (25% of DAU) |
| Messages per day |
500 million |
| Channels per workspace |
Average 200, max 500K |
| Members per channel |
Average 50, max 100K |
Interview Callout: Clarify Requirements
Ask: "What's the max channel size?" (impacts fan-out), "Do we need message ordering guarantees?" (affects architecture), "What's acceptable latency for search?" (determines indexing strategy).
2. Capacity Estimation
Traffic Estimates
| Metric |
Calculation |
Result |
| Messages per second |
500M messages/day ÷ 86400 sec |
~6K writes/sec |
| Peak writes/sec |
6K × 5 (peak factor) |
30K writes/sec |
| Channel fan-out |
6K msg/sec × 50 avg members |
300K deliveries/sec |
| Reads per second |
10:1 read:write ratio |
60K reads/sec |
| WebSocket connections |
5M concurrent / 100K per server |
50 gateway servers |
Storage Estimates
| Component |
Size per Item |
Volume |
Total Storage |
| Message metadata |
500 bytes |
500M/day × 365 days |
91 TB/year |
| Message body |
1 KB avg |
500M/day × 365 days |
183 TB/year |
| File attachments |
2 MB avg |
50M files/day × 365 days |
36 PB/year |
| Search index |
2 KB per message |
500M/day × 365 days |
365 TB/year |
| Total (excluding files) |
- |
- |
~650 TB/year |
Bandwidth Estimates
| Operation |
Calculation |
Bandwidth |
| Message ingress |
6K msg/sec × 1.5 KB |
9 MB/sec |
| Message egress (fan-out) |
300K deliveries/sec × 1.5 KB |
450 MB/sec |
| File uploads |
50M files/day × 2 MB ÷ 86400 sec |
1.2 GB/sec |
| File downloads |
File uploads × 3 (download ratio) |
3.6 GB/sec |
Interview Callout: Back-of-envelope Math
Show the fan-out calculation clearly: "1 message to 50-member channel = 50 deliveries via pub/sub. 6K msg/sec becomes 300K deliveries/sec. This drives WebSocket gateway sizing."
3. High-Level Design
Architecture Overview
graph TB
Client[Client Apps
Web/Mobile/Desktop]
subgraph "Edge Layer"
LB[Load Balancer]
CDN[CDN
Static Assets]
end
subgraph "Gateway Layer"
WSG1[WebSocket Gateway 1
100K connections]
WSG2[WebSocket Gateway 2]
WSGN[WebSocket Gateway N]
end
subgraph "API Layer"
API[REST API
Auth, Channel Mgmt]
Auth[Auth Service]
end
subgraph "Core Services"
MsgSvc[Message Service
Publish/Route]
ChanSvc[Channel Service
Membership/ACL]
PresSvc[Presence Service
Online/Away]
NotifSvc[Notification Service
Push/Email]
FileSvc[File Service
Upload/Preview]
SearchSvc[Search Service
Query/Index]
end
subgraph "Data Stores"
Redis[(Redis
Presence/Cache)]
MySQL[(Vitess/MySQL
Messages/Channels)]
ES[(Elasticsearch
Search Index)]
S3[(S3
File Storage)]
end
subgraph "Messaging Infrastructure"
Kafka[Kafka
Message Queue]
PubSub[Redis Pub/Sub
Channel Delivery]
end
Client -->|HTTPS| LB
Client -->|WebSocket| LB
LB --> API
LB --> WSG1 & WSG2 & WSGN
WSG1 & WSG2 & WSGN --> MsgSvc
WSG1 & WSG2 & WSGN --> PresSvc
WSG1 & WSG2 & WSGN -.subscribe.-> PubSub
API --> Auth
API --> ChanSvc
API --> FileSvc
API --> SearchSvc
MsgSvc --> Kafka
MsgSvc --> PubSub
MsgSvc --> MySQL
ChanSvc --> MySQL
PresSvc --> Redis
SearchSvc --> ES
FileSvc --> S3
NotifSvc --> Kafka
Kafka --> SearchSvc
Kafka --> NotifSvc
Component Responsibilities
| Component |
Responsibility |
Technology |
| WebSocket Gateway |
Maintain persistent connections, route messages, subscribe to channels |
Node.js/Go with Socket.IO |
| Message Service |
Validate, persist, publish messages to pub/sub |
Java/Go microservice |
| Channel Service |
Manage channels, membership, ACL checks |
Java microservice |
| Presence Service |
Track online status, heartbeats, typing indicators |
Go with Redis |
| Notification Service |
Process @mentions, send push notifications |
Consumer from Kafka |
| Search Service |
Index messages, execute queries |
Elasticsearch wrapper |
| File Service |
Upload to S3, generate previews, virus scan |
Java/Python |
Data Flow Summary
| Flow |
Path |
Latency |
| Send message |
Client → WS Gateway → Message Service → MySQL + Pub/Sub → WS Gateway → Clients |
<100ms |
| Load history |
Client → API → Message Service → MySQL (paginated) |
<200ms |
| Search |
Client → API → Search Service → Elasticsearch |
<500ms |
| File upload |
Client → File Service → S3 → Message Service (file URL) |
1-5 sec |
| Presence update |
Client → WS Gateway → Presence Service → Redis → Fan-out to shared channels |
<50ms |
4. Real-Time Messaging
WebSocket Connection Management
| Aspect |
Design Decision |
Rationale |
| Protocol |
WebSocket (wss://) |
Full-duplex, low latency, push from server |
| Connection state |
In-memory on gateway server |
Fast lookup, no external dependency |
| Connection affinity |
Sticky sessions via LB (IP hash) |
Keep user on same gateway server |
| Heartbeat |
30 sec ping/pong |
Detect dead connections, update presence |
| Reconnect |
Exponential backoff (1s, 2s, 4s, max 30s) |
Handle temporary network issues |
| Missed messages |
Client sends last_msg_id on reconnect |
Fetch gap from message service |
Message Routing Architecture
| Step |
Component |
Action |
| 1 |
Client |
Send message via WebSocket: {channel_id, text, thread_id?} |
| 2 |
WS Gateway |
Forward to Message Service via HTTP/gRPC |
| 3 |
Message Service |
Validate (ACL check), generate message ID (Snowflake) |
| 4 |
Message Service |
Persist to MySQL (sharded by channel_id) |
| 5 |
Message Service |
Publish to Redis Pub/Sub: channel:{channel_id} |
| 6 |
Message Service |
Enqueue to Kafka (async processing: search, notifications) |
| 7 |
All WS Gateways |
Receive from Redis Pub/Sub, fan out to connected clients subscribed to channel |
Pub/Sub Channel Subscription Model
| Event |
Action |
Redis Pub/Sub Topic |
| User connects |
WS Gateway subscribes to all user's channels |
channel:{channel_id} (N subscriptions) |
| Join channel |
WS Gateway adds subscription dynamically |
channel:{new_channel_id} |
| Leave channel |
WS Gateway removes subscription |
Unsubscribe from channel:{channel_id} |
| Message arrives |
WS Gateway pushes to all connected clients in channel |
Fan-out within gateway |
| User disconnects |
WS Gateway unsubscribes from all channels |
Cleanup subscriptions |
Message Send Sequence
sequenceDiagram
participant C1 as Client 1
participant WSG as WS Gateway
participant MS as Message Service
participant DB as MySQL
participant PS as Redis Pub/Sub
participant WSG2 as WS Gateway 2
participant C2 as Client 2
C1->>WSG: Send message (channel_id, text)
WSG->>MS: POST /messages {channel_id, user_id, text}
MS->>MS: Validate ACL, generate msg_id
MS->>DB: INSERT message (sharded by channel_id)
DB-->>MS: ACK
MS->>PS: PUBLISH channel:123 {msg_id, user_id, text, ts}
MS-->>WSG: 201 Created {msg_id, ts}
WSG-->>C1: ACK {msg_id, ts}
PS-->>WSG: Message on channel:123
WSG->>C1: Push message (subscribed to channel)
PS-->>WSG2: Message on channel:123
WSG2->>C2: Push message (subscribed to channel)
Note over MS: Async: Enqueue to Kafka for search indexing, notifications
Interview Callout: Why Pub/Sub + Kafka?
"Redis Pub/Sub for real-time fan-out (low latency). Kafka for async processing (search indexing, notifications) with durability. Pub/Sub is ephemeral—if a gateway is down, Kafka ensures no message is lost for indexing."
5. Channel Architecture
Channel Types
| Type |
Characteristics |
Use Case |
| Public channel |
Discoverable, anyone in workspace can join |
#general, #engineering, #random |
| Private channel |
Invite-only, not discoverable |
#leadership, #hr-confidential |
| Direct message (1:1) |
Two participants, always private |
DM between Alice and Bob |
| Group DM |
Up to 9 participants, private, no admin |
Quick team discussion |
Channel Membership Schema
| Table |
Columns |
Index |
| channels |
channel_id (PK), workspace_id, name, type (public/private), created_at, created_by |
IDX on workspace_id |
| channel_members |
channel_id, user_id, role (admin/member), joined_at, last_read_msg_id |
IDX on (user_id, channel_id), IDX on (channel_id) |
| channel_metadata |
channel_id, member_count, message_count, last_message_at |
For channel list display |
Fan-Out Strategies
| Channel Size |
Strategy |
Implementation |
| Small (<100) |
Fan-out on write |
Pub/Sub to channel:{id}, all gateways push to connected members |
| Medium (100-1000) |
Fan-out on write (same) |
Redis Pub/Sub scales to 10K channels easily |
| Large (1K-10K) |
Fan-out on write with batching |
Gateway batches WebSocket sends (100ms window, 50 msg batch) |
| Mega (10K+) |
Hybrid: fan-out on read for offline users |
Only push to online users (via presence check). Offline users fetch on reconnect. |
Large Channel Optimization
| Problem |
Solution |
Benefit |
| 100K members, 50K online |
Check presence before fan-out |
Only push to 50K, not 100K |
| Redis Pub/Sub saturation |
Dedicated Pub/Sub cluster for mega channels |
Isolate traffic |
| Message rate spike (100/sec) |
Rate limit + throttle display (show "N new messages") |
Prevent client overload |
| Offline members missing messages |
Store unread count per user, fetch on reconnect |
Catch up on history |
Channel ACL Checks
| Operation |
Check |
Cache |
| Send message |
User is member of channel |
Redis: user:{user_id}:channels (Set) |
| Read history |
User is member of channel |
Same cache |
| Join channel |
Channel is public OR user is invited |
Invalidate cache after join |
| Delete message |
User is message author OR channel admin |
Check on-demand (rare operation) |
Interview Callout: Mega Channel Tradeoff
"For 100K-member channels (e.g., company-wide #general), fan-out on write becomes expensive. Hybrid approach: push to online users (~25% = 25K), offline users pull on reconnect. Tradeoff: online users get instant updates, offline users have slight delay."
6. Message Storage
Message Schema
| Column |
Type |
Description |
| message_id |
BIGINT (PK) |
Snowflake-style ID (time-sortable) |
| channel_id |
BIGINT |
Shard key (messages partitioned by channel) |
| user_id |
BIGINT |
Message author |
| thread_id |
BIGINT (NULL) |
Parent message ID (NULL for top-level) |
| text |
TEXT |
Message content (up to 40K chars) |
| attachments |
JSON |
Array of file URLs/metadata |
| reactions |
JSON |
{"thumbs_up": 5, "heart": 3} |
| created_at |
TIMESTAMP |
Message timestamp |
| edited_at |
TIMESTAMP (NULL) |
Last edit time |
| deleted_at |
TIMESTAMP (NULL) |
Soft delete (for audit trail) |
Snowflake ID Structure
| Bits |
Component |
Purpose |
| 1 bit |
Sign (unused) |
Always 0 |
| 41 bits |
Timestamp (ms since epoch) |
Time-sortable (69 years range) |
| 10 bits |
Machine ID |
1024 message service instances |
| 12 bits |
Sequence number |
4096 IDs per ms per machine |
Benefit: IDs are globally unique, time-sortable, no coordination required. Supports pagination by ID.
Sharding Strategy
| Shard Key |
Pros |
Cons |
Decision |
| channel_id |
All messages for channel on same shard (fast queries) |
Hot channels create hot shards |
✓ Use this |
| message_id |
Even distribution |
Channel queries scatter across shards |
✗ Too slow |
| user_id |
Good for "my messages" queries |
Channel queries scatter |
✗ Not primary use case |
Handling hot shards: Monitor shard load. Split mega-channels across multiple shards (e.g., shard by hash(channel_id + date)).
Message Pagination
| Direction |
Query |
Use Case |
| Load recent |
SELECT * FROM messages WHERE channel_id = ? ORDER BY message_id DESC LIMIT 50 |
Initial channel load |
| Load older |
WHERE channel_id = ? AND message_id < ? ORDER BY message_id DESC LIMIT 50 |
Scroll up (infinite scroll) |
| Load newer |
WHERE channel_id = ? AND message_id > ? ORDER BY message_id ASC LIMIT 50 |
Scroll down or catch up after disconnect |
| Jump to message |
WHERE channel_id = ? AND message_id >= ? LIMIT 25 + fetch 25 before |
Jump to thread or search result |
Append-Only Optimization
| Optimization |
Implementation |
Benefit |
| Write-optimized |
Messages never updated (edits stored as metadata) |
No locks, pure INSERTs |
| Soft deletes |
Set deleted_at timestamp |
Audit trail, compliance |
| Compression |
gzip text for messages >7 days old |
Reduce storage cost |
| Archival |
Move messages >1 year to cold storage (S3 Glacier) |
99% of queries are recent messages |
Interview Callout: Why Channel-based Sharding?
"Slack's primary query is 'show me channel history.' Sharding by channel_id keeps all messages on one shard, single-query pagination. Hot channels (e.g., #general) can be split by date if needed."
7. Threads
Thread Data Model
| Component |
Implementation |
Storage |
| Parent message |
Regular message with thread_id = NULL |
messages table |
| Thread reply |
Message with thread_id = parent_message_id |
messages table |
| Thread metadata |
reply_count, last_reply_at, participant_ids[] |
thread_metadata table (denormalized) |
| Thread followers |
thread_id, user_id, following (bool) |
thread_followers table |
Thread Queries
| Query |
SQL |
Index |
| Fetch thread |
SELECT * FROM messages WHERE thread_id = ? ORDER BY message_id ASC |
IDX on (thread_id, message_id) |
| Thread count |
SELECT reply_count FROM thread_metadata WHERE thread_id = ? |
Cached in Redis |
| Active threads |
SELECT * FROM thread_metadata WHERE channel_id = ? ORDER BY last_reply_at DESC LIMIT 20 |
IDX on (channel_id, last_reply_at) |
| My threads |
SELECT * FROM thread_followers WHERE user_id = ? AND following = TRUE |
IDX on (user_id, following) |
Thread Notifications
| Condition |
Notify Who |
Implementation |
| Reply to thread |
All thread followers |
Query thread_followers, send push notification |
| @mention in thread |
Mentioned user (even if not following) |
Parse mentions, send direct notification |
| First reply |
Parent message author (auto-follow) |
Insert into thread_followers |
| Unfollow thread |
N/A |
Update following = FALSE (don't delete row for analytics) |
Thread UI Patterns
| Pattern |
Display |
Data Loaded |
| Thread preview |
Show "5 replies" link below parent message |
Thread metadata (reply_count, last_reply_at) |
| Thread sidebar |
Open thread in right panel, show all replies |
Full thread messages (paginated if >100 replies) |
| Thread badge |
Unread count on thread ("3 new replies") |
Compare last_read_msg_id vs. last_reply_id |
| Thread list |
"All Threads" view, sorted by last_reply_at |
Thread metadata + parent message preview |
Interview Callout: Thread Followers Auto-Join
"When you reply to a thread, you auto-follow it (get notifications for future replies). This is stored in
thread_followers. Prevents spam: if you unfollow, you won't get notified unless @mentioned."
8. Presence System
Presence States
| State |
Trigger |
Duration |
| Online |
Active WebSocket connection, heartbeat within 30s |
While connected |
| Away |
No activity for 5 minutes (no keypress, no message sent) |
Until activity resumes |
| Do Not Disturb |
User sets DND (suppress notifications) |
User-configured (e.g., 2 hours) |
| Offline |
WebSocket disconnected, no heartbeat for >60s |
Until reconnect |
Presence Storage
| Data Structure |
Redis Key |
Value |
TTL |
| User presence |
presence:{user_id} |
{status: "online", last_seen: 1672531200} |
60 seconds |
| Typing indicator |
typing:{channel_id} |
Set of user_id (SADD/SREM) |
5 seconds per user |
| Connection state |
conn:{user_id} |
{gateway_id, conn_id, connected_at} |
No TTL (cleaned on disconnect) |
Heartbeat Mechanism
| Action |
Frequency |
Implementation |
| Client ping |
Every 30 seconds |
WebSocket PING frame |
| Server pong |
Immediate |
WebSocket PONG frame |
| Presence update |
On every heartbeat |
Redis SET presence:{user_id} with 60s TTL |
| Stale detection |
60s timeout |
Redis key expires → user marked offline |
Presence Fan-Out
| Event |
Fan-Out To |
Implementation |
| User goes online |
All users in shared channels |
Query channel_members, send presence update via Pub/Sub |
| User goes offline |
All users in shared channels |
Same fan-out |
| Typing indicator |
Only users viewing the same channel |
Pub/Sub to channel:{channel_id}:typing |
| Status change (DND) |
All users in shared channels |
Same as online/offline |
Typing Indicator Flow
sequenceDiagram
participant C as Client
participant WSG as WS Gateway
participant PS as Presence Service
participant Redis as Redis
participant PSub as Redis Pub/Sub
participant C2 as Other Clients
C->>WSG: Typing start (channel_id)
WSG->>PS: POST /presence/typing {user_id, channel_id}
PS->>Redis: SADD typing:channel123 user456 (TTL 5s)
PS->>PSub: PUBLISH channel:123:typing {user_id: 456, typing: true}
PSub-->>WSG: Typing event
WSG->>C2: Push typing indicator
Note over C: User stops typing (no events for 3s)
Redis->>Redis: Key expires (TTL 5s)
PS->>PSub: PUBLISH channel:123:typing {user_id: 456, typing: false}
PSub-->>WSG: Typing event
WSG->>C2: Clear typing indicator
Optimization: Presence Bloom Filter
| Problem |
Solution |
Benefit |
| Large channels: 10K users, need to check who's online |
Bloom filter per channel in Redis |
Fast membership check (O(1) vs. O(N)) |
| False positives ("user might be online") |
Fallback to exact check in Redis hash |
Rare, acceptable tradeoff |
| Stale bloom filter |
Rebuild every 60 seconds |
Matches TTL of presence keys |
Interview Callout: Why Not Fan Out Presence to Everyone?
"Naive approach: broadcast user status to all workspace members. Problem: 10K workspace = 10K updates per status change. Solution: only fan out to shared channels. If Alice and Bob don't share channels, Bob doesn't get Alice's presence updates."
9. Search
Search Index Architecture
| Component |
Technology |
Purpose |
| Search engine |
Elasticsearch |
Full-text search, relevance ranking |
| Index structure |
One index per workspace |
Tenant isolation, independent scaling |
| Document schema |
{message_id, channel_id, user_id, text, attachments, created_at} |
Searchable fields |
| Indexing pipeline |
Kafka consumer → Elasticsearch bulk API |
Async, batch inserts (1000 msgs/batch) |
| Real-time indexing |
Message Service → Kafka → ES (latency: 1-5 sec) |
Near-real-time search |
Search Query Filters
| Filter |
Syntax |
Elasticsearch Query |
| Keyword |
budget proposal |
match: {text: "budget proposal"} |
| From user |
from:@alice |
term: {user_id: 123} |
| In channel |
in:#engineering |
term: {channel_id: 456} |
| Date range |
after:2024-01-01 before:2024-12-31 |
range: {created_at: {gte: "2024-01-01", lte: "2024-12-31"}} |
| Has attachment |
has:link or has:file |
exists: {field: "attachments"} |
| Exact phrase |
"quarterly review" |
match_phrase: {text: "quarterly review"} |
Search Ranking
| Factor |
Weight |
Justification |
| Recency |
High (decay function) |
Recent messages more relevant |
| Exact match |
High |
Exact phrase match ranked above partial |
| Channel membership |
Medium |
Boost messages from user's channels |
| Reaction count |
Low |
Popular messages slightly boosted |
| Thread replies |
Low |
Active discussions slightly boosted |
Indexing Pipeline
| Step |
Component |
Action |
| 1 |
Message Service |
Persist message to MySQL |
| 2 |
Message Service |
Enqueue to Kafka topic messages-for-indexing |
| 3 |
Search Indexer (Kafka consumer) |
Consume messages in batches (1000 msgs) |
| 4 |
Search Indexer |
Transform to ES document format |
| 5 |
Search Indexer |
Bulk insert to Elasticsearch (workspace index) |
| 6 |
Elasticsearch |
Refresh index (1-5 sec latency) |
Search Scalability
| Challenge |
Solution |
Scaling Strategy |
| Large workspaces |
Shard index by time (e.g., monthly shards) |
Query recent shard first, fallback to older |
| Hot queries |
Cache popular searches (Redis, 5 min TTL) |
Reduce ES load |
| Cross-workspace search |
Multi-index query (ES aliases) |
Federated search across workspaces |
| Reindexing |
Blue-green indexing (create new index, swap alias) |
Zero-downtime index updates |
Interview Callout: Real-Time vs. Accuracy Tradeoff
"Search has 1-5 sec lag (Kafka → ES indexing). Acceptable for search UX. Alternative: dual-write to ES from Message Service (faster but risks inconsistency). Choose async for reliability."
10. File Sharing
File Upload Flow
| Step |
Component |
Action |
| 1 |
Client |
Request upload URL: POST /files/upload-url {filename, size, mime_type} |
| 2 |
File Service |
Generate presigned S3 URL (valid for 15 min) |
| 3 |
Client |
Upload file directly to S3 via presigned URL (bypass server) |
| 4 |
S3 |
Trigger Lambda on upload (event notification) |
| 5 |
Lambda |
Virus scan (ClamAV), generate preview (images/PDFs) |
| 6 |
Lambda |
Write metadata to files table (file_id, url, size, status) |
| 7 |
Client |
Send message with file attachment: {text, file_ids: [...]} |
| 8 |
Message Service |
Store message with file references |
File Storage Schema
| Column |
Type |
Description |
| file_id |
BIGINT (PK) |
Unique file identifier |
| workspace_id |
BIGINT |
Tenant isolation |
| uploaded_by |
BIGINT |
User who uploaded |
| filename |
VARCHAR(255) |
Original filename |
| mime_type |
VARCHAR(100) |
image/png, application/pdf, etc. |
| size_bytes |
BIGINT |
File size (max 1GB) |
| s3_key |
VARCHAR(512) |
S3 object key |
| preview_url |
VARCHAR(512) |
Thumbnail/preview (NULL if not applicable) |
| status |
ENUM |
pending, scanning, ready, quarantined |
| created_at |
TIMESTAMP |
Upload timestamp |
File Preview Generation
| File Type |
Preview Strategy |
Tool |
| Images |
Generate 300x300 thumbnail + 1200x1200 preview |
ImageMagick / Sharp.js |
| PDFs |
Render first page as image (300 DPI) |
Ghostscript / pdf2image |
| Videos |
Extract frame at 1 sec + GIF preview (3 sec loop) |
FFmpeg |
| Text/Code |
Syntax-highlighted preview (first 50 lines) |
Prism.js |
| Office docs |
Convert to PDF, then generate image preview |
LibreOffice / Google Docs API |
| Archives (zip) |
List contents (filenames only) |
unzip -l |
Virus Scanning
| Step |
Action |
Tool |
| 1 |
Lambda triggered on S3 upload |
S3 event notification |
| 2 |
Download file from S3 (streaming) |
AWS SDK |
| 3 |
Scan with ClamAV (or third-party API) |
ClamAV / VirusTotal API |
| 4 |
If clean: set status = "ready" |
Update files table |
| 5 |
If infected: set status = "quarantined", delete from S3 |
Notify uploader via DM |
File Access Control
| Scenario |
Access Rule |
Implementation |
| File in public channel |
All workspace members can access |
Check workspace membership |
| File in private channel |
Only channel members can access |
Check channel membership via channel_members |
| File in DM |
Only DM participants can access |
Check DM participant list |
| File link shared externally |
Generate time-limited signed URL (1 hour) |
S3 presigned URL with expiry |
Interview Callout: Why Presigned URLs?
"Direct S3 upload via presigned URL avoids routing 1GB files through application servers. Reduces latency, saves bandwidth. File Service only handles metadata, not file bytes."
11. Scalability
WebSocket Gateway Scaling
| Metric |
Target |
Strategy |
| Connections per server |
100K connections |
Node.js with cluster mode (8 cores × 12.5K conn/core) |
| Gateway count |
50 servers for 5M concurrent |
Horizontal scaling (auto-scale based on conn count) |
| Load balancing |
Sticky sessions (IP hash) |
AWS ALB with stickiness enabled |
| Memory per connection |
~10 KB |
100K × 10 KB = 1 GB per server |
| Connection failover |
Client auto-reconnects |
Exponential backoff, fetch missed messages |
Database Sharding Strategy
| Data Type |
Shard Key |
Sharding Tool |
| Messages |
channel_id |
Vitess (MySQL sharding) |
| Channels |
workspace_id |
Vitess |
| Users |
user_id |
Vitess |
| Workspaces |
No sharding (small table) |
Single MySQL instance with replicas |
Workspace Isolation
| Component |
Isolation Strategy |
Benefit |
| Database |
Shard by workspace_id |
Large workspaces don't impact small ones |
| Redis Pub/Sub |
Dedicated cluster for mega workspaces (>10K users) |
Prevent noisy neighbor problem |
| Elasticsearch |
One index per workspace |
Independent scaling, easier to migrate |
| S3 files |
Bucket prefix by workspace: s3://bucket/workspace123/files/ |
Quota enforcement, cost tracking |
Caching Strategy
| Data |
Cache Location |
TTL |
Invalidation |
| Channel membership |
Redis: user:{user_id}:channels |
10 min |
On join/leave channel |
| Recent messages |
Redis: channel:{channel_id}:recent (last 50) |
5 min |
On new message |
| User profile |
Redis: user:{user_id}:profile |
30 min |
On profile update |
| Workspace metadata |
Redis: workspace:{workspace_id}:meta |
1 hour |
Rare updates |
| Search results |
Redis: search:{query_hash} |
5 min |
Time-based (no invalidation) |
Rate Limiting
| Operation |
Limit |
Window |
Enforcement |
| Send message |
100 messages |
Per minute |
Redis counter per user |
| Create channel |
10 channels |
Per hour |
Redis counter per user |
| File upload |
50 files |
Per hour |
Redis counter per user |
| Search query |
20 queries |
Per minute |
Redis counter per user |
| API calls |
1000 requests |
Per minute |
API Gateway rate limiter |
Monitoring & Metrics
| Metric |
Target |
Alert Threshold |
| Message latency (p99) |
<100ms |
>200ms |
| WebSocket connection count |
5M concurrent |
>80% capacity per server |
| Pub/Sub lag |
<50ms |
>500ms |
| Search query latency (p95) |
<500ms |
>2 sec |
| Database replication lag |
<1 sec |
>5 sec |
| File upload success rate |
>99% |
<95% |
Interview Callout: Bottleneck Analysis
"Initial bottleneck: database writes (6K msg/sec). Solution: shard by channel_id, use Vitess. Next bottleneck: Redis Pub/Sub fan-out (300K deliveries/sec). Solution: cluster Pub/Sub, dedicate cluster for mega channels. Final bottleneck: WebSocket gateway capacity (5M connections). Solution: horizontal scaling (50 servers @ 100K conn each)."
Summary
| Component |
Technology Choice |
Key Insight |
| Real-time delivery |
WebSocket + Redis Pub/Sub |
Sub-100ms latency for message fan-out |
| Message storage |
Vitess (MySQL sharded by channel_id) |
Append-only, time-sortable Snowflake IDs |
| Channel fan-out |
Fan-out on write (Pub/Sub to all gateways) |
Hybrid for mega channels (only push to online users) |
| Presence |
Redis with TTL (60s heartbeat) |
Fan out only to shared channels |
| Search |
Elasticsearch (one index per workspace) |
Async indexing via Kafka (1-5 sec lag acceptable) |
| File sharing |
S3 with presigned URLs |
Direct upload bypasses app servers |
| Scalability |
100K connections per gateway server |
Workspace isolation prevents noisy neighbors |
Extended Topics for Deep Dive
- Message editing/deletion: Append tombstone records, show "edited" label, maintain audit log
- Message reactions: Store as JSON in message row, update via atomic increment (Redis + MySQL)
- Read receipts: Track
last_read_msg_id per user per channel, fan-out read updates via Pub/Sub
- Push notifications: APNs (iOS), FCM (Android), register device tokens, send on @mention or DM
- Webhooks/integrations: Outgoing webhooks (post to external URLs), incoming webhooks (receive from external), OAuth flow
- E2E encryption (Discord-style): Voice/video uses WebRTC with DTLS-SRTP, text messages not E2E by default (server can read for search/moderation)
- Voice/video channels: WebRTC for P2P (small groups), SFU (Selective Forwarding Unit) for large groups, TURN server for NAT traversal
- Workspace limits: Message retention (90 days for free, unlimited for paid), storage quota (5GB free, unlimited paid), API rate limits