阅读时间:约 45 分钟 前置知识:建议先阅读 249|DDD 战略设计 了解领域、子域、限界上下文等概念。 本文目标:掌握 DDD 战术设计的六大核心模式,能在实际项目中落地充血领域模型。
一、从战略进入战术

上一篇文章我们讲完了 DDD 战略设计——你划分了领域和子域,画了限界上下文和上下文映射图。现在你坐在工位上,打开 IDE,面对着空白的 Java 类文件。问题来了:代码怎么写?哪些类放在一起?"订单"这个类应该有哪些字段?库存扣减逻辑放在哪里?
这就进入了 DDD 战术设计(Tactical Design)的范畴。如果战略设计回答的是"系统长什么样",战术设计回答的就是"代码怎么写"。
Evans 在《领域驱动设计》中给出了六种核心战术模式:
┌─────────────────────────────────────────────────────────┐
│ DDD 战术设计六大模式 │
│ │
│ 实体(Entity) 聚合(Aggregate) 仓储(Repository) │
│ 值对象(VO) 领域服务(Domain Service) │
│ 领域事件(Domain Event) │
└─────────────────────────────────────────────────────────┘
它们之间的关系不是平行的,而是有层次和协作关系的。我们先从最基础的两种模式讲起:实体和值对象。它们是领域模型的"原子"构件,理解了它们,聚合、仓储、领域事件才能水到渠成。
二、实体(Entity):有身份的东西
2.1 什么是实体
实体的核心特征:拥有唯一标识(Identity),并通过这个标识来追踪其生命周期。
想象一个电商系统。用户下单后,订单的状态会变化:已创建→已支付→已发货→已签收。无论订单的金额被修改了多少次、收货地址变了几次,只要 OrderId 还是 "ORD-2026-00123",它就是同一个订单。这种**同一性(Sameness)**就是实体的本质。
// 实体:Order
public class Order {
private OrderId id; // 唯一标识——这是实体的"身份证"
private OrderStatus status; // 可变的
private Money totalAmount; // 可变的
private Address shippingAddress; // 可变的
// 实体通过标识判断相等
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false;
Order order = (Order) o;
return id.equals(order.id); // 只比 ID,不管其他字段
}
}
判断实体 vs 值对象的经典问题:问自己——如果这个对象的"金额"从 100 变成 200,"地址"从北京变成上海,它还是它吗?
如果答案是"是"→ 实体;如果答案是"不是,它完全变了一个东西"→ 值对象。
2.2 实体的四个特征
| 特征 | 说明 | 反模式 |
|---|---|---|
| 唯一标识 | 每个实体有唯一 ID,贯穿整个生命周期 | 用数据库自增 ID 作为领域标识(应使用业务主键或 UUID) |
| 可变性 | 实体的属性可以改变,但标识不变 | 把所有属性做成可变的(应区分可变属性和不变属性) |
| 生命周期 | 实体经历创建、状态变迁、最终归档或删除 | 在实体中写 CRUD 方法(实体的方法应体现业务行为) |
| 连续性 | 同一实体在整个生命周期中可以"被认出" | 状态变迁丢失中间状态(使用领域事件记录) |
2.3 标识生成策略
实体标识的生成有四种常见策略:
- 用户提供:如用户名、邮箱。简单但依赖外部。
- 程序生成:UUID、Snowflake。最常用的策略。
- 持久化机制生成:数据库自增 ID。不推荐——应用层在持久化之前拿不到 ID,无法完成业务操作。
- 其他限界上下文提供:通过防腐层适配外部 ID。
实践中最推荐的是策略 2:在创建实体时就分配 ID,不依赖持久化层。
// 推荐:构造时即分配标识
public class Order {
private OrderId id;
private Order() {} // ORM 用
public static Order create(CustomerId customerId, List<OrderItem> items) {
Order order = new Order();
order.id = OrderId.generate(); // UUID 或雪花 ID
order.customerId = customerId;
order.items = new ArrayList<>(items);
order.status = OrderStatus.CREATED;
order.createdAt = Instant.now();
return order;
}
}
三、值对象(Value Object):描述性、无身份
3.1 什么是值对象
值对象的核心特征:没有唯一标识,通过其属性值来定义相等性,并且是不可变的(Immutable)。
继续用电商的例子。订单金额 new Money(100, "CNY") 就是一个值对象。它没有独立的"身份"——两个 new Money(100, "CNY") 是完全等价的。钱本身不需要一个 UUID。
// 值对象:Money
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = Objects.requireNonNull(currency);
}
// 值对象通过所有属性判断相等
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0
&& currency.equals(money.currency);
}
// 值对象不可变:add 返回新实例而非修改自身
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
}
}
3.2 值对象 = 不可变 + 整体替换 + 值相等

