← 2026-04-20 📂 All Days 2026-04-22 →

🏗️ 系统设计 / System Design — Day 28

Design a Chat System (WhatsApp/Slack)

难度 / Difficulty: Advanced | 阶段 / Phase: Growth | 预计阅读 / Read time: 3 min

🧠 真实场景 / Real-World Scenario

想象你在设计一个即时通讯系统,目标是支持 5亿用户,每天发送 500亿条消息(就像WhatsApp)。你的系统需要支持:一对一私聊、群组聊天(最多500人)、消息状态(已发送/已送达/已读)、在线状态显示、文件/图片传输。

Imagine you're designing a real-time messaging system to support 500M users with 50B messages/day (think WhatsApp). Requirements: 1:1 chat, group chat (up to 500 members), message status (sent/delivered/read), online presence, and file/media sharing.


📐 Architecture Overview

┌─────────────────────────────────────┐ │ Client (Mobile/Web) │ └──────────────┬──────────────────────┘ │ WebSocket (persistent) ┌──────────────▼──────────────────────┐ │ API Gateway / LB │ └──────┬──────────────┬───────────────┘ │ │ ┌────────────────▼──┐ ┌──────▼──────────────┐ │ Chat Service │ │ Presence Service │ │ (WebSocket Mgr) │ │ (Redis Pub/Sub) │ └────────┬──────────┘ └──────┬───────────────┘ │ │ ┌─────────────▼──────────┐ ┌──────▼───────────────┐ │ Message Queue (Kafka) │ │ Redis (Online State)│ └─────────────┬──────────┘ └──────────────────────┘ │ ┌─────────────▼──────────────────────────────┐ │ Message Processor │ │ - fan-out to recipients │ │ - push notification if offline │ └───┬────────────────────────┬───────────────┘ │ │ ┌────────▼──────────┐ ┌────────▼───────────────┐ │ Message DB │ │ Push Notification │ │ (Cassandra) │ │ Service (APNs/FCM) │ └───────────────────┘ └────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ Media Service │ │ - S3 for storage, CDN for delivery │ │ - Pre-signed URLs for upload/download │ └──────────────────────────────────────────────────┘

⚡ Key Design Decisions / 关键设计决策

#### 1. WebSocket vs HTTP Polling

用WebSocket,不用轮询。

WebSocket keeps a persistent bidirectional connection — ideal for real-time messaging. HTTP long-polling (Comet) works but wastes resources. Server-Sent Events (SSE) is one-way only.

选WebSocket的原因: 延迟低(< 100ms),服务器可主动推送,连接复用减少握手开销。

#### 2. Message Storage: Cassandra (not MySQL)

  • 写入极重 (write-heavy): 每秒数百万条消息
  • 按 (chat_id, message_id) 分区,天然支持时间范围查询
  • 高可用,无单点故障
  • Schema:

```sql

CREATE TABLE messages (

chat_id UUID,

message_id TIMEUUID, -- sortable by time

sender_id UUID,

content TEXT,

type TEXT, -- 'text', 'image', 'file'

status TEXT, -- 'sent', 'delivered', 'read'

PRIMARY KEY (chat_id, message_id)

) WITH CLUSTERING ORDER BY (message_id DESC);

```

#### 3. Message Fan-Out Strategy

For 1:1 chat: simple — push to recipient's WebSocket connection.

For group chat (up to 500 members): fan-out on write via Kafka.

  • Message lands in Kafka topic
  • Consumer service fans out to each member's inbox
  • Why Kafka? Durability, replay, and decouples producers from consumers

群组消息扇出 (fan-out on write) vs 扇出读取 (fan-out on read):

Fan-out on write = 写入时复制到每人收件箱(适合中小群组)

Fan-out on read = 读取时聚合(适合超大群组/Twitter模式)

500人以内:写时扇出 — 读取延迟更低,体验更好。

#### 4. Online Presence

  • Use Redis pub/sub per user channel
  • Client sends heartbeat every 5s; server marks user online in Redis with TTL=10s
  • Subscribe to friend's channel to receive presence updates
  • 问题: 5亿用户在线状态 → Redis Cluster,按用户ID分片

