聚合的设计原则:一致性边界、事务边界、生命周期管理

聚合(Aggregate)是领域驱动设计中用于封装业务不变量的核心构造。设计一个合理的聚合,需要同时平衡三个维度:一致性边界确保业务规则不被破坏,事务边界保障数据完整性,生命周期管理控制聚合的创建、变更与持久化。本章将逐一拆解这三个原则,并给出可落地的设计指引。

2.1 一致性边界:聚合的“宪法”

一致性边界定义了聚合内部所有实体和值对象必须共同遵守的业务规则。边界内的任何状态变更,都不能违反聚合根所维护的不变量。

核心判断标准: 如果两个对象之间存在“必须同时满足”的业务约束,它们就应该属于同一个聚合;如果约束可以容忍短暂的不一致,则应该分属不同聚合。

以下表格对比了强一致性与最终一致性的适用场景:

维度 强一致性(同一聚合) 最终一致性(跨聚合)
业务约束 必须实时满足,违反即拒绝 可短暂容忍,通过补偿或事件修复
事务范围 单数据库事务 分布式事务或 Saga
性能影响 锁范围大,并发受限 锁范围小,吞吐量高
典型场景 订单总金额与明细金额合计 订单状态与库存扣减

设计一致性边界时,应遵循以下步骤:

  1. 识别不变量:与领域专家确认哪些规则是“必须永远为真”的。
  2. 划定边界:将需要同时满足不变量的对象归入同一聚合。
  3. 验证边界:通过事件风暴或场景推演,检查边界是否过大或过小。
常见陷阱: 将“查询方便”作为聚合划分依据。聚合边界应由业务不变量驱动,而非查询性能。查询优化应通过读模型(CQRS)或数据库索引解决。
聚合一致性边界示意图 聚合一致性边界示意 正确的聚合边界 订单(聚合根) 订单项(实体) 配送地址(值对象) 不变量: • 订单总金额 = 所有订单项金额之和 • 配送地址变更需重新计算运费 错误的聚合边界 用户(聚合根) 订单(实体) 支付记录(实体) 问题: • 用户与订单无强一致性约束 • 导致聚合过大,并发冲突严重

2.2 事务边界:一次提交,一个聚合

事务边界与一致性边界紧密相关。DDD 的核心原则是:一个事务只修改一个聚合实例。这意味着,聚合根是事务的原子操作单元。

实践要点: 如果发现一个业务操作需要同时修改两个聚合,首先应重新审视聚合边界是否划分正确。如果确实需要跨聚合操作,应使用领域事件 + Saga 模式实现最终一致性,而非扩大事务范围。

事务边界的设计决策直接影响系统的并发能力和数据一致性:

  • 单聚合事务:使用数据库本地事务,保证 ACID,适合强一致性场景。
  • 跨聚合事务:使用 Saga(编排或协同),保证最终一致性,适合高吞吐场景。
  • 事件驱动:聚合发布领域事件,其他聚合异步响应,实现松耦合。

以下代码展示了一个聚合根如何通过仓储接口保证事务边界:

// 聚合根:订单
public class Order : AggregateRoot<OrderId>
{
    private List<OrderItem> _items = new();
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    public Money TotalAmount { get; private set; }

    public void AddItem(ProductId productId, int quantity, Money unitPrice)
    {
        // 业务不变量:数量必须大于0
        if (quantity <= 0)
            throw new DomainException("数量必须大于0");

        var item = new OrderItem(productId, quantity, unitPrice);
        _items.Add(item);
        RecalculateTotal();
    }

    private void RecalculateTotal()
    {
        TotalAmount = new Money(_items.Sum(i => i.SubTotal.Amount));
    }
}

// 仓储接口:保证一次只持久化一个聚合
public interface IOrderRepository : IRepository<Order, OrderId>
{
    Task<Order> GetByIdAsync(OrderId id);
    Task AddAsync(Order order);
    Task UpdateAsync(Order order);
}

2.3 生命周期管理:从创建到归档

聚合的生命周期包括创建、状态变更、持久化、归档或删除。良好的生命周期管理能防止聚合处于非法状态,并简化 API 契约的设计。

生命周期管理通常包含以下阶段:

阶段 关键活动 API 契约体现
创建 通过工厂或构造函数创建聚合,确保初始状态满足不变量 POST 请求,返回聚合 ID
变更 通过聚合根方法修改状态,每次变更都校验不变量 PUT/PATCH 请求,幂等性设计
持久化 通过仓储将聚合状态写入数据库 响应状态码 200/201
归档/删除 软删除或状态标记,避免物理删除导致数据丢失 DELETE 请求,返回 204
生命周期陷阱: 不要在聚合外部直接修改聚合内部状态。所有状态变更必须通过聚合根暴露的方法进行,否则一致性边界将被破坏。
聚合生命周期状态图 聚合生命周期状态图 创建 活跃 已归档 已删除 持久化成功 业务完成 软删除 清理策略 生命周期规则 • 创建后必须进入活跃状态 • 归档后不可再修改 • 删除为逻辑删除

2.4 三条原则的协同关系

一致性边界、事务边界和生命周期管理并非孤立的原则,而是相互支撑的三角关系:

  • 一致性边界决定了哪些对象属于同一个聚合,是事务边界和生命周期管理的基础。
  • 事务边界保障了一致性边界内的操作是原子的,防止部分更新导致数据不一致。
  • 生命周期管理确保聚合在整个生命周期内都遵循一致性边界定义的规则,从创建到归档都不例外。
设计检查清单:
  1. 聚合内的每个实体/值对象是否都与聚合根存在强一致性约束?
  2. 一个事务是否只修改一个聚合实例?
  3. 聚合的状态变更是否都通过聚合根方法进行?
  4. 聚合的删除是否为逻辑删除,并保留审计轨迹?