← 2026-04-14 📂 All Days 2026-04-16 →

🏗️ 系统设计 / System Design — Day 21: Design a Notification System

📊 Day 24 | Phase: Growth | Topic: System Design Problems

想象这个场景 / The Scenario

想象你在 Airbnb 工作。用户订了一个民宿,系统需要同时发送:

  • 📱 Push notification(App 通知)
  • 📧 Email(预订确认)
  • 💬 SMS(支付成功短信)

You're at Airbnb. A user books a listing — the system needs to simultaneously send a push notification, confirmation email, and SMS payment receipt. How do you design this at scale?


架构图 / Architecture Diagram

Clients (iOS/Android/Web) │ ▼ [API Gateway] │ ▼ [Notification Service] / | \ ▼ ▼ ▼ [Push] [Email] [SMS] Worker Worker Worker │ │ │ ▼ ▼ ▼ [APNS] [SendGrid] [Twilio] [FCM] ▲ [Message Queue] ←── [Event Sources] (Kafka/SQS) (Order Service, Payment Service, etc.) [Notification DB] ←── stores delivery status [User Preference] ←── opt-in/out per channel [Rate Limiter] ←── prevent spam

核心设计决策 / Key Design Decisions

1. 为什么用消息队列?/ Why Message Queue?

问题 / Problem: 通知系统是下游依赖 — 如果 Email 服务挂了,不应该影响核心业务流程(下单、支付)。

解法 / Solution: 用 Kafka/SQS 解耦生产者和消费者。生产者(订单服务)发消息就完事,消费者(通知工作进程)异步处理。

关键权衡 / Key Tradeoff: 异步 = 高可用 + 高吞吐,但牺牲了实时性(最终一致)。对通知场景完全可接受。

2. 用户偏好表 / User Preference Table

CREATE TABLE user_notification_preferences (
  user_id       BIGINT,
  channel       ENUM('push', 'email', 'sms'),
  notification_type VARCHAR(50),  -- 'marketing', 'transactional'
  enabled       BOOLEAN DEFAULT true,
  PRIMARY KEY (user_id, channel, notification_type)
);

发送前先查这张表 — 用户关闭了营销邮件就别发。

Check this table before sending — respect user opt-outs.

3. 幂等性 / Idempotency

Worker 崩溃重启后可能重复发送 → 每条通知有唯一 notification_id,发送成功后写入 DB。再次处理时先查 DB,已发送则跳过。

Worker restarts may cause duplicate sends → each notification has a unique notification_id. Mark as sent in DB; skip if already delivered.

4. 推送三方服务 / Third-Party Push Services

  • iOS: Apple Push Notification Service (APNS)
  • Android: Firebase Cloud Messaging (FCM)
  • Email: SendGrid / AWS SES
  • SMS: Twilio / AWS SNS

数据量估算 / Scale Estimation

  • DAU: 10M users
  • 每用户每天平均 3 条通知 → 30M notifications/day
  • QPS: 30M / 86400 ≈ 350 notifications/sec (peak 5x = ~1750/sec)
  • 消息队列分区 / Kafka partitions: 按 user_id % N 分区确保顺序

别踩这个坑 / Common Mistakes

❌ 错误做法✅ 正确做法
同步发送通知(阻塞主流程)异步队列解耦
不记录发送状态写 DB + 幂等处理
忽略用户偏好发送前查 preference 表
不限速Rate limiter 防骚扰
只有一个推送渠道失败重试 + fallback(push → SMS)

📚 References

  • https://systemdesign.one/notification-system-design/
  • https://aws.amazon.com/blogs/mobile/how-to-build-a-scalable-notification-system/
  • https://engineering.fb.com/2015/12/03/ios/delivering-billions-of-messages-instantly/

🧒 ELI5

想象你开了一家餐厅。厨师做好菜了,不能亲自跑去告诉每桌客人——他只需要把菜放到传菜台(消息队列),服务员(Worker)去各桌通知。有人不吃辣就不发辣的(用户偏好),菜已经送到就不再送(幂等性)。

Think of it like a restaurant. The chef (core service) puts finished dishes on a conveyor (message queue). Waiters (workers) deliver them to the right tables (push/email/SMS). If a customer said "no spicy food" (user preference), skip it. If the dish was already delivered, don't deliver again (idempotency).