❌ 别踩这些坑 / Common Mistakes

❌ 误区✅ 正确做法
用MySQL存消息用Cassandra/HBase — 写入QPS太高
HTTP轮询实现"实时"WebSocket长连接
同步fan-out到群成员异步Kafka消费
不做消息去重每条消息UUID + 客户端幂等性
媒体文件存数据库S3 + CDN + pre-signed URL
忘记离线消息推送通知 (APNs/FCM) + 消息持久化

🔢 Back-of-Envelope / 估算

DAU: 500M users Messages per user per day: 40 Total messages/day: 20B Peak QPS: 20B / 86400 * 2 (peak factor) ≈ 460K msg/s Storage per message: ~100 bytes Daily storage: 20B * 100 = 2TB/day 5-year retention: ~3.6PB (need compression + tiering) WebSocket connections (peak): ~100M concurrent → Need ~10,000 chat servers (each handles ~10K connections)

📚 References


🧒 ELI5

把聊天系统想成邮局:你发信(消息)→ 邮局(服务器)分拣 → 立刻送到收件人信箱(WebSocket)。如果收件人不在家(离线),就留张纸条(推送通知)让他回来取。Cassandra就像超大容量的信件档案室,永不丢信。
Think of it like a postal service: you drop a letter → post office (server) sorts it → delivers to recipient's mailbox instantly (WebSocket). If they're offline, leave a note (push notification). Cassandra is the giant mail archive that never loses a letter.

💻 算法 / Algorithms — Day 28

#739 Daily Temperatures (Medium) — Stack Pattern (5/7)

模式 / Pattern: 单调栈 (Monotonic Stack) | 阶段 / Phase: Growth | 预计阅读 / Read time: 4 min

🧩 单调栈 (Monotonic Stack) — 5/7 — building on the template from Day 23 (Valid Parentheses), Day 24 (Min Stack), Day 26 (Evaluate Reverse Polish Notation)

今天是单调栈模式的第5题。前面我们用栈做了括号匹配、最小值追踪、表达式求值。今天进入单调栈最经典的用法:找下一个更大元素 (next greater element)。

Today is the 5th problem in the stack block. We've used stacks for parentheses, min tracking, and expression evaluation. Now we hit the most classic monotonic stack use case: finding the next greater element.


🔗 Links


🌍 Real-World Analogy / 现实类比

你站在一排人群中,每个人都想知道:后面什么时候会出现比我更高的人?

暴力解法:每个人回头一个个数。

聪明解法:维护一个"等待队列"——还没找到更高者的人先排队,一旦出现更高的,通知所有比他矮的人。

You're in a queue; each person wants to know: when will someone taller appear behind me?

Brute force: each person looks back one by one.

Smart: maintain a "waiting list" — those who haven't found someone taller yet wait in stack. When a taller person appears, notify everyone shorter.


📋 Problem

Input:  temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
Output: [1,  1,  4,  2,  1,  1,  0,  0]

For each day, find how many days until a warmer temperature. If none, return 0.


🗺️ Mapping to Pattern Template

# Template (from Day 22 intro):
# stack = []
# for i, num in enumerate(arr):
#     while stack and arr[stack[-1]] < num:
#         idx = stack.pop()
#         result[idx] = num  # next greater
#     stack.append(i)

# Daily Temperatures — direct application:
# arr = temperatures
# result[idx] = i - idx  (distance, not value)

这题 vs 模板的区别 / Difference from template:

  • Template records the "next greater value" → this problem records the distance (i - idx)
  • Everything else is identical: maintain decreasing stack of indices

✅ Solution with Trace

def dailyTemperatures(temperatures: list[int]) -> list[int]:
    n = len(temperatures)
    result = [0] * n
    stack = []  # stack of indices, temperatures[stack top] is always decreasing
    
    for i, temp in enumerate(temperatures):
        # Pop all indices where we found a warmer day
        while stack and temperatures[stack[-1]] < temp:
            prev_idx = stack.pop()
            result[prev_idx] = i - prev_idx  # days to wait
        stack.append(i)
    
    # Remaining indices in stack → no warmer day found, result stays 0
    return result

逐步追踪 / Step-by-step trace:

temperatures = [73, 74, 75, 71, 69, 72, 76, 73] index: [0, 1, 2, 3, 4, 5, 6, 7 ] i=0, temp=73: stack=[] → push 0. stack=[0] i=1, temp=74: 74>73(idx0)→result[0]=1; push 1. stack=[1] i=2, temp=75: 75>74(idx1)→result[1]=1; push 2. stack=[2] i=3, temp=71: 71<75 → just push 3. stack=[2,3] i=4, temp=69: 69<71 → just push 4. stack=[2,3,4] i=5, temp=72: 72>69(idx4)→result[4]=1 72>71(idx3)→result[3]=2 72<75 → stop, push 5. stack=[2,5] i=6, temp=76: 76>72(idx5)→result[5]=1 76>75(idx2)→result[2]=4; push 6. stack=[6] i=7, temp=73: 73<76 → push 7. stack=[6,7] result = [1, 1, 4, 2, 1, 1, 0, 0] ✓

⏱️ Complexity: Time O(n) — each element pushed/popped at most once. Space O(n).


🔄 举一反三 / Pattern Variations in This Block

ProblemStack UsageWhat Changes
#20 Valid Parentheses (1/7)matching bracketspop on match
#155 Min Stack (2/7)dual stack for mintrack minimum
#150 Eval RPN (3/7)operator/operandpop two, compute
#739 Daily Temps (5/7)monotonic decreasingnext greater
#853 Car Fleet (6/7)monotonicmerge fleets
#84 Histogram (7/7)monotonicarea calculation

核心变体 / Core variation to recognize:

"Next greater/smaller" → monotonic stack (today's pattern)

"Matching/balanced" → bracket stack (Day 23)

"Evaluate expression" → operand/operator stack (Day 26)


📚 References


🧒 ELI5

想象你在排队买票,每个人记录"我前面什么时候会有空位(比我快的人离开)"。我们维护一个"还在等的人"的列表,按顺序排。一旦有人离开(温度更高),就告诉所有等得比他久的人"你等了几天"。这样每个人最多进出列表一次,比一个个回头数快多了!

Imagine everyone in line tracking "when will someone faster than me appear?" We keep a list of people still waiting in order. When someone faster shows up, we notify everyone slower. Each person enters/leaves the list at most once — way faster than checking one by one!

🗣️ 软技能 / Soft Skills — Day 28

Build vs Buy Decision Making

难度 / Level: Senior/Staff | 类别 / Category: Decision Making | 预计阅读 / Read time: 2 min

🎯 为什么这个问题很重要 / Why This Question Matters

Build vs buy是每个Senior/Staff工程师都会遇到的战略决策。面试官想看:你会不会权衡短期成本 vs 长期维护,控制权 vs 速度,差异化价值 vs 通用功能。这不只是技术问题,更是工程判断力的体现。

Build vs buy is a strategic decision every Senior/Staff engineer faces. Interviewers want to see: can you weigh short-term cost vs long-term maintenance, control vs speed, differentiating value vs commodity functionality? This tests engineering judgment, not just technical skills.


⭐ STAR Framework

Situation / 背景:

"我们在构建实时数据管道,需要一个消息队列。团队有人提议自研,也有人提议用Kafka。这是个200万用户的产品,但消息队列不是我们的核心竞争力。"
"We were building a real-time data pipeline and needed a message queue. Some wanted to build one; others wanted Kafka. 2M users, but messaging wasn't our core differentiator."

Task / 任务:

评估两个方案的真实成本,给出有数据支撑的建议。

Action / 行动:

我做了一个快速分析框架:
1. 核心 vs 非核心 — 消息队列是通用基础设施,不是我们的差异化竞争力
2. 总拥有成本 (TCO) — 自研:3个工程师×6个月 + 永续维护。Kafka:2周集成 + 运维成本(~$500/月托管服务)
3. 风险矩阵 — 自研:稳定性风险、没有社区支持。Kafka:供应商依赖、少量定制限制
4. 团队能力 — 没有分布式系统专家,自研风险高
I built a quick decision framework:
1. Core vs commodity — message queuing is generic infrastructure, not our differentiation
2. TCO — build: 3 engineers × 6 months + perpetual maintenance vs buy: 2-week integration + ~$500/month managed service
3. Risk matrix — build: stability risk, no community. Buy: vendor dependency, some customization limits
4. Team capabilities — no distributed systems experts on team
我建议采用托管Kafka(Confluent Cloud)。节省了约18个工程师月,让团队专注于产品功能。

Result / 结果:

集成用了两周,比预期早6个月上线。运行了1年没有重大故障。后来我把这个框架整理成团队的"Make vs Buy"决策模板,被多个团队复用。
Integration took 2 weeks, shipped 6 months early. Zero major incidents in year one. I documented the framework as a team "Make vs Buy" template — adopted by 3 other teams.

❌ Bad Answer vs ✅ Good Answer

❌ 踩坑回答:

"我们选择了Kafka因为它很流行,大家都在用。"

问题: 没有展示决策过程,没有权衡,听起来像随大流。

✅ 高分回答:

"我建立了一个框架来分析核心竞争力、TCO和团队风险,然后带着数据向stakeholders推荐了Kafka。结果节省了18个工程师月,让我们更早交付了用户真正需要的功能。"

🚀 Senior/Staff级别加分点

  1. 量化影响 — 不只说"省了时间",要说"节省18个工程师月 = $X万美元"
  2. 展示框架 — 面试官想看你有可复用的决策方法,不是ad-hoc
  3. 考虑长期维护 — "我们今天能构建它,但谁来维护?"
  4. 识别错误的build理由 — "我们想完全控制" → 通常是overengineering的信号
  5. 何时应该build — 当功能是核心差异化价值,或有严格合规要求(数据主权),或外部方案有根本性gap

经典Build vs Buy框架:

Build when:
✅ Core differentiator (your competitive moat)
✅ Existing solutions have fundamental gaps
✅ Compliance/data sovereignty requirements
✅ Team has the expertise + bandwidth

Buy when:
✅ Commodity functionality
✅ No differentiation value
✅ High quality options exist
✅ Speed to market matters
✅ Team lacks domain expertise

💡 Key Takeaways / 核心要点

  • 🎯 不要build ego trip — "因为自己写的更好" 不是理由
  • 📊 TCO要算全 — 工程时间 + 机会成本 + 永续维护 + 招聘专才
  • 🔑 核心竞争力优先 — 只在用户愿意为之付钱的差异化功能上build
  • 📝 记录决策 — ADR (Architecture Decision Record) 让团队对齐,避免反复争论

📚 References


🧒 ELI5

自己造汽车还是买车?你要上班,开车去。你是汽车专家吗?不是。造车需要多久?1年。买车需要多久?1周。除非你是赛车手(汽车是你的核心竞争力),否则就买!工程决策也一样——把精力放在你做得比别人更好的地方。

Build a car or buy one? You need to get to work. Are you a car expert? No. How long to build? 1 year. Buy? 1 week. Unless you're a race car driver (cars ARE your competitive edge), just buy! Engineering is the same — spend your energy where you're uniquely better than others.

🎨 前端 / Frontend — Day 28

Promises, async/await — Async Control Flow

类别 / Category: JavaScript Core | Week: 5 | 预计阅读 / Read time: 2 min

🌍 真实场景 / Real Scenario

你在做一个 dashboard,需要同时加载用户数据、最新订单、和通知列表。如果按顺序串行请求,会浪费 3x 时间。你需要理解 Promise 并发控制 才能写出生产级的异步代码。

You're building a dashboard that needs to load user profile, latest orders, and notifications. Sequential requests waste 3x the time. Understanding Promise concurrency control is essential for production-grade async code.


🧪 猜猜输出什么? / What does this output?

async function fetchDashboard(userId: string) {
  console.log("A");
  
  const user = await fetch(`/api/users/${userId}`);
  console.log("B");
  
  const [orders, notifications] = await Promise.all([
    fetch(`/api/orders?userId=${userId}`),
    fetch(`/api/notifications?userId=${userId}`)
  ]);
  console.log("C");
  
  return { user, orders, notifications };
}

fetchDashboard("123");
console.log("D");

What logs first?

  • A) A → B → C → D
  • B) A → D → B → C
  • C) D → A → B → C
  • D) A → B → D → C