这个公式值得背下来:
| 原则 | 含义 | 为什么 |
|---|---|---|
| 不可变 | 所有字段 final,修改返回新对象 | 避免副作用,安全共享 |
| 整体替换 | 要改就整个替换 | 与不可变性配套 |
| 值相等 | equals 比较所有属性 | 因为没有标识 |
3.3 为什么值对象让代码更好
例1:告别基本类型偏执(Primitive Obsession)
// ❌ 错误:用 String 表示邮箱
public class Customer {
private String email;
// 问题:这个 String 可能是"abc"、空字符串、非法格式……到处要写校验
}
// ✅ 正确:封装为值对象
public final class Email {
private final String value;
public Email(String value) {
if (value == null || !value.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$")) {
throw new IllegalArgumentException("Invalid email: " + value);
}
this.value = value;
}
public String getDomain() {
return value.substring(value.indexOf('@') + 1);
}
// 校验逻辑在这里一处,所有使用者自动受益
}
public class Customer {
private Email email; // 类型即文档,类型即约束
}
例2:PhoneNumber、Address、Quantity、Weight、Temperature 都应该是值对象。 任何你发现自己在多个地方重复写"if (amount < 0) throw..."的地方,都在暗示你该提取值对象了。
四、聚合(Aggregate):一致性边界
4.1 两根金科玉律
如果说实体和值对象是砖块,聚合就是把砖块砌成墙的那条灰缝——它定义了边界。
聚合由**聚合根(Aggregate Root)**统领,内部封装一组实体和值对象,保证一组业务不变量(Invariant)随时成立。
聚合设计有两条核心规则,被称为"聚合设计的两根金科玉律":
规则1(外部引用):外部对象只能引用聚合根,不能直接引用聚合内部的实体。 规则2(内部一致性):聚合根负责保证内部所有不变量——对聚合的任何修改,必须通过聚合根这一扇门。
4.2 第一个规则:通过 ID 引用,而非直接引用
// ❌ 错误:Order 直接持有 Product 实体的引用
public class Order {
private Product product; // 坏了!跨聚合直接引用
}
// ✅ 正确:Order 只持有 ProductId(值对象)
public class Order {
private ProductId productId; // 通过 ID 引用
}
为什么?因为 Product 和 Order 属于不同的聚合。如果 Order 直接持有一个 Product 引用,你可能会在 OrderRepository 中通过 order.getProduct().getName() 去懒加载 Product,导致:
- 事务边界模糊(到底该不该一起持久化 Product?)
- 多个聚合被锁在同一事务中,影响并发
- 聚合内部修改时不知道哪些外部引用了自己
4.3 第二个规则:聚合根是唯一入口
// ✅ 聚合根控制所有内部修改
public class Order { // <- 聚合根
private OrderId id;
private List<OrderItem> items; // <- 内部实体
private Address shippingAddress;
private OrderStatus status;
// 外部不能 order.getItems().add(item) ——破坏封装
// 只能通过聚合根的方法:
public void addItem(ProductId productId, Money unitPrice, int quantity) {
// 不变量1:已支付的订单不能修改
if (status != OrderStatus.CREATED) {
throw new OrderAlreadyPaidException(id);
}
// 不变量2:同商品不能重复添加(或合并数量——看业务)
if (items.stream().anyMatch(i -> i.getProductId().equals(productId))) {
// 合并或拒绝,取决于业务规则
}
items.add(new OrderItem(productId, unitPrice, quantity));
}
public void pay() {
// 不变量:只有已创建状态的订单能支付
if (status != OrderStatus.CREATED) {
throw new IllegalOrderStateTransitionException(status, OrderStatus.PAID);
}
this.status = OrderStatus.PAID;
}
}
4.4 聚合的大小:宁小勿大