💻 算法 / Algorithms — Day 22: #155 Min Stack (Medium) — Stack

📊 Day 24 | Phase: Growth | Pattern: 单调栈模式 (2/7)

🧩 单调栈模式 (2/7) — 在 Day 23 的模版基础上延伸

Yesterday (#20 Valid Parentheses) introduced the Stack pattern for matching. Today's Min Stack is a variation: instead of using a stack for matching, we use a stack to augment data retrieval — specifically, tracking the minimum in O(1).

Pattern recap / 模版回顾:

# Core stack pattern: push/pop with invariant maintenance
stack = []
# Each element: (value, auxiliary_info)  ← today's twist

🔗 Links


现实类比 / Real-World Analogy

想象你在整理一摞文件,每次放一份文件上去,你都想随时知道"这摞里最薄的文件是哪本"——不翻找,O(1) 取出。

Imagine stacking folders on your desk. At any moment you need to instantly know the thinnest folder in the stack — without searching through all of them.


问题描述 / Problem

设计一个栈,支持 push、pop、top,以及 O(1) 时间 获取栈内最小值 getMin()。

Design a stack that supports push, pop, top, and getMin() in O(1) time.

MinStack stack = new MinStack(); stack.push(-2); stack.push(0); stack.push(-3); stack.getMin(); # → -3 stack.pop(); stack.top(); # → 0 stack.getMin(); # → -2

映射到模式 / Map to Pattern

关键洞察 / Key Insight: 普通栈只记录值,Min Stack 在每个节点额外记录"到目前为止的最小值"。

Normal stack stores values. Min Stack stores (value, min_so_far) pairs — so every element "knows" the minimum at the time it was pushed.


Python 解法 + 执行追踪 / Solution + Trace

class MinStack:
    def __init__(self):
        # Each entry: (value, current_min_at_this_point)
        self.stack = []

    def push(self, val: int) -> None:
        # If stack is empty, min is val itself
        # Otherwise, min is min(val, top element's recorded min)
        current_min = val if not self.stack else min(val, self.stack[-1][1])
        self.stack.append((val, current_min))

    def pop(self) -> None:
        self.stack.pop()

    def top(self) -> int:
        return self.stack[-1][0]

    def getMin(self) -> int:
        return self.stack[-1][1]

执行追踪 / Trace:

push(-2): stack = [(-2, -2)] # min = -2 push(0): stack = [(-2,-2), (0,-2)] # min still -2 push(-3): stack = [(-2,-2),(0,-2),(-3,-3)] # new min = -3 getMin(): → stack[-1][1] = -3 ✅ pop(): stack = [(-2,-2), (0,-2)] getMin(): → stack[-1][1] = -2 ✅ (min "restored" automatically!)

神奇之处 / The Magic: When we pop -3, the min automatically reverts to -2 — because each element carries its own "min context". No extra work needed!


复杂度 / Complexity

  • Time: O(1) for all operations — push, pop, top, getMin
  • Space: O(n) — extra space for storing min alongside each element

与其他模式变体的对比 / Compare to Block Problems

ProblemStack StoresKey Twist
#20 Valid ParenthesesOpening bracketsMatch on close
#155 Min Stack ←(val, min) pairsAugment with metadata
#150 Eval RPN (next)NumbersPop two, compute, push result
#739 Daily TemperaturesIndicesMonotonic decreasing values

举一反三 / Pattern Connections

这个"携带辅助信息"的技巧在栈中非常通用:

  • Max Stack — 同理,存 (val, max_so_far)
  • 频率栈 (LFU Cache 变体) — 存 (val, frequency)
  • 单调栈 (#739) — 存索引,维护单调性

The "carry auxiliary info" trick generalizes broadly:

  • Max Stack → store (val, max_so_far)
  • Daily Temperatures → store indices, maintain monotonic property

📚 References

  • https://leetcode.com/problems/min-stack/editorial/
  • https://neetcode.io/problems/minimum-stack
  • https://www.geeksforgeeks.org/design-a-stack-that-supports-getmin-in-o1-time-and-o1-extra-space/

🧒 ELI5

每次往盒子里放玩具,你都在玩具上贴一个便利贴,写上"到目前为止,这箱子里最小的玩具是哪个"。想知道最小的?直接看最上面那个玩具的便利贴——不用翻箱子!

Every time you put a toy in the box, write a sticky note on it: "the smallest toy in the box so far is ___." Want to know the minimum? Just read the note on the top toy — no digging needed!

🗣️ 软技能 / Soft Skills — Day 21: Conflict Resolution

📊 Day 24 | Phase: Growth | Level: Senior/Staff

为什么这很重要?/ Why This Matters

在 Senior/Staff 级别,技术问题往往只是表象,真正的挑战在于"人"。面试官问冲突解决,不是想听你如何"和稀泥",而是想看你如何把冲突转化为建设性的讨论,并最终达成共识。

At the Senior/Staff level, technical problems are often just symptoms of human alignment issues. Interviewers ask about conflict resolution to see if you can turn friction into constructive dialogue and reach a consensus, rather than just avoiding the argument.


STAR 拆解 / STAR Breakdown

🌟 Situation (情境)

描述冲突的背景。必须是工作相关的实质性冲突(如架构选择、资源分配),而不是个人恩怨。

Describe the context. It must be a substantive, work-related conflict (e.g., architectural choices, resource allocation), not a personal petty argument.

Example: "前端团队想用 GraphQL 加速开发,而后端团队坚持用 REST 以保持系统稳定性,双方僵持不下。"

🎯 Task (任务)

你的角色是什么?你需要达成什么目标?

What was your role and the goal?

Example: "作为 Tech Lead,我需要打破僵局,确保项目按时交付,同时兼顾开发效率和系统稳定。"

🚀 Action (行动) - 核心部分

你是如何解决的?强调倾听、客观数据和寻找共同点。

How did you solve it? Emphasize listening, objective data, and finding common ground.

  1. De-escalate (降温): 把讨论从"谁对谁错"转移到"我们要解决什么问题"。
  2. Gather Data (收集数据): 建立客观的评估标准(如性能测试、开发时间估算)。
  3. Compromise/Align (妥协/对齐): 找到双赢方案,或者基于数据做出艰难决定并让各方理解(Disagree and Commit)。

🏆 Result (结果)

冲突解决了没?项目成功了吗?学到了什么?

Was the conflict resolved? Did the project succeed? What did you learn?

Example: "我们决定先在非核心服务上试点 GraphQL。项目按时上线,双方都对结果满意。我学到了尽早引入客观评估标准能有效避免情绪化争论。"


❌ 错误示范 vs ✅ 正确示范

❌ Bad✅ Good
"我是对的,所以我强迫他们听我的。" (Too aggressive)"我组织了白板会议,列出双方方案的优缺点。" (Collaborative)
"我去找老板,让老板来定夺。" (Lacks leadership)"我们定义了成功指标,用数据说话。" (Data-driven)
"我们各退一步,随便选了个折中方案。" (Weak compromise)"我们找到了既能满足前端需求,又不需要后端大改的方案。" (Win-win)

💡 Senior/Staff 专属建议 / Senior Tips

  • Focus on the "Why": 冲突往往源于对底层目标的不一致。前端想要速度,后端想要稳定。找出这些 underlying motivations。
  • Disagree and Commit: 展现你理解即使你的方案没被采纳,你也能全力支持团队的决定。
  • Systemic Fixes: 冲突解决后,你有没有建立新的流程或规范,防止类似冲突再次发生?(如:引入 RFC 流程)。

📚 References

  • https://igotanoffer.com/blogs/tech/behavioral-interview-questions-conflict
  • https://blog.pragmaticengineer.com/resolving-engineering-conflicts/

🧒 ELI5

两个小朋友为了玩哪个玩具吵架。你不是去抢玩具,也不是去告老师,而是问他们:"你们到底想玩什么游戏?"然后发现一个人想玩搭房子,一个人想玩开汽车,于是你提议:"我们用积木搭一个停车场吧!"

Two kids are fighting over a toy. Instead of grabbing the toy or telling the teacher, you ask: "What game do you actually want to play?" One wants to build houses, the other wants to drive cars. So you suggest: "Let's build a parking garage with blocks!"

🎨 前端 / Frontend — Day 21: TypeScript Advanced — Utility Types & Type Guards

📊 Day 24 | Phase: Growth | Category: TypeScript

真实场景 / Real-World Scenario

你在做一个 dashboard,需要从后端获取用户数据并更新。后端返回的 User 对象有很多字段,但更新接口只需要传几个字段(比如只需要 name 和 email),而且这些字段都是可选的。你该怎么定义这个更新函数的参数类型?难道要重新写一个 UpdateUserPayload 接口吗?

You're building a dashboard and need to fetch and update user data. The backend User object has many fields, but the update API only needs a few (like name and email), and they're all optional. How do you type the payload without writing a whole new interface?


核心概念:Utility Types (工具类型)

TypeScript 内置了强大的工具类型,可以基于现有类型派生出新类型,保持代码 DRY (Don't Repeat Yourself)。

TypeScript has built-in utility types to derive new types from existing ones, keeping your code DRY.

1. Partial<T> & Pick<T, K>

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: Date;
}

// ❌ Bad: 重复定义 (Duplication)
interface UpdateUserPayload {
  name?: string;
  email?: string;
}

// ✅ Good: 组合使用 Utility Types (Composition)
// 1. 先用 Pick 挑出需要的字段: { name: string; email: string }
// 2. 再用 Partial 把它们变成可选的: { name?: string; email?: string }
type UpdateUserPayload = Partial<Pick<User, 'name' | 'email'>>;

function updateUser(id: string, payload: UpdateUserPayload) {
  // api call...
}

2. Omit<T, K>

如果你只需要排除一两个字段,用 Omit 更方便。

If you only need to exclude a few fields, Omit is easier.

// 创建新用户时,不需要 id 和 createdAt(后端生成)
type CreateUserPayload = Omit<User, 'id' | 'createdAt'>;

核心概念:Type Guards (类型守卫)

在处理联合类型(Union Types)或 unknown/any 时,TypeScript 需要你"证明"变量是某个特定类型,才能安全地访问其属性。

When dealing with Union Types or unknown/any, TypeScript requires you to "prove" a variable is a specific type before accessing its properties.

1. typeof & in (基础守卫)

function processValue(val: string | number | { data: string }) {
  if (typeof val === 'string') {
    return val.toUpperCase(); // TS 知道这里 val 是 string
  }
  
  if (typeof val === 'number') {
    return val.toFixed(2); // TS 知道这里 val 是 number
  }

  if (typeof val === 'object' && val !== null && 'data' in val) {
    return val.data; // TS 知道这里 val 包含 data 属性
  }
}

2. 自定义 Type Guard (is 关键字)

当你需要把复杂的类型检查逻辑抽离成函数时,必须使用 is 谓词。

When extracting complex type checks into a function, you must use the is predicate.

interface Admin { role: 'admin'; privileges: string[] }
interface Customer { role: 'user'; plan: string }

type AppUser = Admin | Customer;

// ❌ Bad: 返回 boolean,TS 调用后依然不知道 user 的具体类型
function isAdminBad(user: AppUser): boolean {
  return user.role === 'admin';
}

// ✅ Good: 返回 user is Admin,告诉 TS 如果为 true,user 就是 Admin
function isAdminGood(user: AppUser): user is Admin {
  return user.role === 'admin';
}

function handleUser(user: AppUser) {
  if (isAdminGood(user)) {
    console.log(user.privileges); // ✅ TS 知道这是 Admin
  } else {
    console.log(user.plan);       // ✅ TS 知道这是 Customer
  }
}

🧠 猜猜看 / Quick Quiz

interface Config {
  theme: string;
  timeout: number;
}
type ReadonlyConfig = Readonly<Config>;

const conf: ReadonlyConfig = { theme: 'dark', timeout: 5000 };
conf.theme = 'light';

这段代码会发生什么?

A) 正常运行,conf.theme 变成 'light'

B) 编译报错,提示 theme 是只读属性

