关于 domain-driven-design
面向使用 Spring Data 的 Java 开发者,提供领域驱动设计的建模与实现指导,涵盖聚合边界、事件发布及外部系统隔离。
主要功能
- 01聚合根与一致性边界规则
- 02不可变值对象建模
- 03领域事件发布与持久化建议
- 04复杂查询规格模式
- 05防腐层集成指导
- 06验证要点与常见陷阱
适用场景
- 01设计聚合及其跨边界引用
- 02实现领域事件与可靠外部副作用
- 03隔离第三方 API 与领域模型
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ rrezartprebreza/ spring-boot-skills --skill domain-driven-design安装后在 Claude Code 中输入/domain-driven-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlexamples/bad-aggregate.javagood-aggregate.javatemplates/DomainEvent.javaMoney.java详细介绍
领域驱动设计
聚合规则
- 每个聚合根对应一个仓储
- 外部代码只能通过聚合根访问聚合,绝不直接访问子实体
- 跨聚合边界时,优先通过 ID 引用;保留有意设置的映射。
- 根据必须原子提交的不变量和预期竞争情况来选择边界, 而不是按固定的子实体数量。拆分时必须制定明确的一致性方案。
以下代码片段仅用于说明,需要使用应用程序的领域类型和导入语句。 除非任务包含迁移,否则保留现有的公共标识符和持久化映射。
// ✅ Aggregate root controls all access to children
order.addItem(productId, quantity); // through root
order.removeItem(itemId); // through root
// ❌ Direct child access from outside
order.getItems().add(new OrderItem(...)); // bypasses invariants
值对象
不可变、无标识,通过值判断相等:
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Amount cannot be negative");
Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!currency.equals(other.currency))
throw new CurrencyMismatchException(currency, other.currency);
return new Money(amount.add(other.amount), currency);
}
public static Money of(String amount, String currency) {
return new Money(new BigDecimal(amount), Currency.getInstance(currency));
}
}
public record EmailAddress(String value) {
public EmailAddress {
if (!value.matches("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$"))
throw new InvalidEmailException(value);
}
}
领域事件
// Event — immutable record
public record OrderPlaced(OrderId orderId, CustomerId customerId, Money total, Instant occurredAt) {
public static OrderPlaced of(Order order) {
return new OrderPlaced(order.getId(), order.getCustomerId(), order.getTotal(), Instant.now());
}
}
// Collect events in aggregate, publish after save
@Entity
public class Order {
@Transient
private final List<Object> domainEvents = new ArrayList<>();
public void place() {
this.status = OrderStatus.PLACED;
domainEvents.add(OrderPlaced.of(this));
}
public List<Object> pullDomainEvents() {
var events = List.copyOf(domainEvents);
domainEvents.clear();
return events;
}
}
// Publish inside the transaction; save() is not commit.
@Service
@RequiredArgsConstructor
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order placeOrder(PlaceOrderCommand command) {
Order order = orderRepository.findById(command.orderId()).orElseThrow();
order.place();
Order saved = orderRepository.save(order);
saved.pullDomainEvents().forEach(eventPublisher::publishEvent); // commit-bound listener below
return saved;
}
}
// Listen to events — bind to commit, not just publish.
// @EventListener fires synchronously inside the TX; if the TX later rolls back you've
// already sent the email. AFTER_COMMIT avoids rollback delivery but is best effort.
@Component
@RequiredArgsConstructor
public class OrderPlacedHandler {
private final EmailService emailService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlaced event) {
emailService.sendOrderConfirmation(event.customerId(), event.orderId());
}
}
交由 Spring Data 为你发布。 不要手动调用
pullDomainEvents(),而是在聚合根上提供一个@DomainEvents方法(返回收集到的事件)和一个@AfterDomainEventPublication方法 (清除这些事件)。Spring Data 的仓储会在每次save()时自动提取并发布事件,无需在服务中手动配置。
这两种发布机制都不保证可靠交付。对于必须执行的外部操作,
请将发件箱条目与聚合变更一起持久化,或使用持久化发布注册表,
并采用幂等消费者以及重试/恢复机制。@Async 只改变执行方式,不改变持久性。
提交后写入数据库时需要开启新事务,因为原始资源在事务完成后可能仍处于绑定状态。请参阅事务边界、
消息传递和 Modulith。
规范(复杂查询)
public class OrderSpecifications {
public static Specification<Order> byStatus(OrderStatus status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<Order> byCustomer(UUID customerId) {
return (root, query, cb) -> cb.equal(root.get("customerId"), customerId);
}
public static Specification<Order> placedAfter(Instant date) {
return (root, query, cb) -> cb.greaterThan(root.get("placedAt"), date);
}
}
// Compose
Specification<Order> spec = OrderSpecifications.byStatus(PLACED)
.and(OrderSpecifications.byCustomer(customerId))
.and(OrderSpecifications.placedAfter(lastWeek));
orderRepository.findAll(spec, pageable);
防腐层(ACL)
- 与外部系统或旧版代码集成时,不要让它们的模型渗入你的领域
- 创建 ACL,即将外部数据转换为领域语言的翻译层
- ACL 属于基础设施层,而非领域层
// ✅ GOOD — ACL translates external payment API to domain concepts
@Component
@RequiredArgsConstructor
public class PaymentGatewayAdapter implements PaymentPort {
private final ExternalPaymentClient client; // third-party SDK
@Override
public PaymentConfirmation charge(OrderId orderId, Money amount) {
// Translate domain → external
PaymentApiRequest apiRequest = new PaymentApiRequest(
orderId.value().toString(),
amount.amount(), // illustrative provider accepts BigDecimal
amount.currency().getCurrencyCode());
// Call external system
PaymentApiResponse apiResponse = client.charge(apiRequest);
// Translate external → domain
return new PaymentConfirmation(
PaymentId.of(apiResponse.getTransactionId()),
apiResponse.isSuccessful() ? PaymentStatus.CONFIRMED : PaymentStatus.DECLINED);
}
}
使用提供方精确的金额表示方式。对于整数最小货币单位,应根据货币的小数位数进行转换,并使用精确算术;不要静默舍入,也不要将金额转换为二进制浮点数。
验证
测试聚合不变量、回滚如何阻止外部通知,以及提交后发生崩溃时如何恢复可靠发布。验证现有 API ID 和数据库关系仍与所请求的变更兼容。
注意事项
- Agent 创建只有 getter/setter 的贫血模型——应将行为放在领域对象中
- Agent 将现有的 Long/UUID API ID 替换为值对象——应保留公共契约;明确映射内部类型。
- Agent 将领域逻辑放入服务中——服务应负责编排,而非做决策
- Agent 从外部直接访问子实体——始终通过聚合根访问
- Agent 混淆 save 与 commit——在事务中发布,并有意选择监听器/可靠交付语义。
- Agent 按任意子实体数量拆分聚合——应根据业务不变量推导边界。
- Agent 将外部 API 模型引入领域——使用防腐层进行转换
官方资料
评论
支持 Markdown