答案 / Answer: B — async functions run synchronously until the first await. So "A" logs, then the function suspends at await fetch(...), and "D" logs immediately. When the fetch resolves, "B" logs, then "C" after Promise.all.

🔑 Core Concepts

#### Promise 状态机 / Promise State Machine

pending → fulfilled (resolve) → rejected (reject) Once settled, state never changes.

#### 三种并发模式 / Three Concurrency Patterns

// ❌ Pattern 1: Sequential (串行) — SLOW
const user = await fetchUser(id);      // waits 200ms
const orders = await fetchOrders(id);  // waits another 300ms
// Total: 500ms — unnecessary!

// ✅ Pattern 2: Parallel (并行) — FAST
const [user, orders] = await Promise.all([
  fetchUser(id),    // both start at the same time
  fetchOrders(id),  // 
]);
// Total: max(200ms, 300ms) = 300ms 🚀

// ⚠️ Pattern 3: Promise.allSettled — when you need ALL results
// even if some fail (e.g., non-critical widgets)
const results = await Promise.allSettled([
  fetchUser(id),
  fetchOrders(id),
  fetchNotifications(id),
]);
results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value);
  else console.error(r.reason);
});

❌ Common Mistakes / 常见错误

// ❌ Mistake 1: await inside map() — sequential, not parallel
const data = await Promise.all(
  ids.map(id => await fetchUser(id))  // SyntaxError! can't await inside non-async map
);

// ✅ Correct: pass Promise directly
const data = await Promise.all(
  ids.map(id => fetchUser(id))  // no await inside map
);

// ❌ Mistake 2: unhandled rejection
const result = await Promise.all([fetchA(), fetchB()]); // if B rejects, A's result lost!

// ✅ Correct: use allSettled or try/catch per request
const [a, b] = await Promise.allSettled([fetchA(), fetchB()]);

// ❌ Mistake 3: missing error handling
async function loadData() {
  const data = await fetchData(); // throws? crashes silently!
  return data;
}

// ✅ Correct:
async function loadData() {
  try {
    return await fetchData();
  } catch (err) {
    console.error("Failed to load:", err);
    return null; // or throw to parent
  }
}

🎯 When to Use / When NOT to Use

Use Promise.all when:
✅ All requests are independent
✅ You need ALL to succeed (fail fast if any fails)
✅ Performance matters (dashboard loads, batch ops)

Use Promise.allSettled when:
✅ Some failures are acceptable
✅ Non-critical widgets/sections
✅ You need all results regardless of failure

Use sequential await when:
✅ Request B depends on result of A
✅ Ordering matters (database transactions)
✅ You need to avoid rate limiting

Avoid:
❌ await in a for loop when items are independent
❌ new Promise() wrapping async functions (anti-pattern)
❌ .then() chains when async/await is available (less readable)

🔥 Production Pattern: TypeScript async with error handling

type Result<T> = { data: T; error: null } | { data: null; error: Error };

async function safeAsync<T>(promise: Promise<T>): Promise<Result<T>> {
  try {
    const data = await promise;
    return { data, error: null };
  } catch (err) {
    return { data: null, error: err instanceof Error ? err : new Error(String(err)) };
  }
}

// Usage — no try/catch needed at call site:
const { data: user, error } = await safeAsync(fetchUser("123"));
if (error) return <ErrorState message={error.message} />;

📚 References


🧒 ELI5

Promise就像点外卖的收据。你点了3个菜(3个请求)。串行 = 等第一个菜上了再点第二个,太慢。Promise.all = 同时点3个菜,哪个先好哪个先上,全部好了再吃。allSettled = 就算有菜没做出来,你也能知道哪些成功了哪些失败了。

A Promise is like a takeout receipt. You ordered 3 dishes. Sequential = wait for dish 1 before ordering dish 2 (slow!). Promise.all = order all 3 at once, eat when all are ready. allSettled = even if one fails, you know which succeeded and which didn't.

🤖 AI — Day 28

MCP (Model Context Protocol) — The USB for AI

