已完成领域建模、边界上下文与 API 契约

约 30 分钟 · 阶段:分布式基石 · 能力:划分限界上下文并设计可演进的 REST/OpenAPI 契约

为什么要学本章

架构风格:单体、SOA 与微服务 选了「单体还是微服务」;本章回答边界画在哪、对外说什么语言。没有清晰的 限界上下文 与 OpenAPI 契约,拆服务只会得到多个互相渗透的「小泥球」——集成靠口头约定,改接口即全站停摆。

学完你能
  • 用 DDD 战略设计说明 ShopFlow 的 Order / Inventory / Payment 三大上下文与子域类型
  • 对比「按业务能力拆分」与「按技术层拆分」,并识别模型泄漏信号
  • 设计 REST 资源 URI、版本策略与统一错误码,并理解契约优先与 消费者驱动契约 的价值
本章先记住 3 件事
  • 上下文即边界:每个限界上下文有独立统一语言与模型;跨上下文只通过显式 API/事件,禁止直接读对方表。
  • 契约先于实现:OpenAPI 是团队间「法律文本」;改契约须版本化与消费者协商,而非偷偷改 JSON 字段。
  • ShopFlow 三上下文:Order 管订单生命周期;Inventory 管预留/扣减与防超卖;Payment 管支付与退款——各管各的数据。

DDD 战略设计:限界上下文与子域

领域驱动设计(DDD)战略设计关注「大问题如何切」:识别子域(Subdomain)类型,并为每个核心能力划定限界上下文(Bounded Context)。

子域三分法

类型含义ShopFlow 示例
核心域 差异化竞争力,值得自研投入 订单编排、库存防超卖策略
支撑域 必需但非差异化,可适度定制 支付对接、物流通知
通用域 行业通用能力,优先买/用 SaaS 短信验证码、对象存储

限界上下文的关键属性

  • 统一语言(Ubiquitous Language):上下文内术语一致;Order 里的 OrderLine 与 Inventory 里的 StockReservation 不必强行合并。
  • 模型边界:实体、聚合根、仓储仅在本上下文内完整;对外只暴露 DTO / 应用服务 / 事件。
  • 与部署单元的关系:理想上一上下文对应一个可独立演进的服务(或模块化单体中的一个模块),与 康威定律 对齐。
ShopFlow · 三大限界上下文(战略视图)
flowchart LR
  subgraph orderCtx [Order 上下文]
    O[Order 聚合]
    OL[OrderLine]
  end
  subgraph invCtx [Inventory 上下文]
    S[Sku / Stock]
    R[Reservation]
  end
  subgraph payCtx [Payment 上下文]
    P[Payment]
    Ref[Refund]
  end
  orderCtx -->|ReserveCommand API| invCtx
  orderCtx -->|InitiatePayment API| payCtx
  payCtx -->|PaymentCompleted 事件| orderCtx

箭头表示集成关系(API 或事件),不是共享数据库。各上下文内部模型互不直接引用。

模型泄漏
  • Order 实体里嵌套 Inventory 的 Warehouse 字段
  • 跨上下文 JOIN 同一张宽表
  • 改库存字段导致订单编译失败
清晰边界
  • Order 只持有 skuId、qty 等集成所需最小字段
  • 库存详情通过 GET /skus/{id} 按需查询
  • 上下文间契约版本化,内部重构不影响对方

按业务能力拆分 vs 按技术层拆分

架构风格:单体、SOA 与微服务 已对比分层单体与模块化单体;本节从 DDD 视角强调:微服务边界应沿业务能力(限界上下文)切,而非沿 Controller / Service / Repository 等技术层切。

两种切法对比

维度按技术层拆分按业务能力 / 限界上下文拆分
包/服务结构 user-controller、order-controller 各一服务,共享同一 DB order-service、inventory-service,各管各的数据
变更影响 改订单逻辑仍可能动「公共 order 表」 订单模型变更局限在 Order 上下文内
团队对齐 按「前端组 / 后端组」分工,与业务脱节 订单小队拥有 Order 上下文全栈
分布式代价 背上网络调用却无独立数据边界 → 分布式单体 真正独立部署与演进,但须设计 API/事件与一致性

代码对比:集成方式

java
// 反例:按「层」拆服务,却共享 order 表与实体
// order-query-service 直接读 order 表 join inventory 表
@Query("SELECT o, i FROM Order o JOIN Inventory i ON ...")
List<OrderDetailDto> findAllWithStock();
java
// 推荐:Order 上下文通过契约调用 Inventory
public OrderView getOrder(UUID orderId) {
  Order order = orderRepo.findById(orderId);
  StockSnapshot stock = inventoryClient.getSku(order.getSkuId());
  return OrderView.from(order, stock); // 组合视图,非共享实体
}
实践提示

模块/服务内仍可分层(controller → application → domain → infra);拆分维度是业务能力,不是禁止分层。