C) 运行时报错

D) 编译通过,但运行时 theme 不会改变

<details><summary>答案 / Answer</summary>

B) 编译报错,提示 theme 是只读属性

Readonly<T> 会把接口的所有属性都变成 readonly,防止意外修改。

</details>


别踩这个坑 / Common Mistakes

❌ 错误做法✅ 正确做法
复制粘贴接口定义来创建 DTO使用 Pick, Omit, Partial 派生类型
用 as 强行断言类型 (val as Admin)使用 Type Guard (isAdmin(val)) 在运行时安全检查
自定义检查函数只返回 boolean使用 user is Admin 谓词,让 TS 编译器也能理解

📚 References

  • https://www.typescriptlang.org/docs/handbook/utility-types.html
  • https://www.typescriptlang.org/docs/handbook/2/narrowing.html

🧒 ELI5

Utility Types 就像是乐高积木的模具。你有一个完整的汽车模型(User),你想做一个只有车轮和方向盘的模型(UpdatePayload),你不需要重新画图纸,只要用模具(Pick/Partial)把需要的部分切下来就行了。

Type Guards 就像是俱乐部的保安。你拿着一张"可能是 VIP 也可能是普通会员"的卡(Union Type),保安(typeof / is)检查了你的卡,确认你是 VIP 后,才允许你进入 VIP 休息室(访问特定属性)。