这是 DDD 中最容易犯错的点。很多人把整个订单子域做成一个聚合,包含订单、订单项、收货地址、支付信息、物流轨迹等等。结果是:
- 一个事务锁住太多数据
- 并发冲突严重(两个请求同时修改订单的不同行项目也会冲突)
- 测试变得复杂
经验法则:
| 聚合大小 | 特征 |
|---|---|
| 太小 | 每个实体一个聚合——失去一致性保护 |
| 刚好 | 满足"一个事务只需要修改一个聚合实例"的约束 |
| 太大 | 多个并发修改点挤在一个聚合中,事务冲突严重 |
判断聚合是否过大的信号:你在写业务代码时,发现一个用例需要加载一个聚合,却只修改了它几千行代码中的一个小角落 → 聚合该拆了。
4.5 不变量 vs 最终一致性
有些业务规则必须强一致(聚合边界内),有些可以接受最终一致(跨聚合)。
聚合内部:强一致事务
├── 订单总金额必须等于各订单项金额之和 —— 必须强一致
├── 已支付的订单不能修改收货地址 —— 必须强一致
└── 订单创建时必须有至少一个订单项 —— 必须强一致
跨聚合:最终一致性
├── 订单支付后通知物流模块发货 —— 通过领域事件,可以异步
├── 订单取消后释放库存 —— 通过领域事件,允许短暂不一致
└── 订单金额更新后通知对账系统 —— 通过领域事件
分区划界的关键问题:如果这条规则被短暂违反,会不会造成业务损失? 如果会 → 聚合内部强一致;如果不会 → 跨聚合最终一致。
五、领域服务(Domain Service):无家可归的业务逻辑
5.1 什么时候需要领域服务
有些业务逻辑不属于任何一个实体或值对象——把它硬塞进某个实体,既别扭又破坏了单一职责。这些"无家可归"但又是纯粹领域逻辑的操作,就适合放在领域服务中。
判断标准:
| 特征 | 放在实体/值对象 | 放在领域服务 |
|---|---|---|
| 操作只涉及一个聚合内部 | ✓ | ✗ |
| 操作需要协调多个聚合 | ✗ | ✓ |
| 逻辑自然依附于某个实体 | ✓ | ✗ |
| 逻辑不依附于任何实体,但仍是领域概念 | ✗ | ✓ |
| 有状态 | ✗ | ✗(无状态) |
5.2 经典场景:转账
// 领域服务:TransferService
// 转账涉及两个 Account 聚合,不属于任何一个 Account 实体
public class TransferService {
public void transfer(AccountId from, AccountId to, Money amount) {
Account source = accountRepository.findById(from);
Account target = accountRepository.findById(to);
source.debit(amount); // 扣款
target.credit(amount); // 到账
accountRepository.save(source);
accountRepository.save(target);
}
}
5.3 领域服务 vs 应用服务

这是一个高频面试题。两者的区别是:
// 应用服务:编排,不包含业务逻辑
@Service
public class OrderApplicationService {
public void cancelOrder(OrderId orderId) {
// 1. 事务管理(基础设施)
// 2. 加载聚合
Order order = orderRepository.findById(orderId);
// 3. 调用领域逻辑(只做这一步,但不能自己写 if/else 判断)
order.cancel();
// 4. 持久化
orderRepository.save(order);
// 5. 发布领域事件
eventPublisher.publish(order.getEvents());
}
}
// 领域服务:包含纯业务逻辑,不处理事务/持久化
public class PricingService { // 领域服务
public Money calculateTotalPrice(List<OrderItem> items,
DiscountPolicy policy,
CouponId couponId) {
Money subtotal = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
Money afterDiscount = policy.apply(subtotal);
Money afterCoupon = couponService.applyCoupon(afterDiscount, couponId);
return afterCoupon;
}
}
关键区别:应用服务是"胶水代码"——它把领域对象、仓储、事务管理粘在一起,但不包含任何业务判断。如果应用服务里有 if 语句判断业务规则,那它就在越位。
六、仓储(Repository):为聚合提供"集合"语义
6.1 仓储不是一个 DAO
DAO 是面向数据库表的数据访问对象。仓储是面向聚合的持久化抽象,它模拟了一个内存中的集合,隐藏了持久化机制的细节。
DAO 思维: "给我一条 order 表的记录"
仓储思维: "给我 ID 为 X 的 Order 聚合"
6.2 仓储的两个核心职责
public interface OrderRepository {
// 1. 通过聚合根 ID 获取完整聚合
Optional<Order> findById(OrderId id);
// 2. 保存整个聚合(而非逐个实体保存)
void save(Order order);
// 3. 面向集合的查询(不是 CRUD)
List<Order> findByCustomerId(CustomerId customerId, Pageable page);
// ❌ 不要暴露:updateStatus(), deleteById(), updateAmount()
// 这些是 DAO 思维——对聚合的修改应该通过 load→modify→save 完成
}
仓储只操作聚合根。OrderRepository 不应该有 saveOrderItem() 方法——OrderItem 是 Order 聚合的内部实体,它的持久化由 Order 聚合根全权负责。
6.3 仓储接口放在领域层,实现放在基础设施层

