# 09. 状态

如何设计状态、生命周期、state machine、optimistic locking 与 eventual consistency——不靠魔法,也不靠随手加的 flag。

- **Author:** Fedor Chishchin
- **Level:** advanced
- **Reading time:** 约 25 分钟阅读
- **Published:** 2026-05-23
- **Source:** https://fedorthinks.com/zh/learn/software-architect-09-state

---

## 1. 核心思想

如果一个对象有生命周期,就把它设计成 state machine,而不是堆一堆 boolean flag。

### 状态必须显式

> 交通灯能被理解,是因为它的状态有限,且转换被规定清楚了。如果所有灯一起亮,司机就会在路口争论起来。

Order、payment、delivery、subscription、ticket、deployment——这些都是带 lifecycle 的实体。它们有状态和转换:`draft`、`pending_payment`、`paid`、`cancelled`、`refunded`。架构师要做的事,就是定义允许的转换以及每个转换的归属。

当 state machine 被 `isPaid`、`isCancelled`、`isRefunded`、`isArchived` 这类 flag 取代时,系统很快就会出现不可能的组合:同时既已支付又已取消、未付款却已发货、没有 capture 却出现了 refund。

### Concurrency 是模型的一部分

> 如果两个人同时编辑一份文档,又没有规则,赢的不是有理的那位,而是最后保存的那位。

真实系统里,多个进程可能修改同一份状态:用户、worker、webhook、admin、retry。你需要 optimistic locking、version field、transactions、unique constraints 或 compare-and-swap 操作。

架构师必须问清楚:谁有资格修改这个状态、竞态怎么解决、哪些操作幂等、什么算最终状态。

## 2. 正例与反例

好的状态可以用一张图讲清楚。坏的状态只能靠一长串例外来解释。

### 正例:payment lifecycle

> 一笔银行操作有它的阶段。你不能先退款,然后再决定钱有没有被扣过。

Payment 可以从 `created` 进入 `authorized`,再走向 `captured` 或 `voided`。Refund 只能在 `captured` 之后发生。Provider 的 webhook 不会随意写字段,而是触发一次带当前状态和 version 校验的状态转换。

这种做法把 edge case 变成了模型的一部分,而不是散落在代码各处的一堆 `if`。

### 反例:用 flag 代替生命周期

> 一份有十个勾选框的表单看起来很灵活,直到两个勾互相矛盾。

Order 有 `paid_at`、`cancelled_at`、`failed_at`、`refunded_at`、`completed_at`,但没有一个清楚的状态字段。不同模块对组合的解读不一样。客服看到的是一个版本,财务看到的是另一个版本,API 又是第三个版本。

这不是灵活,是一台没有规则的隐式 state machine。

#### 自查

- 这个实体有哪些状态?
- 哪些转换是被禁止的?
- 谁拥有这个转换:用户、系统,还是外部 provider?
- 我们怎么防止重试和竞态?

下一模块:[分布式系统](./software-architect-10-distributed-systems)。