🤖 AI — Day 24: Chain of Thought & Reasoning

📊 Day 24 | Phase: Growth | Category: Techniques

直觉解释 / Intuitive Explanation

想象你问一个小学生:"池塘里有 5 只青蛙,飞来 3 只鸟,又跳走 2 只青蛙,现在有几只动物?"

如果他直接脱口而出"6只",大概率是猜的。但如果他掰着手指算:"5 只青蛙减去 2 只等于 3 只,加上 3 只鸟等于 6 只",他就算对了。

Chain of Thought (CoT, 思维链) 就是让大语言模型(LLM)"掰着手指算"。它强迫模型在给出最终答案前,先一步一步写出中间的推理过程。

Imagine asking a kid a math word problem. If they blurt out an answer immediately, it's a guess. If they work it out step-by-step ("5 frogs minus 2 is 3, plus 3 birds is 6"), they get it right.

Chain of Thought (CoT) forces Large Language Models (LLMs) to do exactly this: generate intermediate reasoning steps before outputting the final answer.


它是如何工作的?/ How it Works

LLM 本质上是"接字游戏"(Next Token Prediction)。如果直接生成答案,它只有一次预测的机会。如果问题很复杂,一次预测很容易出错。

CoT 通过让模型生成中间步骤,把一个复杂的大问题,拆解成了多个简单的小问题。每生成一个中间步骤(Token),模型就多了一次"思考"的时间,并且后续的生成可以基于前面已经写出的正确逻辑。