领域层:domain/
└── repository/
└── OrderRepository.java ← 接口(纯领域概念)
基础设施层:infrastructure/
└── persistence/
└── MyBatisOrderRepository.java ← 实现(依赖 MyBatis/JPA)
这种"依赖倒置"让领域层不依赖任何框架——你可以从 MyBatis 换到 JPA,从 MySQL 换到 MongoDB,领域代码一行不动。
七、领域事件(Domain Event):聚合的"事后通知"
7.1 领域事件是什么
领域事件是领域中已经发生的重要事情——注意"已经发生"这四个字。事件的名字总是过去式:OrderPaid、ItemShipped、AccountDebited。
// 领域事件
public class OrderPaidEvent implements DomainEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final Money paidAmount;
private final Instant paidAt;
// 事件的构造函数记录"发生了什么"——是事实,不可修改
public OrderPaidEvent(OrderId orderId, CustomerId customerId,
Money paidAmount, Instant paidAt) {
this.orderId = orderId;
this.customerId = customerId;
this.paidAmount = paidAmount;
this.paidAt = paidAt;
}
}
7.2 在聚合中生成并发布事件
public class Order {
private List<DomainEvent> events = new ArrayList<>();
public void pay() {
// 1. 先执行业务逻辑
if (status != OrderStatus.CREATED) {
throw new IllegalOrderStateTransitionException(status, OrderStatus.PAID);
}
this.status = OrderStatus.PAID;
this.paidAt = Instant.now();
// 2. 再生成领域事件
events.add(new OrderPaidEvent(this.id, this.customerId,
this.totalAmount, this.paidAt));
}
// 应用服务在 save 后调用此方法收集事件
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events);
}
}
7.3 领域事件的三个用途

- 跨聚合的最终一致性:Order 支付后,Inventory 扣库存、Logistics 创建运单——各干各的,事件驱动。
- 审计与追溯:
OrderPaid、OrderShipped、OrderDelivered形成一条完整的事件流,任何时候可以重放事件回到任意历史状态。 - 与限界上下文集成:多个限界上下文通过领域事件松耦合通信——甚至一个上下文完全不需要知道其他上下文的存在。
Event Sourcing(事件溯源)是领域事件的进阶用法:不存储聚合的当前状态,而是存储所有事件的序列,通过重放事件来重建当前状态。这是提升到架构层面的技术模式,超出了战术设计的范畴,但在面试中经常被问到。
八、六大模式协作全景