REST 资源建模、版本化与错误码规范

限界上下文对外通常以 RESTful HTTP API 暴露能力(practice 阶段亦用 OpenFeign 消费)。好的资源建模让调用方「读 URI 即懂语义」,并预留演进空间。

资源 URI 原则

  • 名词复数、表意资源:/orders、/skus/{skuId}/reservations,避免 /createOrder 动词路径。
  • 动作用语义子资源或 PATCH:取消订单 POST /orders/{id}/cancel 或 PATCH /orders/{id} + {"status":"CANCELLED"}。
  • 跨上下文 ID 引用:Order 只存 skuId,不在 URI 里嵌 inventory 内部仓库 ID,除非该 ID 已是稳定集成标识。
  • 幂等键:写操作(如 POST /reservations)要求 Header Idempotency-Key 或 body 内 requestId,应对 部分失败 重试。

ShopFlow 核心端点(示意)

上下文方法路径说明
OrderPOST/api/v1/orders创建订单
OrderGET/api/v1/orders/{orderId}查询订单
InventoryPOST/api/v1/reservations预留库存(幂等)
InventoryGET/api/v1/skus/{skuId}查询 SKU 可售量
PaymentPOST/api/v1/payments发起支付

版本化策略

  • /api/v1/orders → /api/v2/orders:直观,网关路由简单
  • 旧版保留 deprecate 周期,文档标明下线日
  • ShopFlow 入门推荐:路径前缀 /api/v1
  • Accept: application/vnd.shopflow.order.v2+json
  • 或 Header Api-Version: 2
  • URI 不变,适合公开 API 长期兼容;网关配置稍复杂

统一错误响应

json
{
  "code": "INVENTORY_INSUFFICIENT",
  "message": "SKU-1001 可售库存不足",
  "traceId": "7f3a2b1c-...",
  "details": { "skuId": "SKU-1001", "requested": 5, "available": 2 }
}
  • code:机器可读、文档化枚举(各服务可前缀 ORDER_ / INV_,网关可映射为统一表)。
  • traceId:与日志、链路追踪关联,便于排障。
  • HTTP 状态码表达协议层语义(404 资源不存在、409 冲突、422 业务校验失败),body 的 code 表达业务层语义。

契约优先:OpenAPI 与消费者驱动契约

契约优先(Contract First):先写 OpenAPI 规范,再生成/实现 Controller 与客户端桩代码。这样 Order 与 Inventory 团队在编码前就对齐字段、错误码与幂等语义。

OpenAPI 片段(Inventory · 预留库存)

yaml
openapi: 3.1.0
info:
  title: ShopFlow Inventory API
  version: 1.0.0
paths:
  /api/v1/reservations:
    post:
      operationId: createReservation
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [orderId, skuId, quantity]
              properties:
                orderId: { type: string, format: uuid }
                skuId: { type: string }
                quantity: { type: integer, minimum: 1 }
      responses:
        '201':
          description: 预留成功
        '409':
          description: 库存不足或冲突

契约优先工作流

  1. 领域专家 + 开发在 PR 中评审 openapi/inventory-api.yaml。
  2. CI 校验 breaking change(如删除字段、改类型)。
  3. 实现方对照契约写 Spring MVC / WebFlux;消费方用 OpenFeign + 生成 DTO 或手写映射。
  4. 契约测试(Pact 等)在 CI 验证「消费者期望 ⊆ 提供者实现」。

消费者驱动契约(CDC)

在 微服务 集成中,调用方(消费者) 定义「我需要哪些字段与状态码」,写入契约文件;提供方 在 CI 中验证能否满足。这样 inventory 重构内部表结构时,只要仍满足 order-service 的契约,集成测试即可通过。

契约优先 + CDC 协作
sequenceDiagram
  participant OC as order-service 消费者
  participant Pact as 契约仓库 / CI
  participant IS as inventory-service 提供者
  OC->>Pact: 提交期望契约(reserve 201/409)
  IS->>Pact: Provider 验证任务
  Pact->>IS: 验证通过 / 失败
  Note over OC,IS: 实现前对齐;改 API 须双方评审

对比「联调当天才发现字段少了」:契约将集成失败左移到 PR 阶段。

FAQ:OpenAPI 与 Java 代码谁为准?

团队应显式选定真源:契约优先则以 YAML 为准;代码优先则须 CI 反向生成/校验 OpenAPI diff。ShopFlow 教程采用契约优先,与 SpringDoc 可并存(由 YAML 生成或双向校验)。

ShopFlow:订单、库存、支付上下文划分

将本章方法落到 ShopFlow 全课主线:三个上下文各司其职,通过 API 网关 对外统一入口,内部走同步 API + 异步事件。

上下文职责表

上下文核心聚合拥有的数据对外能力
Order Order 订单头、明细、状态机 创建/查询/取消订单;订阅 PaymentCompleted
Inventory Sku、Reservation 可售量、预留记录 预留/释放/扣减;强一致 防超卖
Payment Payment 支付单、渠道回调 发起支付、退款;发布 PaymentCompleted