LLMs are just predicting the next token. For complex problems, a single jump to the answer often fails. CoT breaks a hard problem into smaller, sequential token predictions. Each reasoning step acts as context for the next, dramatically improving accuracy on math, logic, and coding tasks.

两种常见形式 / Two Common Forms:

  1. Zero-Shot CoT: 在 Prompt 结尾加上一句神奇的咒语:"Let's think step by step." (让我们一步一步思考)。
  2. Few-Shot CoT: 在 Prompt 里给模型提供几个带有完整推理过程的示例(Examples)。

应用场景 / Applications

  • 复杂数学计算 (Math word problems)
  • 逻辑推理 (Logic puzzles, riddles)
  • 代码生成与 Debug (Planning architecture before coding)
  • 数据提取与分析 (Explaining why a data point was chosen)

💻 Runnable Python Snippet

演示如何用 OpenAI API 实现 Zero-Shot CoT。

Demonstrating Zero-Shot CoT with the OpenAI API.

# pip install openai
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

problem = "A juggler has 3 balls. He buys 2 more, loses 1, and finds 4. How many balls does he have?"

# ❌ Without CoT (Might guess wrong)
# prompt = f"Answer this: {problem}"

# ✅ With Zero-Shot CoT
prompt = f"Answer this: {problem}\n\nLet's think step by step."

response = client.chat.completions.create(
    model="gpt-3.5-turbo", # Or gpt-4
    messages=[{"role": "user", "content": prompt}],
    temperature=0
)

print(response.choices[0].message.content)
# Expected Output:
# 1. The juggler starts with 3 balls.
# 2. He buys 2 more, so 3 + 2 = 5 balls.
# 3. He loses 1, so 5 - 1 = 4 balls.
# 4. He finds 4, so 4 + 4 = 8 balls.
# The juggler has 8 balls.

📚 References


🧒 ELI5

考试的时候,老师为什么要求你写出"解题步骤"而不仅仅是最终答案?因为写步骤能帮你理清思路,不容易算错。CoT 就是让 AI 也在试卷上写出解题步骤。

Why do math teachers tell you to "show your work"? Because writing down the steps prevents silly mistakes. CoT is just telling the AI to show its work before giving the final answer.