我们以一个订单取消的完整流程为例,看看六大模式如何协作:
1. OrderApplicationService.cancelOrder(orderId)
│
2. OrderRepository.findById(orderId) ← 仓储
│ 返回 Order 聚合
│
3. order.cancel() ← 聚合根(内部协调实体和值对象)
│ - 检查状态不变量
│ - 生成 OrderCancelledEvent ← 领域事件
│
4. OrderRepository.save(order) ← 仓储
│
5. eventPublisher.publish(order.getEvents())
│ - InventoryService.onOrderCancelled() ← 跨聚合通过领域服务
│ inventory.releaseReserved(orderId) ← 另一个聚合根
│ - NotificationService.send(orderId)
在这个流程中:
- 实体:Order、OrderItem 各自有 ID
- 值对象:Money、Address——不可变、值比较
- 聚合:Order 是聚合根,保证不变量
- 领域服务:InventoryService 协调跨聚合逻辑
- 仓储:OrderRepository 封装持久化
- 领域事件:OrderCancelledEvent 驱动下游
九、DDD 战术设计的常见误区
这些是实践中最常见的踩坑点:
9.1 误区一:贫血模型
// ❌ 贫血模型——DDD 的反面教材
public class Order {
private Long id;
private String status;
// getters + setters...
}
@Service
public class OrderService {
public void cancel(Order order) {
if (!"PAID".equals(order.getStatus())) { // 业务逻辑泄漏到 Service
throw new RuntimeException("...");
}
order.setStatus("CANCELLED");
}
}
// ✅ 充血模型
public class Order {
public void cancel() {
if (status != OrderStatus.PAID) {
throw new IllegalOrderStateTransitionException(status, OrderStatus.CANCELLED);
}
this.status = OrderStatus.CANCELLED;
}
}
9.2 误区二:聚合过大或过小
- 整个系统只有一个"用户聚合"→ 太大
- 每个数据库表一个聚合 → 太小
- 正确:以业务不变量为边界
9.3 误区三:用 Application Service 冒充 Domain Service
// ❌ 这个包含了业务逻辑 if/else,应该在 Domain Service 或实体中
public class OrderService { // 实际上是应用服务
public void process(Order order) {
if (order.isVIP()) {
order.setDiscount(0.8);
}
if (order.getAmount() > 10000) {
// 大额订单审计
}
}
}
9.4 误区四:仓储返回 DTO 而非聚合
// ❌ 仓储返回 PO/DTO
public interface OrderRepository {
OrderPO findById(Long id); // PO 不是领域对象
}
// ✅ 仓储返回领域聚合
public interface OrderRepository {
Order findById(OrderId id); // 返回完整的 Order 聚合
}
9.5 误区五:跨聚合事务

