Appearance
应用架构:从能力责任推导系统边界与协作契约
统一订单中心项目上线前,团队发现它已经承接了商品查询、会员等级、优惠计算、库存锁定、支付回调、积分发放和客服查询。它看起来像全渠道核心,实际上成为了一个谁都能调用、谁都能要求修改的“业务抽屉”。
应用架构要阻止的不是系统数量增加,而是责任没有边界。它需要回答:哪个应用承载哪项业务能力,哪个系统拥有哪类事实,系统之间通过什么契约协作,发生故障时谁负责恢复,以及旧应用如何逐步退出。
应用不是系统清单,而是责任组合
应用组合至少要同时描述五个维度:
| 维度 | 需要回答的问题 |
|---|---|
| 能力 | 这个应用承载哪些稳定业务能力 |
| 数据 | 它可以创建、修改、读取哪些事实 |
| 契约 | 它向谁提供什么接口或事件,版本如何演进 |
| 运行 | 它需要怎样的可用性、性能、隔离和发布节奏 |
| 责任 | 哪个团队对功能、数据、值班和生命周期负责 |
只列出“商城、订单、库存、会员”四个系统,无法判断它们是否重复写库存、是否拥有不该拥有的规则,也无法判断一个新服务应该放在哪个边界内。
从能力和事实推导边界
应用边界通常由多个信号共同决定,而不是由类图或数据库表单独决定:
- 业务规则内聚:必须一起判断的规则尽量在同一责任边界内。
- 数据修改责任:同一业务事实的最终修改权只有一个主人。
- 一致性范围:必须原子成立的状态变化不要轻易跨边界。
- 变化节奏:不同发布节奏和生命周期的能力可以隔离。
- 团队责任:边界需要与真实的开发、运维和决策责任匹配。
- 故障影响:非核心能力不应把核心交易拖入同一故障域。
这些信号可能互相冲突。订单创建和库存预占在业务上有关联,但不代表必须放在同一个服务;库存预占的事实责任和高并发约束可能要求独立边界,两个边界再通过明确的同步接口和事件协作。
全渠道应用责任地图
| 应用边界 | 负责的能力和事实 | 明确不负责 | 主要协作 |
|---|---|---|---|
| 渠道应用 | 展示、购物车、渠道交互和编排 | 企业商品、会员、库存和订单事实 | API、BFF、查询视图 |
| 会员中心 | 客户身份、等级、权益账户 | 订单状态、库存扣减 | 客户查询、权益校验、会员事件 |
| 商品中心 | 企业商品、类目、渠道映射 | 渠道页面、实时库存 | 商品 API、商品变更事件 |
| 订单管理 | 订单身份、交易状态、价格快照 | 直接修改实物库存、履约节点库存 | 库存、支付、履约 |
| 库存平台 | 可售视图、预占、释放、确认和调整记录 | 支付结果、订单业务有效性 | 同步预占、库存事件 |
| 履约系统 | 选节点、拆包裹、发货、签收、异常 | 改变订单交易规则 | 订单事件、库存查询、物流接口 |
| 分析平台 | 历史事实、指标和模型 | 交易裁决、回写核心状态 | CDC、业务事件、批处理 |
“不负责”同样重要。它限制了应用边界,防止为了一个临时需求把会员等级、库存判断或订单状态重新复制到另一个系统。
模块化单体还是微服务
TOGAF 定义责任和演进目标,不会直接规定部署单元。应用架构应根据边界稳定性和运行收益选择实现方式。
| 条件 | 模块化单体更合适 | 独立服务更合适 |
|---|---|---|
| 业务边界 | 规则仍在快速探索 | 边界和数据责任稳定 |
| 一致性 | 大量操作必须同一事务 | 服务内事务、服务间可接受最终一致 |
| 团队 | 一个团队统一交付 | 多团队需要独立发布和负责 |
| 扩缩容 | 流量模式相近 | 某能力需要独立扩展或隔离 |
| 故障 | 共享运行风险可接受 | 需要独立故障域和降级 |
| 运维 | 平台能力有限 | 已具备发布、观测和治理基础 |
先用模块化单体验证业务模型,不是保守;直接拆微服务也不等于架构成熟。判断标准是边界是否已经提供足够的独立价值,能够抵消网络、部署、测试、观测和一致性成本。
下单链路中的同步与事件
应用协作要按业务语义选择同步调用或事件,而不是把所有调用改成异步。
text
客户端
-> 订单管理:提交订单
-> 库存平台:请求预占,等待成功/失败结果
-> 支付平台:创建支付意图
<- 订单管理:返回待支付订单
支付平台
-> 订单管理:支付结果回调(可能重复、延迟、乱序)
-> 订单管理:校验状态并记录订单已支付
-> 事件总线:发布 OrderPaid
-> 库存平台:预占转确认
-> 履约系统:创建履约计划
-> 会员中心:发放交易权益同步接口适合需要立即决定是否继续的操作,例如库存预占;事件适合传播已经发生的事实。两者都必须设计幂等、超时、重试和补偿,但失败语义不同:同步调用要给当前请求明确结果,事件消费失败则要可重放并避免重复副作用。
契约首先表达责任
库存预占接口不应只定义 URL 和字段,还要说明谁可以调用、谁做最终判断、请求重复时返回什么、超时后订单处于什么状态。
yaml
operation: reserve_inventory
owner: inventory-platform
caller: order-management
idempotency_key: order_id + reservation_attempt
success: reservation_id, expires_at
business_failures:
- OUT_OF_STOCK
- LOCATION_NOT_SERVABLE
- RESERVATION_LIMIT_EXCEEDED
timeout: order remains pending-reservation and can be retried
side_effect: publish InventoryReserved only after durable state is committed事件契约也要声明:事件代表已发生事实、唯一 ID、发生时间、业务主键、Schema 版本、重复和乱序处理、敏感字段、保留时间,以及消费者无法处理时的重放路径。
状态机比状态字段更重要
订单状态不能由多个系统直接写字符串。至少需要定义合法转移和前置条件:
text
待预占 -> 待支付 -> 已支付 -> 履约中 -> 已完成
| | | |
v v v v
预占失败 已取消 退款中 履约异常支付成功回调到达时,如果订单已经因超时取消,系统不能简单把状态改成已支付。它需要依据状态机和业务补偿规则决定是否重新激活、挂起人工处理或发起退款。状态机是应用架构与业务规则的交界面,不能只留在某个 Controller 的条件分支里。
读模型不是事实副本
客服需要跨渠道查询订单,首页需要快速展示库存,分析平台需要沉淀历史。它们可以建立读模型,但读模型必须声明:
- 来源事实和最后更新时间。
- 允许的最大延迟。
- 是否允许展示给客户。
- 是否可以被用户操作反向修改。
- 重建方式和数据丢失后的恢复方式。
读模型无法参与交易裁决时,界面必须表达“数据暂时不可用”或采用保守策略,而不能把过期数据伪装成实时事实。
遗留系统如何退出应用组合
应用架构还要描述生命周期:引入、共存、迁移、冻结、归档和下线。
| 生命周期阶段 | 需要保留的证据 |
|---|---|
| 引入 | 为什么新应用能补足能力差距,谁承担责任 |
| 共存 | 哪个范围由谁写入,数据如何对账 |
| 迁移 | 流量、数据、配置和权限如何切换 |
| 冻结 | 旧系统是否停止新功能和新数据入口 |
| 归档 | 历史查询、审计和恢复如何满足 |
| 下线 | 依赖、任务、凭证和访问权限是否清退 |
旧系统仍然能够查询,不代表它还应该拥有写入权。生命周期状态必须与实际流量和责任一致。
应用架构的最小交付物
- 基线应用组合与能力映射。
- 目标应用责任和“不负责”边界。
- 数据权威关系与读模型来源。
- 同步 API、事件和批处理契约。
- 关键业务状态机和异常转移。
- 质量属性场景与故障隔离策略。
- 应用生命周期、迁移范围和退出条件。
- 关键取舍的 ADR 与复审触发条件。
常见失效方式
- 按数据库表或技术分层拆服务,业务责任被拆散。
- 订单服务变成所有跨域规则的集中入口。
- 事件只表达“请某服务做某事”,却没有事实语义和幂等规则。
- 读模型没有时效标记,过期数据继续参与交易判断。
- 共享数据库被多个服务直接写,接口只是表的另一种外壳。
- 只设计新应用,不设计旧应用的冻结、归档和退出。
能力验证
- 能从业务能力、数据责任、一致性和故障域推导应用边界。
- 能说明某项能力为什么适合模块化单体或独立服务。
- 能为同步接口和业务事件写出不同的失败语义。
- 能用状态机约束重复、乱序和逆向状态变化。
- 能区分权威事实、读模型和分析副本。
- 能把应用迁移拆成共存、切换、冻结和退出证据。
总结
应用架构不是把系统画成更多方框,而是把能力和事实责任落实到可以被团队拥有、被契约约束、被故障隔离、被独立演进的应用边界上。好的应用组合会让后端团队更清楚“谁可以改什么、谁必须通知谁、失败后谁负责恢复”,而不是只增加服务数量。
