Aggregate(聚合)
解决什么问题:在多个强关联的对象里,挑一个做"入口",让其它对象通过它访问—— 没有聚合 = 谁都能改任何对象,并发冲突、业务规则散落各处、一致性无法保证。 聚合 = 划定"一致性边界"——边界内强一致、边界外最终一致。
它是什么
聚合(Aggregate) 是 DDD 战术层的核心模式——一组必须保持强一致性的领域对象,作为一个整体被访问和修改的单元。
每个聚合有:
- 聚合根(Aggregate Root)——外部唯一能引用的对象
- 内部实体 / 值对象——只能通过根访问
聚合 Order { ← 根:Order
id, customerId, status ← 外部只能引用 Order.id
items: List<OrderItem> ← 内部实体:通过 Order.items 修改
shippingAddress: Address ← 值对象:不可变
}Vernon Ch 10: "An aggregate is a cluster of domain objects that can be treated as a single unit... An aggregate will have one root entity (the aggregate root) and any number of subordinate entities and value objects."
V2 视角(V2.7 B3 段 3 "分解的最小单元"):聚合是 L1 战术层的"最小分解单元"——比类大,比 BC 小。一个 BC 由 N 个聚合组成,聚合之间靠 ID 引用、不持对象引用。
关键属性
| 属性 | 值/含义 |
|---|---|
| 边界 | 一个聚合 = 一个一致性边界 |
| 根 | Aggregate Root,唯一对外引用点 |
| 引用 | 聚合间用 ID 引用,不持对象引用 |
| 事务 | 一个事务 = 一个聚合的修改(不能跨聚合事务) |
| 大小 | 越小越好——越小 = 并发越高 = 性能越好 |
| 不变式 | 根负责守护整个聚合的业务规则 |
| 仓储 | 一个聚合 = 一个 Repository(根的仓储) |
| 删除 | 删除根 = 整个聚合级联删除 |
怎么用
场景 1:识别聚合(从业务事件反推)
事件风暴产物:
- OrderPlaced { orderId, items, customerId }
- OrderItemAdded { orderId, itemId, qty }
- OrderPaid { orderId, amount }
- OrderShipped { orderId, address }问一个问题:哪些对象必须"同时修改"才不破坏业务规则?
- Order + OrderItem → 必须同事务(先加 item 后下单 = 数据不一致)
- Order + Payment → 异步解耦(不要塞一个聚合里)
- Order + ShippingAddress → 同聚合(订单地址跟着订单走)
结论:Order + OrderItem + ShippingAddress = 一个聚合。
场景 2:聚合根的代码骨架(Java 示例)
public class Order { // 聚合根
private OrderId id;
private CustomerId customerId; // 跨聚合用 ID
private List<OrderItem> items;
private ShippingAddress address; // 值对象
// 工厂方法——保证创建即合法
public static Order place(CustomerId cid,
Cart cart,
ShippingAddress addr) {
if (cart.isEmpty()) throw new EmptyCartException();
return new Order(OrderId.generate(), cid, cart.toItems(), addr);
}
// 业务行为——内部一致性由根守护
public void addItem(Product p, int qty) {
if (status != OrderStatus.DRAFT)
throw new OrderAlreadyPlacedException();
items.add(new OrderItem(p.id(), qty, p.price()));
}
public Money total() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
// 不暴露 setter——只能通过业务行为修改
}场景 3:跨聚合的最终一致(最常见的坑)
// 错误:跨聚合事务
@Transactional
public void placeOrderAndReserveStock(Order order) {
orderRepo.save(order);
stockRepo.reserveAll(order.items()); // ← 跨聚合事务 = 分布式锁
}
// 正确:发领域事件,本聚合内强一致,跨聚合最终一致
public class Order {
public void place() {
this.status = OrderStatus.PLACED;
DomainEventPublisher.publish(
new OrderPlaced(this.id, this.items)
);
}
}
// 库存 BC 订阅事件,异步扣减
@EventListener
public void on(OrderPlaced e) {
stockService.reserveFor(e.orderId, e.items);
}Vernon Ch 10 反复强调:"一个事务只改一个聚合"——这不是性能限制,是设计原则。
场景 4:聚合大小的权衡
V2 B6 段 5 视角(折衷):
- 太大:并发差(一个用户下单锁住整个订单系统)、性能差、难重构
- 太小:业务规则散落、需要大量应用层协调、最终一致难维护
判定标准:能不能用一句话说清"这个聚合守护的不变式是什么"。
- "Order 聚合守护:未支付的订单必须有未过期的商品报价" → ✓ 清晰
- "Order+Payment+Shipping 守护:付款后必须立即发货" → ✗ 三个不同生命周期
常见误区
- "聚合越大越完整"——错。大聚合 = 锁竞争 = 性能差。能用 ID 引用就别包进来
- "跨聚合用事务保证强一致"——错。Vernon Ch 10:跨聚合 = 最终一致 + 领域事件。强一致只能在一个聚合内
- "聚合根就是 Entity"——错。聚合根是 Entity,但 Entity 不一定是根——只有"外部引用入口"才叫根
- "暴露 setter 让外部改内部对象"——错。暴露 setter = 绕过不变式检查 = 数据脏。只能通过业务行为修改
- "一个 BC 一个聚合"——错。一个 BC 通常 5-15 个聚合。一个聚合 = 一个一致性单元
关键洞察
聚合 = "事务边界" + "不变式边界" + "并发边界"——三合一。
划清聚合 = 划清了"强一致到哪儿为止"。在这之内强一致(同步、事务), 在这之外最终一致(异步、事件)。这是分布式系统设计的最关键决策—— 比微服务拆分、消息队列选型都更基础。
相关链接
- Entity & VO——聚合内部的成员类型
- Domain Service & Event——跨聚合用 Domain Service / Event
- Repository & Factory——一个聚合一个仓储
- Bounded Context——聚合的更大语境
- [V3 涌现作为] · 分解——聚合是最小分解单元
- [V3 涌现作为] · 抽象——聚合是抽象边界
- [V2 B6 折衷] · 折衷——聚合大小的折衷