// ❌ 把两个聚合锁在一个事务里
@Transactional
public void payAndReduceInventory(...) {
order.pay();
inventory.reduce(...); // 不同的聚合!
}
// ✅ 先提交 Order,再通过领域事件异步处理 Inventory
@Transactional
public void pay(OrderId id) {
order.pay();
orderRepository.save(order);
eventPublisher.publish(new OrderPaidEvent(...)); // 事件驱动
}
// 另一个事务
@Transactional
public void onOrderPaid(OrderPaidEvent event) {
inventory.reduce(event.getProductId(), event.getQuantity());
}
十、在项目中落地:一个可操作的步骤清单
如果你想在现有项目中引入 DDD 战术设计,这里有一个务实的路径:
- 识别实体和值对象:浏览现有代码,标记所有贫血实体和原始类型滥用 → 提取值对象(Money、Email、PhoneNumber 等最容易上手)
- 找出聚合边界:画出你的领域对象关系图,按"一致性边界"圈出聚合
- 重构为充血模型:把 Service 层的 if/else 业务判断迁入实体方法中
- 引入仓储接口:在领域层定义仓储接口,实现移到基础设施层
- 识别领域事件:对每个聚合的状态转换,判断哪些需要通知其他模块
- 引入领域服务:把残留的跨聚合逻辑迁入领域服务
不需要一次性完成。从值对象开始,收益最大、风险最低。
🎤 面试官视角:高频追问
Q1:实体和值对象到底怎么区分?给一个可操作的标准。
考察点:是否真正理解了"同一性"的概念,还是只会背定义。
参考回答思路:问自己一个问题——如果这个对象的所有属性都变了,它还是它吗? 如果不靠任何属性、靠一个 ID 就能认出它 → 实体。如果属性变了它就不是它了 → 值对象。
举例:一个人的姓名从"张三"改成"李四",他身份证号没变,还是同一个人 → 人是实体,身份证号是标识。一个人钱包里的 100 元变成 50 元 + 50 元的零钱,你不能说"这是原来的 100 元" → 钱是值对象。
技术层面补充:值对象推荐 @Embeddable(JPA)或直接 JSON 列存储;实体必须有独立的表或文档。
Q2:聚合设计中最难的点是什么?你怎么判断一个聚合"刚刚好"?
考察点:有没有在真实项目中被聚合设计坑过。
参考回答思路:最难点是不变量识别——什么规则必须强一致、什么可以最终一致,需要和领域专家深入讨论。很多开发者的错误是凭"数据关系"而非"业务规则"来分聚合。
判断聚合是否"刚刚好"的标准:一个典型用例是否只修改一个聚合。如果一个用例需要加载聚合 A 却只动了 A 的一个浅层属性,或者一个用例需要同时锁住 A 和 B 来保证一致性 → 聚合粒度有问题。
Q3:领域服务和 Application Service 的区别,用一个实际例子说明。
考察点:分层是否清楚,会不会把业务逻辑堆在 Application Service 里。
参考回答思路:用"计算运费"举例:
PricingDomainService.calculateShipping(Address, Weight, ShippingMethod)→ 这是纯业务计算,不依赖基础设施,多个场景复用 → 领域服务OrderApplicationService.placeOrder(OrderRequest)→ 这个做了:校验输入 → 调仓储加载 → 调领域对象/领域服务 → 保存 → 发事件 → 返回结果 → 应用服务
一句话:领域服务里只有领域逻辑,没有**@Transactional**,没有**@Autowired Repository**,没有 HTTP/RPC 调用。
Q4:Repository 和 DAO 的区别是什么?为什么 DDD 要单独提 Repository?
考察点:是否理解"面向集合"和"面向表"的范式差异。
参考回答思路:
- DAO 是数据库表的一层薄封装,方法名通常是
insert/update/delete/selectByXxx - Repository 假装聚合放在一个内存集合里,方法名是
find/save,表达的是"找"和"放回"的意图
根本区别:DAO 暴露了持久化机制(表/字段),Repository 隐藏了持久化机制。当你从单体拆微服务、从 MySQL 换 MongoDB 时,Repository 的调用者无需感知——这是依赖倒置带来的灵活性。
Q5:领域事件如何保证可靠投递?如果发事件失败了怎么办?
考察点:事件驱动架构的可靠性设计。
参考回答思路:这是一个重要的工程问题。几种方案:
- 事务发件箱(Transactional Outbox):把事件写入与聚合同一个数据库的
outbox表,在一个本地事务中完成"改状态 + 写事件"。独立进程轮询 outbox 表发送到 MQ。这是最可靠的方案。 - Event Sourcing:只写事件流,当前状态是事件重放的投影——天然不丢事件。
- 反查 + 补偿:事件消费幂等 + 定时对账,发现不一致时补偿。
不要简单地说"加个重试就行"——如果重试之前进程重启了呢?需要有一个持久化的中间态。
Q6:你在一个已有的大型项目中推行 DDD 战术设计,第一步应该做什么?为什么?
考察点:落地经验 + 务实的工程思维。
参考回答思路:绝对不是说"全部重写"。推荐路径:
- 先找代码中最臭的"基本类型偏执"——String 电话、String 邮箱、BigDecimal 金额配 String 币种 → 提取为值对象。这是纯增量的改进,不破坏现有行为,风险为零。
- 从最重要的核心域开始识别聚合边界——不要动支撑子域和通用子域。
- 在新需求中应用聚合和领域事件,老代码维持现状——这就是经典的"绞杀者模式"。
关键是让团队看到收益:引入 Money 值对象后,金额计算 bug 大幅减少——有了信服力再推下一步。
📚 延伸阅读
- Eric Evans《领域驱动设计》(蓝皮书)第 5-9 章——战术设计的原典
- Vaughn Vernon《实现领域驱动设计》(红皮书)第 5-12 章——更工程化的实践指南
- Martin Fowler: Domain Event
- Chris Richardson: Pattern: Transactional Outbox
- 上一篇:249|DDD 战略设计
本文是 Java 技术专家/架构师面试 250 个高频考点系列的第 250 篇,也是本系列的最后一篇。恭喜你完成了全套 250 个知识点的学习!🎉