下单 happy path(集成视图)

ShopFlow · 创建订单(同步 + 事件)
sequenceDiagram
  participant U as 用户
  participant GW as API Gateway
  participant O as order-service
  participant I as inventory-service
  participant P as payment-service
  U->>GW: POST /api/v1/orders
  GW->>O: 转发
  O->>I: POST /reservations (Idempotency-Key)
  I-->>O: 201 Reserved
  O->>P: POST /payments
  P-->>O: 202 Accepted
  O-->>U: 201 Order PENDING_PAYMENT
  P-->>O: PaymentCompleted 事件
  O->>O: 状态 → PAID

Order 编排流程;Inventory 与 Payment 不直接互调。失败补偿在 分布式数据:事务、幂等与 Saga(Saga)展开。

上下文映射(Context Map)简图

  • Order → Inventory:Customer-Supplier(Order 为客,Inventory 为供);Inventory 上游发布 OpenAPI,Order 消费。
  • Order → Payment:同上;Payment 完成后以事件通知 Order(避免 Payment 同步回调 Order 造成环依赖)。
  • Anti-corruption Layer:Order 内 InventoryClient 将外部 DTO 转为域内 ReservationResult,隔离外部模型变化。
先想 10 秒:能否让 Payment 服务直接 UPDATE order 表的 status 字段?

不应:违反上下文数据所有权。应通过 API 或事件让 Order 上下文自行变更状态;跨库写是 分布式单体 典型信号。事件可靠投递可结合 Outbox(CAP、BASE 与一致性模型 预览,Kafka 事件驱动与 Redis 缓存 展开)。

下一章 通信模式:同步、异步与事件驱动 将把同步/异步、编排与编舞、Outbox 与本章契约映射到 Spring Cloud 概念。

本章结论

每条对应上一节 h3,用于评审前 30 秒复述。

  • 战略 DDD:用子域分类投入优先级;限界上下文划定统一语言与模型边界,跨上下文禁止共享实体与随意 JOIN。
  • 拆分维度:按业务能力/限界上下文切服务或模块,而非按 Controller 等技术层;否则易成分布式单体。
  • REST 规范:资源 URI、路径版本、统一错误码与幂等键;HTTP 状态与业务 code 分工明确。
  • 契约优先:OpenAPI 先行 + CI 防 breaking change;CDC 让消费者定义期望,集成失败左移到 PR。
  • ShopFlow 三上下文:Order 编排、Inventory 防超卖、Payment 收单;网关统一入口,Payment 以事件回传 Order。

复习与自检

过关清单(勾选会保存在本浏览器)

0 / 5 已勾选

本章速查卡
概念一句
限界上下文独立语言与模型;对外只暴露 API/事件
按能力拆分Order/Inventory/Payment,非按 Controller 拆
契约优先OpenAPI 先评审再编码
CDC消费者定义期望,提供者 CI 验证
幂等键写操作 Header Idempotency-Key
答辩 / 面试口述题
  1. 什么是模型泄漏?举 ShopFlow 一个反例与一个推荐做法。
  2. URI 版本与 Header 版本各适合什么场景?
  3. 为何 Payment 不应直接更新 order 表?应如何集成?

下一步:勾选过关清单、完成测验与 Demo;下一章 通信模式:同步、异步与事件驱动 讲同步/异步通信与 Outbox 预览。

动手练习

产出 Inventory 最小 OpenAPI 与三上下文映射图,对照错误码与幂等头约定自检。

操作步骤

  1. 按「REST 错误码约定」表,为 POST /api/v1/reservations 补全 409 库存不足与幂等冲突的 JSON body(含 code、message)。
  2. 在 YAML 中添加 Idempotency-Key header 参数,与正文 OpenAPI 片段对齐。
  3. 绘制三上下文映射图,标 Customer-Supplier 边与 PaymentCompleted 事件方向。

判断练习

  1. 哪条 API 设计违反契约?① Inventory 暴露 PATCH /orders/{id}/status ② Order 调 POST /reservations 带幂等键 ③ Payment 回调用 callbackToken 验签

    参考答案

    答:①。订单状态属 Order 上下文,Inventory 不应直接改 orders;应通过 API/事件让 Order 自行变更。②③ 符合契约与幂等/验签约定。

  2. OpenAPI 缺少 Idempotency-Key header,却写「支持重复提交安全」。违反哪条约定?应改哪一段?

    参考答案

    违反契约优先与预留接口幂等约定。应在 OpenAPI parameters 增加 header 定义,并在 POST 说明中引用;实现与文档保持一致。

Demo:限界上下文与 API 契约实验室

目录:demos/basics-04-domain-api-lab/。验收:YAML 含 reservations 路径 + 响应码 201/409/422。

延伸学习