类别 / Category: Agents | 阶段 / Phase: Growth | 预计阅读 / Read time: 2 min

🧠 直觉解释 / Intuitive Explanation

想象你的电脑没有USB标准。每个设备都需要专用接口——鼠标一种、键盘一种、硬盘一种。你每次想接新设备都要改电脑硬件。这很蠢对吧?

MCP (Model Context Protocol) 就是AI工具的USB标准。

它由Anthropic于2024年底发布,解决了一个核心问题:AI模型(LLMs)如何以统一的方式连接到外部工具、数据库、API?以前每家公司都要自己写集成代码,现在有了MCP,任何AI应用都能"即插即用"地接入MCP服务器。

Imagine your computer had no USB standard. Every device needs a custom port. Annoying, right?

MCP is the USB standard for AI tools.

Released by Anthropic in late 2024, it solves a core problem: how can AI models connect to external tools, databases, and APIs in a unified way? Before MCP, every company wrote custom integration code. Now any AI app can "plug in" to any MCP server.


⚙️ How It Works / 工作原理

┌─────────────────────────────────────────────────────┐ │ MCP Architecture │ │ │ │ ┌──────────────┐ MCP Protocol ┌──────────┐ │ │ │ AI Host │◄──────────────────►│ MCP │ │ │ │ (Claude, │ JSON-RPC over │ Server │ │ │ │ Cursor, │ stdio / HTTP/SSE │ │ │ │ │ Copilot) │ │ Exposes: │ │ │ └──────────────┘ │ • Tools │ │ │ │ • Resources│ │ MCP Client │ • Prompts │ │ │ (built into host) └──────────┘ │ │ │ │ Examples of MCP Servers: │ │ • Filesystem (read/write local files) │ │ • GitHub (search repos, create PRs) │ │ • PostgreSQL (query databases) │ │ • Slack (send messages, search) │ │ • Brave Search (web search) │ └─────────────────────────────────────────────────────┘

三个核心原语 / Three Core Primitives:

  1. Tools (工具) — Functions the AI can call (e.g., search_web, create_file)
  2. Resources (资源) — Data the AI can read (e.g., file contents, database rows)
  3. Prompts (提示词) — Reusable prompt templates exposed by the server

🐍 Runnable Python Example

# pip install mcp anthropic

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types

# Build a minimal MCP server that exposes a weather tool
app = Server("weather-server")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="get_weather",
            description="Get current weather for a city",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "get_weather":
        city = arguments["city"]
        # In real code: call a weather API here
        return [types.TextContent(
            type="text",
            text=f"Weather in {city}: 72°F, sunny ☀️"
        )]

# Run the server (Claude Desktop can now connect to it!)
if __name__ == "__main__":
    import asyncio
    asyncio.run(stdio_server(app))

Connect this to Claude Desktop via claude_desktop_config.json and Claude can now call get_weather as a native tool!


🌐 Applications / 应用场景

Use CaseMCP Server
AI reads/writes your codebaseFilesystem MCP
AI creates GitHub PRsGitHub MCP
AI queries your databasePostgreSQL MCP
AI searches the webBrave Search MCP
AI books calendar eventsGoogle Calendar MCP
AI reads company docsNotion/Confluence MCP

为什么重要 / Why It Matters:

  • 标准化 → 生态繁荣(类比:npm为JS,MCP为AI工具)
  • 安全 → MCP服务器可控制权限,AI不能随意访问
  • 可组合 → 一个AI客户端可同时接多个MCP服务器

⚡ MCP vs Function Calling

Function CallingMCP
标准化各厂商自定义统一协议
服务器端部署不支持支持
资源访问无支持 (Resources)
跨模型兼容❌✅

📚 References


🧒 ELI5

以前AI想用工具,就像每次换手机都要重新教它所有的按钮。MCP就是给AI装了个"学徒手册":任何工具只要按格式写好,AI就能立刻会用,不用每次重新配置。就像学会了用筷子,就能夹任何菜。

Before MCP, teaching an AI a new tool was like rewriting the instruction manual every time. MCP is a standard instruction format — any tool written to spec can be used by any AI immediately. Like learning chopsticks: once you know the technique, you can pick up any food.