
Ddd Architecture Layered
- 14 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Refactors traditional 3-layer apps into a DDD 4-layer architecture (Interface/Application/Domain/Infrastructure) with dependency inversion and ArchUnit checks.
About
Guides transformation of the traditional Controller/Service/DAO stack into a DDD four-layer architecture with dependency inversion. A developer uses it as a simple entry point to adopt DDD in small-to-medium teams.
- Directory structure and writing rules
- Step-by-step migration from three-layer
Ddd Architecture Layered by the numbers
- 14 all-time installs (skills.sh)
- Ranked #3,507 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/full-statck-skills/ddd-skills --skill ddd-architecture-layeredAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | full-statck-skills/ddd-skills ↗ |
What it does
Refactors traditional 3-layer apps into a DDD 4-layer architecture (Interface/Application/Domain/Infrastructure) with dependency inversion and ArchUnit checks.
Files
DDD Layered Architecture — 分层架构落地指南
DDD Layered Architecture 是 DDD 生态中最简的入门架构。它将传统三层(Controller/Service/DAO)重构为四层(Interface/Application/Domain/Infrastructure),通过依赖倒置使领域层成为系统核心。
Audience — 目标用户
| 用户类型 | 特征 | 推荐入口 |
|---|---|---|
| DDD 初学者 | 了解 MVC 三层,想入门 DDD | 先读 Core Principles → 看 06 单体简单示例 → 跟随 Implementation Phases |
| 架构师 | 负责项目架构选型和技术规划 | 先读 When to Use 边界 → 对照 06-12 规模谱系选型 → 看 Evolution 演进路径 |
| 高级开发者 | 有三层开发经验,需要落地分层 | 直接看 Directory Structure + Writing Rules → 使用 07/08 示例启动项目 |
| 技术负责人 | 评估团队引入 DDD 的可行性和成本 | 先读 When to Use + 场景边界 → 评估 Implementation Phases 时间 → 参考 09-12 微服务规划 |
Workflow
Step 1: awesome (入门) → 了解 DDD 概念
Step 2: selector (架构选型) → 确认分层架构适合
Step 3: layered (分层架构) ★ ← 你在这里
Step 4: domain-designer (领域设计) → 设计领域模型
Step 5: code-reviewer (审查) → 分层合规检查When to Use — 场景边界
✅ 适用场景
| # | 场景 | 说明 |
|---|---|---|
| 1 | 团队 < 10 人,DDD 刚起步 | 学习曲线低,分层架构是最简入口 |
| 2 | 中等复杂业务,有明确领域模型 | 聚合根清晰,充血模型收益大 |
| 3 | 已有三层项目逐步演进 | 三层→四层渐进迁移,不颠覆原架构 |
| 4 | Spring Boot/MyBatis 技术栈 | 国内主流技术栈,示例可直接复用 |
⚠️ 需要条件(慎重评估后可用)
| # | 场景 | 条件 |
|---|---|---|
| 1 | 团队 10-20 人 | 需先评估是否要物理模块隔离 → 见 08 多模块方案 |
| 2 | 多聚合根复杂业务 | 需确认聚合边界清晰 → 先做事件风暴再落地分层 |
| 3 | 已有微服务拆分意向 | 先按分层架构落地单服务 → 再参考 09-12 微服务方案拆分 |
| 4 | 非 Spring Boot 技术栈 | DDD 分层思想通用,但目录结构和代码示例需自行适配 |
❌ 不适用场景
| # | 场景 | 替代方案 |
|---|---|---|
| 1 | 纯 CRUD 无业务规则 | 直接用 Spring Boot 三层 + MyBatis-Plus 更省成本 |
| 2 | 多入口系统(REST+CLI+MQ) | 参考 Hexagonal Architecture(六边形架构)更合适 |
| 3 | 需要物理模块隔离 | 使用 COLA 架构或微服务方案(见 09-12 示例) |
| 4 | 高频基础设施替换 | 基础设施频繁变化时,推荐 Clean Architecture |
使用前提:有稳定领域专家 + 团队愿意切换到充血模型。
Core Principles — 核心原理
三层 → 四层
三层架构: DDD 四层:
Controller Interface (用户接口层)
↓ ↓
Service Application (应用层) — 纯编排
↓ ↓
DAO Domain (领域层) ★ — 核心业务
↓
Infrastructure (基础设施层)四个核心原则
| # | 原则 | 说明 |
|---|---|---|
| 1 | Domain 零依赖 | 不 import Spring/JPA/MyBatis |
| 2 | 依赖倒置 | Infra 实现 Domain 定义的接口 |
| 3 | Application 薄层 | 只编排,不放业务 if/else |
| 4 | Interface 协议转换 | 只做 DTO 转换,不含业务 |
Directory Structure
{project}/
├── {project}-interface/ # 用户接口层
│ ├── controller/ # REST 控制器
│ ├── dto/ # 请求/响应 DTO
│ │ ├── request/
│ │ └── response/
│ ├── converter/ # DTO ↔ Command 转换
│ └── advice/ # 全局异常处理
├── {project}-application/ # 应用层
│ ├── service/ # 应用服务(纯编排)
│ ├── command/ # 命令对象(写)
│ ├── query/ # 查询对象(读)
│ ├── assembler/ # DO ↔ DTO 组装
│ └── event/ # 事件处理
├── {project}-domain/ # 领域层 ★(零依赖)
│ ├── {aggregate}/ # 按聚合分包
│ │ ├── entity/ # 实体+聚合根(充血)
│ │ ├── valueobject/ # 值对象(不可变)
│ │ ├── service/ # 领域服务
│ │ ├── repository/ # 仓储接口(只定义)
│ │ └── event/ # 领域事件
│ ├── factory/ # 领域工厂
│ └── shared/ # 共享值对象
└── {project}-infrastructure/ # 基础设施层
├── repository/ # 仓储实现
├── persistence/ # PO 实体+映射
├── messaging/ # 消息队列
├── external/ # 外部服务
└── config/ # 配置Writing Rules — 开发规范
依赖方向:Interface → Application → Domain ← Infrastructure
| 层 | 可依赖 | 不可依赖 |
|---|---|---|
| Interface | Application | Domain、Infrastructure |
| Application | Domain | Interface、Infrastructure |
| Domain | JDK 原生类型 | 所有其他层 + 框架 |
| Infrastructure | Domain | Interface、Application |
代码命名:
- 聚合根:
Order(业务名,非 OrderEntity) - 值对象:
Money、Email(不可变,无 setter) - 仓储接口:
OrderRepository(在 Domain) - 仓储实现:
JpaOrderRepository(在 Infra) - 领域事件:
OrderPlaced(过去式)
禁止事项:
- 禁止 Controller 中出现 if/else 业务判断
- 禁止 Application Service 写 SQL
- 禁止 Domain 层 import Spring 或 JPA 注解
- 禁止跨聚合直接引用对象(用 ID 引用)
Implementation Phases
Phase 1: 识别聚合(1-2天)
→ 事件风暴 → 聚合根 → 不变式 → 通用语言表
Phase 2: 搭建分层骨架(1天)
→ 多模块 pom.xml → 基类(Entity/VO/AggregateRoot)
Phase 3: 实现领域层(2-5天)
→ 充血模型 → Repository 接口 → 领域服务 → 领域事件
Phase 4: 实现基础设施层(2-3天)
→ Repository 实现 → PO↔DO 映射 → DB 配置
Phase 5: 实现应用层(1-2天)
→ AppService 编排 → Command/Query → 事务管理
Phase 6: 实现接口层(1-2天)
→ Controller → DTO → 参数校验 → 异常处理
Phase 7: 审查验证(0.5天)
→ ddd-code-reviewer → ArchUnit → Domain 测试 ≥ 80%Evolution
Phase 1: 传统三层 ← 当前状态
Phase 2: 四层基础 ← 抽取 Domain + Infra
Phase 3: 充血模型 ← 实体含业务方法
Phase 4: 领域事件+CQRS ← L1 模型分离
Phase 5: 升级架构 ← 根据需要选 Hexagonal/Clean/COLAGotchas
15 个常见陷阱详见 references/09-gotchas.md。
FAQ
15 个常见问题详见 references/10-faq.md。
Keywords
分层架构, DDD 四层, 传统分层, 三层变四层, DDD 分层, layered architecture, DDD layered, three tier to DDD four layer, 依赖倒置, 充血模型, 贫血模型, 仓储模式, Repository 模式, Spring Boot DDD, MyBatis DDD, 目录结构 DDD, ArchUnit 分层检查, 依赖方向验证
References
| 文件 | 内容 |
|---|---|
| references/01-domain-layer.md | 领域层详解:实体、值对象、聚合、领域服务 |
| references/02-application-layer.md | 应用层详解:AppService、Command/Query、事务 |
| references/03-interface-layer.md | 接口层详解:Controller、DTO、异常处理 |
| references/04-infrastructure-layer.md | 基础设施层详解:Repository 实现、PO 映射 |
| references/05-dependency-rules.md | 依赖方向规则与层间通信约定 |
| references/06-migration-guide.md | 三层→四层渐进式迁移指南 |
| references/07-archunit-config.md | ArchUnit 自动化验证配置 |
| references/08-cqrs-integration.md | CQRS 轻量集成(L1 模型分离) |
| references/09-gotchas.md | 15 个常见陷阱 |
| references/10-faq.md | 15 个常见问题 |
Examples
| 文件 | 内容 |
|---|---|
| examples/spring-boot-order-example.md | Spring Boot 订单系统完整示例 |
| examples/ddd4j-springboot-practice.md | DDD4J Spring Boot 实战(Nova Coffee) |
| examples/partme-91-code-example.md | 在线请假系统完整代码 |
| examples/ddd4j-spring-boot-guide.md | Spring Boot DDD 分层实操指南 |
| examples/05-archunit-layered-config.md | ArchUnit 分层验证配置 |
| examples/06-monolith-simple.md | 单体简单项目:单模块四层分包 |
| examples/07-monolith-complex.md | 单体复杂项目:多聚合根 + 跨聚合编排 |
| examples/08-monolith-multi-module.md | 单体多模块:四层 Maven 模块隔离 |
| examples/09-microservice-simple.md | 微服务简单:每服务一个 DDD 分层单体 |
| examples/10-microservice-complex.md | 微服务复杂:事件驱动 + Saga 编排 |
| examples/11-microservice-multi-module.md | 微服务多模块:每服务四层子模块 |
| examples/12-microservice-complex-multi.md | 微服务复杂多模块:Shared Kernel + CQRS |
Output
当使用本 Skill 时,提供以下产出: 1. 项目骨架:完整的分层目录结构(多模块或单模块) 2. 基类模板:Entity/AggregateRoot/ValueObject/DomainEvent 3. 依赖配置:Maven/Gradle(含 ArchUnit、Spring Boot、JPA) 4. 完整 Demo:一个聚合端到端实现 5. ArchUnit 配置:依赖方向自动化验证 6. 演进路线图:三层→四层→可选架构升级 7. 迁移指南:存量三层项目迁移步骤
Security & Stability
- 代码模板为教学示例,请替换占位符为环境配置
- ArchUnit 规则强制分层边界,建议集成 CI 流水线
- DDD 四层结构隔离领域逻辑与基础设施,减少攻击面
- 本 Skill 不包含可执行脚本,所有操作为代码生成和审查
- 本 Skill 为纯文档型 Skill:不收集、不处理、不存储任何用户数据;不访问网络或外部服务;不使用任何第三方脚本。
Java 基于DDD分层架构的实操指南
原文:https://cloud.tencent.com/developer/article/2554516
随着微服务架构的普及,领域驱动设计(DDD)在复杂业务系统中的应用越来越广泛。本文将结合最新技术栈(Spring Boot 3.x、Spring Data JPA 3.x、Lombok等),通过一个电商订单系统的实例,详细讲解DDD分层架构的具体实现。
一、技术栈选择
- 核心框架:Spring Boot 3.2.x(支持Jakarta EE 9+)
- ORM框架:Spring Data JPA 3.2.x
- 简化代码:Lombok 1.18.x
- 验证框架:Jakarta Validation 3.0
- 数据库:PostgreSQL 16(支持JSON类型,适合存储值对象)
- 构建工具:Maven 3.9.x
二、项目结构设计
采用模块化设计,按DDD分层思想组织代码结构:
order-service/
├── src/main/java/com/example/order/
│ ├── application/ // 应用层
│ │ ├── command/ // 命令对象
│ │ ├── dto/ // 数据传输对象
│ │ ├── service/ // 应用服务
│ │ └── mapper/ // DTO与领域对象映射
│ ├── domain/ // 领域层
│ │ ├── model/ // 领域模型
│ │ │ ├── entity/ // 实体
│ │ │ ├── vo/ // 值对象
│ │ │ └── aggregate/ // 聚合根
│ │ ├── repository/ // 仓储接口
│ │ └── service/ // 领域服务
│ ├── infrastructure/ // 基础设施层
│ │ ├── persistence/ // 持久化实现
│ │ ├── messaging/ // 消息组件
│ │ └── config/ // 配置类
│ └── interfaces/ // 用户接口层
│ ├── rest/ // REST接口
│ └── facade/ // 外部服务接口
└── src/main/resources/
├── application.yml // 应用配置
└── db/migration/ // 数据库迁移脚本三、各层具体实现
1. 领域层实现(核心业务)
领域层是系统的核心,包含了所有业务规则和领域模型。以订单为例:
值对象实现:
// 货币值对象
@Value
public class Money {
@NonNull
BigDecimal amount;
@NonNull
Currency currency;
// 确保货币值不能为负
public Money(BigDecimal amount, Currency currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
this.amount = amount;
this.currency = currency;
}
// 金额加法操作
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}实体与聚合根实现:
// 订单项实体
@Entity
@Table(name = "order_item")
@Data
@NoArgsConstructor
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productId;
private String productName;
private int quantity;
@Embedded
private Money unitPrice;
// 计算订单项总价
public Money calculateTotal() {
return new Money(
unitPrice.getAmount().multiply(BigDecimal.valueOf(quantity)),
unitPrice.getCurrency()
);
}
}
// 订单聚合根
@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private String customerId;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Embedded
private Money totalAmount;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private List<OrderItem> items = new ArrayList<>();
// 领域行为:添加订单项
public void addItem(OrderItem item) {
this.items.add(item);
recalculateTotal();
}
// 领域行为:重新计算订单总价
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::calculateTotal)
.reduce(new Money(BigDecimal.ZERO, Currency.getInstance("CNY")), Money::add);
}
// 领域行为:确认订单
public void confirm() {
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("只有创建状态的订单可以确认");
}
this.status = OrderStatus.CONFIRMED;
}
}领域服务与仓储接口:
// 订单仓储接口
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
Optional<Order> findByOrderNumber(String orderNumber);
}
// 订单领域服务(处理跨聚合的业务逻辑)
@Service
@RequiredArgsConstructor
public class OrderDomainService {
private final InventoryService inventoryService;
// 检查订单商品库存
public boolean checkInventory(Order order) {
return order.getItems().stream()
.allMatch(item -> inventoryService.hasStock(item.getProductId(), item.getQuantity()));
}
}2. 应用层实现
应用层负责协调领域对象完成业务用例,处理事务和权限:
// 命令对象(封装创建订单的请求参数)
@Data
public class CreateOrderCommand {
@NotBlank(message = "客户ID不能为空")
private String customerId;
@NotEmpty(message = "订单项不能为空")
private List<OrderItemCommand> items;
}
// 应用服务
@Service
@RequiredArgsConstructor
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderDomainService orderDomainService;
private final OrderMapper orderMapper;
// 创建订单用例
public OrderDTO createOrder(CreateOrderCommand command) {
// 1. 转换命令为领域对象
Order order = orderMapper.toDomain(command);
// 2. 生成订单号
order.setOrderNumber(generateOrderNumber());
order.setStatus(OrderStatus.CREATED);
// 3. 业务规则校验
if (!orderDomainService.checkInventory(order)) {
throw new InsufficientInventoryException("商品库存不足");
}
// 4. 保存订单
Order savedOrder = orderRepository.save(order);
// 5. 发布领域事件(后续通过消息队列实现)
// orderEventPublisher.publish(new OrderCreatedEvent(savedOrder));
// 6. 转换为DTO返回
return orderMapper.toDTO(savedOrder);
}
// 确认订单用例
public void confirmOrder(String orderNumber) {
Order order = orderRepository.findByOrderNumber(orderNumber)
.orElseThrow(() -> new OrderNotFoundException("订单不存在: " + orderNumber));
order.confirm();
orderRepository.save(order);
}
// 生成唯一订单号
private String generateOrderNumber() {
return "ORD" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ RandomStringUtils.randomNumeric(4);
}
}3. 基础设施层实现
基础设施层提供技术支持,实现领域层定义的接口:
// 订单仓储JPA实现
@Repository
public interface JpaOrderRepository extends OrderRepository, JpaRepository<Order, Long> {
@Override
Optional<Order> findByOrderNumber(String orderNumber);
}
// 库存服务(外部服务调用实现)
@Service
public class InventoryServiceImpl implements InventoryService {
private final RestTemplate restTemplate;
@Override
public boolean hasStock(String productId, int quantity) {
// 调用库存服务API检查库存
String url = "http://inventory-service/api/inventory/check?productId=" + productId + "&quantity=" + quantity;
try {
return restTemplate.getForObject(url, Boolean.class);
} catch (Exception e) {
log.error("检查库存失败", e);
return false;
}
}
}4. 用户接口层实现
用户接口层处理HTTP请求和响应:
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderApplicationService orderApplicationService;
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@Valid @RequestBody CreateOrderCommand command) {
OrderDTO orderDTO = orderApplicationService.createOrder(command);
return ResponseEntity
.created(URI.create("/api/orders/" + orderDTO.getOrderNumber()))
.body(orderDTO);
}
@PutMapping("/{orderNumber}/confirm")
public ResponseEntity<Void> confirmOrder(@PathVariable String orderNumber) {
orderApplicationService.confirmOrder(orderNumber);
return ResponseEntity.noContent().build();
}
@GetMapping("/{orderNumber}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable String orderNumber) {
return orderApplicationService.getOrderByNumber(orderNumber)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}四、关键技术点说明
- 值对象的持久化:使用@Embedded和@Embeddable注解将值对象嵌入实体中,避免数据库表膨胀。
- 聚合根的设计:订单作为聚合根,负责维护订单项的一致性,所有对订单项的操作都通过订单聚合根完成。
- 领域事件处理:在应用服务中发布领域事件,通过Spring的事件机制或消息队列(如Kafka)实现领域事件的异步处理,解耦系统组件。
- 事务管理:在应用服务层使用@Transactional注解管理事务边界,确保业务操作的原子性。
- 对象映射:使用MapStruct框架实现DTO与领域对象之间的映射,避免手动编写大量getter/setter代码。
五、总结
基于DDD的分层架构通过清晰的职责划分,将业务逻辑与技术实现分离,使系统更具可维护性和扩展性。在实际项目中,应根据业务复杂度灵活调整DDD的实现方式,不必生搬硬套所有概念。
随着微服务架构的发展,DDD与微服务的结合越来越紧密,通过领域边界划分微服务可以有效降低服务间的耦合,这也是DDD在现代软件开发中越来越受欢迎的重要原因。
基于Springboot的DDD实战(不依赖框架)
领域驱动设计(DDD)是一把锋利的双刃剑。它既是斩断复杂业务“一团乱麻”的神兵利器,也可能在经验不足的团队手中,成为过度设计、拖累项目的沉重枷锁。
今天,我们一起结合经典之作《实现领域驱动设计》(红皮书),软件设计的基本原则,通过一个复杂的业务场景,探讨如何在Spring生态中,真正地、务实地落地DDD,并融入一些好的的工程实践。
1. 理念的基石:DDD不是银弹,而是战略罗盘
在开始之前,我们必须达成一个共识:DDD的核心价值在于战略设计,而非战术上的炫技。
- 传统分层架构的问题: 数据驱动,贫血模型,业务逻辑散落在大量的Service类中。当业务变得复杂时,这些Service会迅速膨胀,最终变成难以维护的“上帝类”。
- DDD的承诺: 将系统的核心——业务领域——置于中心地位。通过通用语言(Ubiquitous Language)统一团队认知,通过限界上下文(Bounded Context)拆分复杂问题,让软件的结构精准地反映业务的本质。
- 工程哲学共鸣:
- 高内聚、低耦合: 微服务理念本质上就是DDD限界上下文的物理实现。每个服务(上下文)都拥有自己的数据和业务逻辑,团队对其有完全的自主权。
- 清晰性与可测试性: 充血的领域模型将数据和行为封装在一起,使得业务规则的单元测试变得极其简单,这与对代码质量和可维护性的极致追求不谋而合。
- 设计文档(Design Docs): 任何重要的设计都需要文档化并进行评审。DDD的战略设计过程,尤其是上下文地图(Context Map),正是设计文档中最重要的输入。
2. 我们的竞技场:一个复杂的业务场景——“Nova Coffee” 新零售平台
为了让讨论不流于空泛,我们设定一个足够复杂的业务背景。
业务背景: “Nova Coffee”是一家精品连锁咖啡品牌,希望打造一个线上下单、线下履约、会员一体化的新零售平台。
核心流程:
- 用户端: 消费者通过App浏览商品、下单、支付、选择自提或外送。
- 门店端: 咖啡师在门店工作台(POS/平板)接收订单,制作饮品,完成订单(叫号自提或交由骑手)。
- 会员系统: 用户通过消费累积积分(星星),兑换优惠券,升级会员等级。
- 营销活动: 运营人员可以配置各种营销活动,如“第二杯半价”、“满30减5优惠券”等。
- 履约配送: 如果是外送单,系统需要与第三方运力平台对接,进行叫单、状态同步。
这个场景的复杂性在于,它涉及多个相互关联但又职责分明的业务领域。
3. 战略设计:绘制架构蓝图,而非一头扎进代码
这是落地DDD最关键,也最容易被忽视的一步。先别急着创建Spring Boot项目!
过程:
1. 识别领域与子域:
- 核心域 (Core Domain): 订单交易。这是业务的核心,是我们最需要投入精力的部分。
- 支撑子域 (Supporting Subdomain): 门店运营、营销活动。这些领域为核心域服务,需要我们自己构建,但不是业务的根本。
- 通用子域 (Generic Subdomain): 会员身份(可以是标准的认证授权服务)、支付(对接支付宝/微信)。这些是通用的功能,可以直接外购或使用开源方案。
2. 建立通用语言 (Ubiquitous Language):
- 与产品经理、业务专家一起,定义每个领域的通用语言。
- 订单领域:
订单(Order)、商品(Item)、消费者(Consumer)、支付(Payment)、履约方式(Fulfillment)。 - 门店领域:
订单(Order)(注意!此订单非彼订单)、饮品制作单(ProductionTicket)、咖啡师(Barista)、库存(Inventory)。 - 会员领域:
会员(Member)、星星(Star)、优惠券(Coupon)、等级(Tier)。 - 我们立刻发现,“订单”在不同领域含义不同。在交易中,它关心的是金额、商品列表;在门店,它关心的是制作要求、取餐码。这是划分限界上下文的强烈信号。
3. 定义限界上下文 (Bounded Context):
基于子域和通用语言的差异,我们定义限界上下文:
- Ordering Context (订单上下文)
- Store Operations Context (门店运营上下文)
- Membership Context (会员上下文)
- Marketing Context (营销上下文)
- Delivery Context (履约上下文)
4. 绘制上下文地图 (Context Map):
这是战略设计的核心产出,它定义了上下文之间的关系。我们将使用Spring Cloud来实现这些关系。

解读:
- 会员和营销是上游,为订单提供服务。订单通过防腐层(ACL)调用它们提供的开放主机服务(OHS)(通常是REST API)。ACL确保上游模型的变更不会污染下游的订单模型。
- 订单是核心,它通过发布语言(PL)(通常是领域事件,如OrderCreatedEvent)将状态变更通知下游的门店和履约上下文。这种异步、事件驱动的方式实现了完美的解耦。
4. 战术设计:在限界上下文中精雕细琢
现在,我们可以选择一个限界上下文,比如核心的Ordering Context,来深入战术设计和项目搭建。
4.1 项目结构 (多模块Maven/Gradle)
这是避免DDD被滥用的第一道防线:强制性的分层隔离。
nova-coffee/
├── pom.xml
└── ordering-context/
├── pom.xml
├── domain/ # 领域层 (纯粹的领域模型,无任何框架依赖)
│ ├── pom.xml
│ └── src/main/java/
│ └── com/novacoffee/ordering/domain/
│ ├── model/
│ │ ├── order/
│ │ │ ├── Order.java (聚合根)
│ │ │ ├── OrderItem.java (实体)
│ │ │ ├── OrderStatus.java (枚举)
│ │ │ └── Money.java (值对象)
│ │ └── ...
│ ├── event/
│ │ └── OrderCreatedEvent.java (领域事件)
│ ├── repository/
│ │ └── OrderRepository.java (仓储接口)
│ └── service/
│ └── PricingService.java (领域服务)
├── application/ # 应用层 (编排领域层,处理用例)
│ ├── pom.xml
│ └── src/main/java/
│ └── com/novacoffee/ordering/application/
│ ├── OrderingApplicationService.java (应用服务)
│ └── dto/
│ ├── CreateOrderCommand.java (命令)
│ └── OrderDTO.java (数据传输对象)
├── infrastructure/ # 基础设施层 (实现领域接口,与外界交互)
│ ├── pom.xml
│ └── src/main/java/
│ └── com/novacoffee/ordering/infrastructure/
│ ├── persistence/
│ │ └── JpaOrderRepository.java (JPA实现仓储)
│ ├── acl/
│ │ └── MembershipACL.java (防腐层实现)
│ └── messaging/
│ └── KafkaEventPublisher.java (事件发布实现)
└── interfaces/ # 接口层 (暴露API,处理外部请求)
├── pom.xml
└── src/main/java/
└── com/novacoffee/ordering/interfaces/
└── web/
└── OrderController.java (Spring MVC Controller)依赖关系: interfaces -> application -> domain。infrastructure -> domain。关键:domain层不依赖任何其他层,它是项目的核心和灵魂。
4.2 样例代码 (以Order聚合为例)
Domain层: Order.java (聚合根)
// package com.novacoffee.ordering.domain.model.order;
// 无Spring、JPA注解,纯粹的Java对象
public class Order {
private Long id;
private OrderStatus status;
private List<OrderItem> items;
private Money totalPrice;
private Long consumerId;
// 构造函数负责创建时业务规则校验
public Order(Long consumerId, List<OrderItem> items, PricingService pricingService) {
if (consumerId == null) throw new IllegalArgumentException("Consumer ID cannot be null.");
if (items == null || items.isEmpty()) throw new IllegalArgumentException("Order must have at least one item.");
this.consumerId = consumerId;
this.items = new ArrayList<>(items);
this.status = OrderStatus.PENDING_PAYMENT;
// 委托领域服务计算价格
this.totalPrice = pricingService.calculateTotalPrice(this.items);
}
// 业务方法,封装行为和状态变更
public void pay() {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("Order is not pending payment.");
}
this.status = OrderStatus.PAID;
// 此处可以发布领域事件: DomainEventPublisher.publish(new OrderPaidEvent(this.id));
}
// getters... (注意:仅暴露必要信息,保护内部状态)
}Domain层: OrderRepository.java (仓储接口)
// package com.novacoffee.ordering.domain.repository;
public interface OrderRepository {
Optional<Order> findById(Long id);
void save(Order order); // 保存操作涵盖了新增和更新
}Application层: OrderingApplicationService.java
// package com.novacoffee.ordering.application;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// ... imports
@Service
public class OrderingApplicationService {
private final OrderRepository orderRepository;
private final PricingService pricingService; // 领域服务
private final MembershipACL membershipACL; // 防腐层
// 构造函数注入
public OrderingApplicationService(...) { ... }
@Transactional
public Long createOrder(CreateOrderCommand command) {
// 1. 通过ACL获取外部信息
if (!membershipACL.isMemberActive(command.getConsumerId())) {
throw new BusinessException("Member is not active.");
}
// 2. 将DTO转换为领域对象
List<OrderItem> items = command.getItems().stream().map(...).collect(Collectors.toList());
// 3. 创建聚合根,执行领域逻辑
Order newOrder = new Order(command.getConsumerId(), items, pricingService);
// 4. 使用仓储持久化
orderRepository.save(newOrder);
// 5. (可选)发布领域事件
// eventPublisher.publish(new OrderCreatedEvent(newOrder.getId()));
return newOrder.getId();
}
}5. 架构师的红线:如何避免错误的DDD实践(实践反例)
1. 贫血模型 + 上帝Service (最常见的错误)
- 错误做法:
Order类只有一堆getter/setter。OrderingApplicationService里有上千行代码,包含了各种if/else来处理状态流转和价格计算。 - 为何错误: 这完全违背了DDD的封装原则,业务逻辑泄露,Order沦为数据载体。最终导致代码难以测试和维护。
- 正确做法: 如上例所示,将业务逻辑和规则(如
pay()方法)内聚到Order聚合根中。
2. 领域层被框架污染
- 错误做法: 在Order.java领域模型上添加@Entity, @Table, @Column等JPA注解。
- 为何错误: 这让你的核心领域模型与持久化技术紧紧绑定。如果有一天你想从JPA切换到MyBatis,或者甚至换成NoSQL数据库,将是一场灾难。
- 正确做法: 在infrastructure层创建单独的JPA实体OrderJpaEntity,并在仓储实现中完成领域对象Order和持久化对象OrderJpaEntity之间的转换(可以使用MapStruct等工具)。
3.无视限界上下文,跨服务直接查库
- 错误做法: Ordering Context的服务为了获取会员等级,直接配置Membership Context的数据库连接池,跨库JOIN查询。
- 为何错误: 这是微服务架构的头号杀手。它破坏了服务的封装性和自主性,两个团队被紧紧耦合在一起,一方的数据库变更可能导致另一方服务崩溃。
- 正确做法: 严格遵守上下文地图。Ordering只能通过Membership发布的API(OHS)或订阅其事件来获取数据。
4.万物皆聚合
- 错误做法: 把所有有关联的实体都塞进一个巨大的聚合里,比如Consumer聚合里包含了
List<Order>,List<Address>,List<Coupon>… - 为何错误: 聚合的设计原则是尽可能小,并保证事务的一致性。巨大的聚合会导致严重的性能问题(加载整张对象图)和并发冲突。
- 正确做法: 聚合之间通过ID引用。Order聚合中只包含consumerId,而不是整个Consumer对象。如果需要Consumer的信息,通过应用服务去查询。
DDD是一场回归软件本质的修行
将DDD与Spring生态结合是一件有趣的事。它要求我们不仅是代码的编写者,更是业务的思考者和模型的塑造者。
- 请记住,DDD的成功不在于你使用了多少时髦的模式,而在于:
- 你的代码能否让新来的业务人员看懂? (通用语言)
- 你的系统边界是否清晰,能否支持团队独立、高效地工作? (限界上下文)
- 你的核心业务逻辑是否被妥善地保护、封装和测试? (聚合根与领域模型)
———————————————— 版权声明:本文为CSDN博主「zero13_小葵司」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_42288219/article/details/152282099
项目回顾
“在线请假考勤”项目中,请假的核心业务流程是:请假人填写请假单提交审批;根据请假人身份、请假类型和请假天数进行校验并确定审批规则;根据审批规则确定审批人,逐级提交上级审批,逐级核批通过则完成审批,否则审批不通过则退回申请人。
请假微服务采用的DDD设计思想
请假微服务中用到了很多的DDD设计思想和方法,主要包括以下几个:
聚合中的对象
请假微服务包含请假(leave)、人员(person)和审批规则(rule)三个聚合。leave聚合完成请假申请和审核核心逻辑;person聚合管理人员信息和上下级关系;rule是一个单实体聚合,提供请假审批规则查询。
Leave是请假微服务的核心聚合,它有请假单聚合根leave、审批意见实体ApprovalInfo、请假申请人Applicant和审批人Approver值对象(它们的数据来源于person聚合),还有部分枚举类型,如请假类型LeaveType,请假单状态Status和审批状态类型ApprovalType等值对象。
下面我们通过代码来了解一下聚合根、实体以及值对象之间的关系。
1. 聚合根
聚合根leave中有属性、值对象、关联实体和自身的业务行为。Leave实体采用充血模型,有自己的业务行为,具体就是聚合根实体类的方法,如代码中的getDuration和addHistoryApprovalInfo等方法。
聚合根引用实体和值对象,它可以组合聚合内的多个实体,在聚合根实体类方法中完成复杂的业务行为,这种复杂的业务行为也可以在聚合领域服务里实现。但为了职责和边界清晰,我建议聚合要根据自身的业务行为在实体类方法中实现,而涉及多个实体组合才能实现的业务能力由领域服务完成。
下面是聚合根leave的实体类方法,它包含属性、对实体和值对象的引用以及自己的业务行为和方法。
public class Leave {
@Autowired
LeaveRepositoryInterface leaveRepositoryInterface;
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel; //审批领导的最高级别
ApprovalInfo currentApprovalInfo;
List<ApprovalInfo> historyApprovalInfos;
public long getDuration() {
return endTime.getTime() - startTime.getTime();
}
public Leave addHistoryApprovalInfo(ApprovalInfo approvalInfo) {
if (null == historyApprovalInfos)
historyApprovalInfos = new ArrayList<>();
this.historyApprovalInfos.add(approvalInfo);
return this;
}
public Leave create(){
this.setStatus(Status.APPROVING);
this.setStartTime(new Date());
return this;
}
//其它方法
}2. 实体
审批意见实体ApprovalInfo被leave聚合根引用,用于记录审批意见,它有自己的属性和值对象,如approver等,业务逻辑相对简单。
public class ApprovalInfo {
String approvalInfoId;
Approver approver;
ApprovalType approvalType;
String msg;
long time;
}3. 值对象
在Leave聚合有比较多的值对象。
我们先来看一下审批人值对象Approver。这类值对象除了属性集之外,还可以有简单的数据查询和转换服务。Approver数据来源于person聚合,从person聚合获取审批人返回后,从person实体获取personID、personName和level等属性,重新组合为approver值对象,因此需要数据转换和重新赋值。
Approver值对象同时被聚合根leave和实体approvalInfo引用。这类值对象的数据来源于其它聚合,不可修改,可重复使用。将这种对象设计为值对象而不是实体,可以提高系统性能,降低数据库实体关联的复杂度,所以我一般建议优先设计为值对象。
public class Approver {
String personId;
String personName;
int level; //管理级别
public static Approver fromPerson(Person person){
Approver approver = new Approver();
approver.setPersonId(person.getPersonId());
approver.setPersonName(person.getPersonName());
approver.setLevel(person.getRoleLevel());
return approver;
}
}下面是枚举类型的值对象Status的代码。
public enum Status {
APPROVING, APPROVED, REJECTED
}这里你要记住一点,由于值对象只做整体替换、不可修改的特性,在值对象中基本不会有修改或新增的方法。
4. 领域服务
如果一个业务行为由多个实体对象参与完成,我们就将这部分业务逻辑放在领域服务中实现。领域服务与实体方法的区别是:实体方法完成单一实体自身的业务逻辑,是相对简单的原子业务逻辑,而领域服务则是多个实体组合出的相对复杂的业务逻辑。两者都在领域层,实现领域模型的核心业务能力。
一个聚合可以设计一个领域服务类,管理聚合内所有的领域服务。
请假聚合的领域服务类是LeaveDomainService。领域服务中会用到很多的DDD设计模式,比如:用工厂模式实现复杂聚合的实体数据初始化,用仓储模式实现领域层与基础层的依赖倒置和用领域事件实现数据的最终一致性等。
public class LeaveDomainService {
@Autowired
EventPublisher eventPublisher;
@Autowired
LeaveRepositoryInterface leaveRepositoryInterface;
@Autowired
LeaveFactory leaveFactory;
@Transactional
public void createLeave(Leave leave, int leaderMaxLevel, Approver approver) {
leave.setLeaderMaxLevel(leaderMaxLevel);
leave.setApprover(approver);
leave.create();
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
LeaveEvent event = LeaveEvent.create(LeaveEventType.CREATE_EVENT, leave);
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
eventPublisher.publish(event);
}
@Transactional
public void updateLeaveInfo(Leave leave) {
LeavePO po = leaveRepositoryInterface.findById(leave.getId());
if (null == po) {
throw new RuntimeException("leave does not exist");
}
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
}
@Transactional
public void submitApproval(Leave leave, Approver approver) {
LeaveEvent event;
if (ApprovalType.REJECT == leave.getCurrentApprovalInfo().getApprovalType()) {
leave.reject(approver);
event = LeaveEvent.create(LeaveEventType.REJECT_EVENT, leave);
} else {
if (approver != null) {
leave.agree(approver);
event = LeaveEvent.create(LeaveEventType.AGREE_EVENT, leave); } else {
leave.finish();
event = LeaveEvent.create(LeaveEventType.APPROVED_EVENT, leave);
}
}
leave.addHistoryApprovalInfo(leave.getCurrentApprovalInfo());
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
eventPublisher.publish(event);
}
public Leave getLeaveInfo(String leaveId) {
LeavePO leavePO = leaveRepositoryInterface.findById(leaveId);
return leaveFactory.getLeave(leavePO);
}
public List<Leave> queryLeaveInfosByApplicant(String applicantId) {
List<LeavePO> leavePOList = leaveRepositoryInterface.queryByApplicantId(applicantId);
return leavePOList.stream().map(leavePO -> leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
public List<Leave> queryLeaveInfosByApprover(String approverId) {
List<LeavePO> leavePOList = leaveRepositoryInterface.queryByApproverId(approverId);
return leavePOList.stream().map(leavePO -> leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
}领域服务开发时的注意事项:
在领域服务或实体方法中,我们应尽量避免调用其它聚合的领域服务或引用其它聚合的实体或值对象,这种操作会增加聚合的耦合度。在微服务架构演进时,如果出现聚合拆分和重组,这种跨聚合的服务调用和对象引用,会变成跨微服务的操作,导致这种跨聚合的领域服务调用和对象引用失效,在聚合分拆时会增加你代码解耦和重构的工作量。
以下是一段不建议使用的代码。在这段代码里Approver是leave聚合的值对象,它作为对象参数被传到person聚合的findNextApprover领域服务。如果在同一个微服务内,这种方式是没有问题的。但在架构演进时,如果person和leave两个聚合被分拆到不同的微服务中,那么leave中的Approver对象以及它的getPersonId()和fromPersonPO方法在person聚合中就会失效,这时你就需要进行代码重构了。
public class PersonDomainService {
public Approver findNextApprover(Approver currentApprover, int leaderMaxLevel) {
PersonPO leaderPO = personRepository.findLeaderByPersonId(currentApprover.getPersonId());
if (leaderPO.getRoleLevel() > leaderMaxLevel) {
return null;
} else {
return Approver.fromPersonPO(leaderPO);
}
}
}那正确的方式是什么样的呢?在应用服务组合不同聚合的领域服务时,我们可以通过ID或者参数来传数,如单一参数currentApproverId。这样聚合之间就解耦了,下面是修改后的代码,它可以不依赖其它聚合的实体,独立完成业务逻辑。
public class PersonDomainService {
public Person findNextApprover(String currentApproverId, int leaderMaxLevel) {
PersonPO leaderPO = personRepository.findLeaderByPersonId(currentApproverId);
if (leaderPO.getRoleLevel() > leaderMaxLevel) {
return null;
} else {
return personFactory.createPerson(leaderPO);
}
}
}领域事件
在创建请假单和请假审批过程中会产生领域事件。为了方便管理,我们将聚合内的领域事件相关的代码放在聚合的event目录中。领域事件实体在聚合仓储内完成持久化,但是事件实体的生命周期不受聚合根管理。
1. 领域事件基类DomainEvent
你可以建立统一的领域事件基类DomainEvent。基类包含:事件ID、时间戳、事件源以及事件相关的业务数据。
public class DomainEvent {
String id;
Date timestamp;
String source;
String data;
}2. 领域事件实体
请假领域事件实体LeaveEvent继承基类DomainEvent。可根据需要扩展属性和方法,如leaveEventType。data字段中存储领域事件相关的业务数据,可以是XML或Json等格式。
public class LeaveEvent extends DomainEvent {
LeaveEventType leaveEventType;
public static LeaveEvent create(LeaveEventType eventType, Leave leave){
LeaveEvent event = new LeaveEvent();
event.setId(IdGenerator.nextId());
event.setLeaveEventType(eventType);
event.setTimestamp(new Date());
event.setData(JSON.toJSONString(leave));
return event;
}
}3. 领域事件的执行逻辑
一般来说,领域事件的执行逻辑如下:
第一步:执行业务逻辑,产生领域事件。
第二步:完成业务数据持久化。
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));第三步:完成事件数据持久化。
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));第四步:完成领域事件发布。
eventPublisher.publish(event);以上领域事件处理逻辑代码详见LeaveDomainService中submitApproval领域服务,里面有请假提交审批事件的完整处理逻辑。
4. 领域事件数据持久化
为了保证事件发布方与事件订阅方数据的最终一致性和数据审计,有些业务场景需要建立数据对账机制。数据对账主要通过对源端和目的端的持久化数据比对,从而发现异常数据并进一步处理,保证数据最终一致性。
对于需要对账的事件数据,我们需设计领域事件对象的持久化对象PO,完成领域事件数据的持久化,如LeaveEvent事件实体的持久化对象LeaveEventPO。再通过聚合的仓储完成数据持久化:
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event))。事件数据持久化对象LeaveEventPO格式如下:
public class LeaveEventPO {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
int id;
@Enumerated(EnumType.STRING)
LeaveEventType leaveEventType;
Date timestamp;
String source;
String data;
}仓储模式
领域模型中DO实体的数据持久化是必不可少的,DDD采用仓储模式实现数据持久化,使得业务逻辑与基础资源逻辑解耦,实现依赖倒置。持久化时先完成DO与PO对象的转换,然后在仓储服务中完成PO对象的持久化。
1. DO与PO对象的转换
Leave聚合根的DO实体除了自身的属性外,还会根据领域模型引用多个值对象,如Applicant和Approver等,它们包含多个属性,如:personId、personName和personType等属性。
在持久化对象PO设计时,你可以将这些值对象属性嵌入PO属性中,或设计一个组合属性字段,以Json串的方式存储在PO中。
以下是leave的DO的属性定义:
public class Leave {
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel;
ApprovalInfo currentApprovalInfo;
List<ApprovalInfo> historyApprovalInfos;
}
public class Applicant {
String personId;
String personName;
String personType;
}
public class Approver {
String personId;
String personName;
int level;
}为了减少数据库表数量以及表与表的复杂关联关系,我们将leave实体和多个值对象放在一个LeavePO中。如果以属性嵌入的方式,Applicant值对象在LeavePO中会展开为:applicantId、applicantName和applicantType三个属性。
以下为采用属性嵌入方式的持久化对象LeavePO的结构。
public class LeavePO {
@Id
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator="idGenerator")
String id;
String applicantId;
String applicantName;
@Enumerated(EnumType.STRING)
PersonType applicantType;
String approverId;
String approverName;
@Enumerated(EnumType.STRING)
LeaveType leaveType;
@Enumerated(EnumType.STRING)
Status status;
Date startTime;
Date endTime;
long duration;
@Transient
List<ApprovalInfoPO> historyApprovalInfoPOList;
}2. 仓储模式
为了解耦业务逻辑和基础资源,我们可以在基础层和领域层之间增加一层仓储服务,实现依赖倒置。通过这一层可以实现业务逻辑和基础层资源的依赖分离。在变更基础层数据库的时候,你只要替换仓储实现就可以了,上层核心业务逻辑不会受基础资源变更的影响,从而实现依赖倒置。
一个聚合一个仓储,实现聚合数据的持久化。领域服务通过仓储接口来访问基础资源,由仓储实现完成数据持久化和初始化。仓储一般包含:仓储接口和仓储实现。
2.1仓储接口
仓储接口面向领域服务提供接口。
public interface LeaveRepositoryInterface {
void save(LeavePO leavePO);
void saveEvent(LeaveEventPO leaveEventPO);
LeavePO findById(String id);
List<LeavePO> queryByApplicantId(String applicantId);
List<LeavePO> queryByApproverId(String approverId);
}2.2仓储实现
仓储实现完成数据持久化和数据库查询。
@Repository
public class LeaveRepositoryImpl implements LeaveRepositoryInterface {
@Autowired
LeaveDao leaveDao;
@Autowired
ApprovalInfoDao approvalInfoDao;
@Autowired
LeaveEventDao leaveEventDao;
public void save(LeavePO leavePO) {
leaveDao.save(leavePO);
approvalInfoDao.saveAll(leavePO.getHistoryApprovalInfoPOList());
}
public void saveEvent(LeaveEventPO leaveEventPO){
leaveEventDao.save(leaveEventPO);
}
@Override
public LeavePO findById(String id) {
return leaveDao.findById(id)
.orElseThrow(() -> new RuntimeException("leave not found"));
}
@Override
public List<LeavePO> queryByApplicantId(String applicantId) {
List<LeavePO> leavePOList = leaveDao.queryByApplicantId(applicantId);
leavePOList.stream()
.forEach(leavePO -> {
List<ApprovalInfoPO> approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
@Override
public List<LeavePO> queryByApproverId(String approverId) {
List<LeavePO> leavePOList = leaveDao.queryByApproverId(approverId);
leavePOList.stream()
.forEach(leavePO -> {
List<ApprovalInfoPO> approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
}这里持久化组件采用了Jpa。
public interface LeaveDao extends JpaRepository<LeavePO, String> {
List<LeavePO> queryByApplicantId(String applicantId);
List<LeavePO> queryByApproverId(String approverId);
}2.3仓储执行逻辑
以创建请假单为例,仓储的执行步骤如下。
第一步:仓储执行之前将聚合内DO会转换为PO,这种转换在工厂服务中完成:
leaveFactory.createLeavePO(leave)。第二步:完成对象转换后,领域服务调用仓储接口:
leaveRepositoryInterface.save。第三步:由仓储实现完成PO对象持久化。
代码执行步骤如下:
public void createLeave(Leave leave, int leaderMaxLevel, Approver approver) {
leave.setLeaderMaxLevel(leaderMaxLevel);
leave.setApprover(approver);
leave.create();
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
}工厂模式
对于大型的复杂领域模型,聚合内的聚合根、实体和值对象之间的依赖关系比较复杂,这种过于复杂的依赖关系,不适合通过根实体构造器来创建。为了协调这种复杂的领域对象的创建和生命周期管理,在DDD里引入了工厂模式(Factory),在工厂里封装复杂的对象创建过程。
当聚合根被创建时,聚合内所有依赖的对象将会被同时创建。
工厂与仓储模式往往结对出现,应用于数据的初始化和持久化两类场景。
DO对象的初始化:获取持久化对象PO,通过工厂一次构建出聚合根所有依赖的DO对象,完数据初始化。
DO的对象持久化:将所有依赖的DO对象一次转换为PO对象,完成数据持久化。
下面代码是leave聚合的工厂类LeaveFactory。其中createLeavePO(leave)方法组织leave聚合的DO对象和值对象完成leavePO对象的构建。getLeave(leave)通过持久化对象PO构建聚合的DO对象和值对象,完成leave聚合DO实体的初始化。
public class LeaveFactory {
public LeavePO createLeavePO(Leave leave) {
LeavePO leavePO = new LeavePO();
leavePO.setId(UUID.randomUUID().toString());
leavePO.setApplicantId(leave.getApplicant().getPersonId());
leavePO.setApplicantName(leave.getApplicant().getPersonName());
leavePO.setApproverId(leave.getApprover().getPersonId());
leavePO.setApproverName(leave.getApprover().getPersonName());
leavePO.setStartTime(leave.getStartTime());
leavePO.setStatus(leave.getStatus());
List<ApprovalInfoPO> historyApprovalInfoPOList = approvalInfoPOListFromDO(leave);
leavePO.setHistoryApprovalInfoPOList(historyApprovalInfoPOList);
return leavePO;
}
public Leave getLeave(LeavePO leavePO) {
Leave leave = new Leave();
Applicant applicant = Applicant.builder()
.personId(leavePO.getApplicantId())
.personName(leavePO.getApplicantName())
.build();
leave.setApplicant(applicant);
Approver approver = Approver.builder()
.personId(leavePO.getApproverId())
.personName(leavePO.getApproverName())
.build();
leave.setApprover(approver);
leave.setStartTime(leave.getStartTime());
leave.setStatus(leave.getStatus());
List<ApprovalInfo> approvalInfos = getApprovalInfos(leavePO.getHistoryApprovalInfoPOList());
leave.setHistoryApprovalInfos(approvalInfos);
return leave;
}
//其它方法
}服务的组合与编排
应用层的应用服务完成领域服务的组合与编排。一个聚合的应用服务可以建立一个应用服务类,管理聚合所有的应用服务。比如leave聚合有LeaveApplicationService,person聚合有PersonApplicationService。
在请假微服务中,有三个聚合:leave、person和rule。我们来看一下应用服务是如何跨聚合来进行服务的组合和编排的。以创建请假单createLeaveInfo应用服务为例,分为这样三个步骤。
第一步:根据请假单定义的人员类型、请假类型和请假时长从rule聚合中获取请假审批规则。这一步通过approvalRuleDomainService类的getLeaderMaxLevel领域服务来实现。
第二步:根据请假审批规则,从person聚合中获取请假审批人。这一步通过personDomainService类的findFirstApprover领域服务来实现。
第三步:根据请假数据和从rule和person聚合获取的数据,创建请假单。这一步通过leaveDomainService类的createLeave领域服务来实现。
由于领域核心逻辑已经很好地沉淀到了领域层中,领域层的这些核心逻辑可以高度复用。应用服务只需要灵活地组合和编排这些不同聚合的领域服务,就可以很容易地适配前端业务的变化。因此应用层不会积累太多的业务逻辑代码,所以会变得很薄,代码维护起来也会容易得多。
以下是leave聚合的应用服务类。代码是不是非常得少?
public class LeaveApplicationService{
@Autowired
LeaveDomainService leaveDomainService;
@Autowired
PersonDomainService personDomainService;
@Autowired
ApprovalRuleDomainService approvalRuleDomainService;
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
public void updateLeaveInfo(Leave leave){
leaveDomainService.updateLeaveInfo(leave);
}
public void submitApproval(Leave leave){
//find next approver
Person approver = personDomainService.findNextApprover(leave.getApprover().getPersonId(), leave.getLeaderMaxLevel());
leaveDomainService.submitApproval(leave, Approver.fromPerson(approver));
}
public Leave getLeaveInfo(String leaveId){
return leaveDomainService.getLeaveInfo(leaveId);
}
public List<Leave> queryLeaveInfosByApplicant(String applicantId){
return leaveDomainService.queryLeaveInfosByApplicant(applicantId);
}
public List<Leave> queryLeaveInfosByApprover(String approverId){
return leaveDomainService.queryLeaveInfosByApprover(approverId);
}
}应用服务开发注意事项:
为了聚合解耦和微服务架构演进,应用服务在对不同聚合领域服务进行编排时,应避免不同聚合的实体对象,在不同聚合的领域服务中引用,这是因为一旦聚合拆分和重组,这些跨聚合的对象将会失效。
在LeaveApplicationService中,leave实体和Applicant值对象分别作为参数被rule聚合和person聚合的领域服务引用,这样会增加聚合的耦合度。下面是不推荐使用的代码。
public class LeaveApplicationService{
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
ApprovalRule rule = approvalRuleDomainService.getLeaveApprovalRule(leave);
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(rule);
leave.setLeaderMaxLevel(leaderMaxLevel);
//find next approver
Approver approver = personDomainService.findFirstApprover(leave.getApplicant(), leaderMaxLevel);
leave.setApprover(approver);
leaveDomainService.createLeave(leave);
}
}那如何实现聚合的解耦呢?我们可以将跨聚合调用时的对象传值调整为参数传值。一起来看一下调整后的代码,getLeaderMaxLevel由leave对象传值调整为personType,leaveType和duration参数传值。findFirstApprover中Applicant值对象调整为personId参数传值。
public class LeaveApplicationService{
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
}在微服务演进和聚合重组时,就不需要进行聚合解耦和代码重构了。
微服务聚合拆分时的代码演进
如果请假微服务未来需要演进为人员和请假两个微服务,我们可以基于请假leave和人员person两个聚合来进行拆分。由于两个聚合已经完全解耦,领域逻辑非常稳定,在微服务聚合代码拆分时,聚合领域层的代码基本不需要调整。调整主要集中在微服务的应用服务中。
我们以应用服务createLeaveInfo为例,当一个微服务拆分为两个微服务时,看看代码需要做什么样的调整?
1. 微服务拆分前
createLeaveInfo应用服务的代码如下:
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}2. 微服务拆分后
leave和person两个聚合随微服务拆分后,createLeaveInfo应用服务中下面的代码将会变成跨微服务调用。
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);由于跨微服务的调用是在应用层完成的,我们只需要调整createLeaveInfo应用服务代码,将原来微服务内的服务调用personDomainService.findFirstApprover修改为跨微服务的服务调用:personFeignService. findFirstApprover。
同时新增ApproverAssembler组装器和PersonResponse的DTO对象,以便将person微服务返回的person DTO对象转换为approver值对象。
// PersonResponse为调用微服务返回结果的封装
//通过personFeignService调用Person微服务用户接口层的findFirstApprover facade接口
PersonResponse approverResponse = personFeignService. findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
Approver approver = ApproverAssembler.toDO(approverResponse);在原来的person聚合中,由于findFirstApprover领域服务已经逐层封装为用户接口层的Facade接口,所以person微服务不需要做任何代码调整,只需将PersonApi的findFirstApprover Facade服务,发布到API网关即可。
如果拆分前person聚合的findFirstApprover领域服务,没有被封装为Facade接口,我们只需要在person微服务中按照以下步骤调整即可。
第一步:将person聚合PersonDomainService类中的领域服务findFirstApprover封装为应用服务findFirstApprover。
@Service
public class PersonApplicationService {
@Autowired
PersonDomainService personDomainService;
public Person findFirstApprover(String applicantId, int leaderMaxLevel) {
return personDomainService.findFirstApprover(applicantId, leaderMaxLevel);
}
}第二步:将应用服务封装为Facade服务,并发布到API网关。
@RestController
@RequestMapping("/person")
@Slf4j
public class PersonApi {
@Autowired
@GetMapping("/findFirstApprover")
public Response findFirstApprover(@RequestParam String applicantId, @RequestParam int leaderMaxLevel) {
Person person = personApplicationService.findFirstApprover(applicantId, leaderMaxLevel);
return Response.ok(PersonAssembler.toDTO(person));
}
}服务接口的提供
用户接口层是前端应用与微服务应用层的桥梁,通过Facade接口封装应用服务,适配前端并提供灵活的服务,完成DO和DTO相互转换。
当应用服务接收到前端请求数据时,组装器会将DTO转换为DO。当应用服务向前端返回数据时,组装器会将DO转换为DTO。
1. facade接口
facade接口可以是一个门面接口实现类,也可以是门面接口加一个门面接口实现类。项目可以根据前端的复杂度进行选择,由于请假微服务前端功能相对简单,我们就直接用一个门面接口实现类来实现就可以了。
public class LeaveApi {
@PostMapping
public Response createLeaveInfo(LeaveDTO leaveDTO){
Leave leave = LeaveAssembler.toDO(leaveDTO);
leaveApplicationService.createLeaveInfo(leave);
return Response.ok();
}
@PostMapping("/query/applicant/{applicantId}")
public Response queryByApplicant(@PathVariable String applicantId){
List<Leave> leaveList = leaveApplicationService.queryLeaveInfosByApplicant(applicantId);
List<LeaveDTO> leaveDTOList = leaveList.stream().map(leave -> LeaveAssembler.toDTO(leave)).collect(Collectors.toList());
return Response.ok(leaveDTOList);
}
//其它方法
}2. DTO数据组装
组装类(Assembler):负责将应用服务返回的多个DO对象组装为前端DTO对象,或将前端请求的DTO对象转换为多个DO对象,供应用服务作为参数使用。组装类中不应有业务逻辑,主要负责格式转换、字段映射等。Assembler往往与DTO同时存在。LeaveAssembler完成请假DO和DTO数据相互转换。
public class LeaveAssembler {
public static LeaveDTO toDTO(Leave leave){
LeaveDTO dto = new LeaveDTO();
dto.setLeaveId(leave.getId());
dto.setLeaveType(leave.getType().toString());
dto.setStatus(leave.getStatus().toString());
dto.setStartTime(DateUtil.formatDateTime(leave.getStartTime()));
dto.setEndTime(DateUtil.formatDateTime(leave.getEndTime()));
dto.setCurrentApprovalInfoDTO(ApprovalInfoAssembler.toDTO(leave.getCurrentApprovalInfo()));
List<ApprovalInfoDTO> historyApprovalInfoDTOList = leave.getHistoryApprovalInfos()
.stream()
.map(historyApprovalInfo -> ApprovalInfoAssembler.toDTO(leave.getCurrentApprovalInfo()))
.collect(Collectors.toList());
dto.setHistoryApprovalInfoDTOList(historyApprovalInfoDTOList);
dto.setDuration(leave.getDuration());
return dto;
}
public static Leave toDO(LeaveDTO dto){
Leave leave = new Leave();
leave.setId(dto.getLeaveId());
leave.setApplicant(ApplicantAssembler.toDO(dto.getApplicantDTO()));
leave.setApprover(ApproverAssembler.toDO(dto.getApproverDTO()));
leave.setCurrentApprovalInfo(ApprovalInfoAssembler.toDO(dto.getCurrentApprovalInfoDTO()));
List<ApprovalInfo> historyApprovalInfoDTOList = dto.getHistoryApprovalInfoDTOList()
.stream()
.map(historyApprovalInfoDTO -> ApprovalInfoAssembler.toDO(historyApprovalInfoDTO))
.collect(Collectors.toList());
leave.setHistoryApprovalInfos(historyApprovalInfoDTOList);
return leave;
}
}DTO类:包括requestDTO和responseDTO两部分。
DTO应尽量根据前端展示数据的需求来定义,避免过多地暴露后端业务逻辑。尤其对于多渠道场景,可以根据渠道属性和要求,为每个渠道前端应用定义个性化的DTO。由于请假微服务相对简单,我们可以用leaveDTO代码做个示例。
@Data
public class LeaveDTO {
String leaveId;
ApplicantDTO applicantDTO;
ApproverDTO approverDTO;
String leaveType;
ApprovalInfoDTO currentApprovalInfoDTO;
List<ApprovalInfoDTO> historyApprovalInfoDTOList;
String startTime;
String endTime;
long duration;
String status;
}今天我们了解了用DDD开发出来的微服务代码到底是什么样的。你可以将这些核心设计思想逐步引入到项目中去,慢慢充实自己的DDD知识体系。我还想再重点强调的是:由于架构的演进,微服务与生俱来就需要考虑聚合的未来重组。因此微服务的设计和开发要做到未雨绸缪,而这最关键的就是解耦了。
聚合与聚合的解耦:当多个聚合在同一个微服务时,很多传统架构开发人员会下意识地引用其他聚合的实体和值对象,或者调用其它聚合的领域服务。因为这些聚合的代码在同一个微服务内,运行时不会有问题,开发效率似乎也更高,但这样会不自觉地增加聚合之间的耦合。在微服务架构演进时,如果聚合被分别拆分到不同的微服务中,原来微服务内的关系就会变成跨微服务的关系,原来微服务内的对象引用或服务调用将会失效。最终你还是免不了要花大量的精力去做聚合解耦。虽然前期领域建模和边界划分得很好,但可能会因为开发稍不注意,而导致解耦工作前功尽弃。
微服务内各层的解耦:微服务内有四层,在应用层和领域层组成核心业务领域的两端,有两个缓冲区或数据转换区。前端与应用层通过组装器实现DTO和DO的转换,这种适配方式可以更容易地响应前端需求的变化,隐藏核心业务逻辑的实现,保证核心业务逻辑的稳定,实现核心业务逻辑与前端应用的解耦。而领域层与基础层通过仓储和工厂模式实现DO和PO的转换,实现应用逻辑与基础资源逻辑的解耦。
最后我想说,DDD知识体系虽大,但你可以根据企业的项目场景和成本要求,逐步引入适合自己的DDD方法和技术,建立适合自己的DDD开发模式和方法体系。
这一期的加餐到这就结束了,希望你能对照完整代码认真阅读今天的内容,有什么疑问,欢迎在留言区与我交流!
Spring Boot DDD 分层架构实操示例
技术栈
- Spring Boot 3.2.x + Spring Data JPA 3.2.x + Lombok 1.18.x
- Jakarta Validation 3.0 + PostgreSQL 16
- Maven 3.9.x
聚合根示例(充血模型)
@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private String customerId;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Embedded
private Money totalAmount;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private List<OrderItem> items = new ArrayList<>();
// 领域行为
public void addItem(OrderItem item) {
this.items.add(item);
recalculateTotal();
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::calculateTotal)
.reduce(Money.zero(), Money::add);
}
public void confirm() {
if (this.status != OrderStatus.CREATED) {
throw new InvalidOrderStateException("只有已创建状态的订单才能确认");
}
this.status = OrderStatus.CONFIRMED;
}
}值对象示例
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public static Money zero() {
return new Money(BigDecimal.ZERO, "CNY");
}
}仓储接口
public interface OrderRepository {
Optional<Order> findById(Long id);
Order save(Order order);
List<Order> findByCustomerId(String customerId);
}应用服务
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
public OrderDTO createOrder(CreateOrderCommand command) {
Order order = new Order();
order.setCustomerId(command.getCustomerId());
for (OrderItemCommand itemCmd : command.getItems()) {
order.addItem(new OrderItem(itemCmd.getProductId(), itemCmd.getQuantity(), itemCmd.getUnitPrice()));
}
order.confirm();
return OrderMapper.toDTO(orderRepository.save(order));
}
}源代码
ArchUnit 分层架构验证配置示例
依赖
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>分层规则验证
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.*;
public class LayeredArchitectureTest {
private final JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.order");
@Test
void verifyLayeredArchitecture() {
ArchRule rule = layeredArchitecture()
.consideringAllDependencies()
.layer("Interface").definedBy("..interface..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
.whereLayer("Interface").mayOnlyBeAccessedByLayers("Interface")
.whereLayer("Application").mayOnlyBeAccessedByLayers("Interface", "Application")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
.whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Infrastructure");
rule.check(classes);
}
@Test
void domainMustNotDependOnSpring() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAnyPackage("org.springframework..")
.because("Domain 层不能依赖 Spring 框架")
.check(classes);
}
@Test
void domainMustNotDependOnInfrastructure() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..")
.because("Domain 层不能依赖 Infrastructure 层")
.check(classes);
}
@Test
void applicationMustNotDependOnInterface() {
noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat()
.resideInAPackage("..interface..")
.because("Application 层不能依赖 Interface 层")
.check(classes);
}
@Test
void repositoryImplShouldBeInInfrastructure() {
classes()
.that().haveSimpleNameEndingWith("RepositoryImpl")
.should().resideInAPackage("..infrastructure..")
.because("Repository 实现必须在 Infrastructure 层")
.check(classes);
}
@Test
void repositoryInterfaceShouldBeInDomain() {
classes()
.that().haveSimpleNameEndingWith("Repository")
.and().areInterfaces()
.should().resideInAPackage("..domain..")
.because("Repository 接口必须在 Domain 层")
.check(classes);
}
}Maven 插件集成(CI 阶段自动执行)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<includes>
<include>**/*ArchitectureTest.java</include>
</includes>
</configuration>
</plugin>关键说明
- ArchUnit 在测试阶段执行,不影响运行时性能
- 规则失败构建即中断,防止违规代码合入
- 建议将
LayeredArchitectureTest放在src/test/java的根包下 - 包名模式
..interface..表示任何层级下的对应包
单体简单项目 — DDD 分层示例
适用场景
| 维度 | 描述 |
|---|---|
| 团队规模 | 3-8 人,DDD 初学者 |
| 业务复杂度 | 单一聚合根(如订单),CRUD 为主但含状态机 |
| 部署方式 | 单体 Spring Boot JAR,单数据库 |
| 技术栈 | Spring Boot + MyBatis/JPA + Maven |
| 迁移起点 | 传统三层(Controller/Service/DAO)逐步演进 |
典型业务:订单管理、用户注册登录、简单审批流。
目录树
order-simple/
├── pom.xml
└── src/
└── main/
└── java/
└── com/example/order/
├── OrderApplication.java # Spring Boot 入口
│
├── interface/ # 用户接口层
│ ├── controller/
│ │ └── OrderController.java
│ ├── dto/
│ │ ├── request/
│ │ │ ├── CreateOrderRequest.java
│ │ │ └── UpdateOrderRequest.java
│ │ └── response/
│ │ └── OrderResponse.java
│ └── converter/
│ └── OrderDtoConverter.java # DTO ↔ Command 转换
│
├── application/ # 应用层
│ ├── service/
│ │ └── OrderApplicationService.java
│ ├── command/
│ │ ├── CreateOrderCommand.java
│ │ └── UpdateOrderCommand.java
│ └── query/
│ └── OrderQuery.java
│
├── domain/ # 领域层 ★(零框架依赖)
│ ├── order/
│ │ ├── entity/
│ │ │ ├── Order.java # 聚合根(充血模型)
│ │ │ └── OrderItem.java # 实体
│ │ ├── valueobject/
│ │ │ ├── Money.java
│ │ │ ├── OrderStatus.java # 状态枚举
│ │ │ └── Address.java
│ │ ├── service/
│ │ │ └── OrderDomainService.java # 跨实体逻辑
│ │ ├── repository/
│ │ │ └── OrderRepository.java # 仓储接口(只定义)
│ │ └── event/
│ │ └── OrderPlacedEvent.java # 领域事件
│ └── shared/
│ └── BaseEntity.java # 公共基类
│
└── infrastructure/ # 基础设施层
├── repository/
│ └── MyBatisOrderRepository.java # 仓储实现
├── persistence/
│ ├── OrderPO.java # 持久化对象
│ └── OrderItemPO.java
└── config/
└── DddConfiguration.java # DI 配置包结构
com.example.order
├── interface → 依赖 application
├── application → 依赖 domain
├── domain → 零依赖(仅 JDK)
└── infrastructure → 依赖 domain模块依赖
<!-- 单模块 pom.xml,无父子模块划分 -->
<groupId>com.example</groupId>
<artifactId>order-simple</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 编译期依赖方向靠包结构和 ArchUnit 约束,非 Maven 模块 -->
</dependencies>依赖方向:Interface → Application → Domain ← Infrastructure
- Interface 依赖 Application(注入 AppService)
- Application 依赖 Domain(调用 Repository 接口、领域实体)
- Domain 零依赖(纯 JDK 类型)
- Infrastructure 依赖 Domain(实现 Repository 接口)
由于是单模块,依赖方向依赖约定优于配置,配合 ArchUnit 测试强制约束。实际运行时 Spring DI 容器自动注入 Infrastructure 实现到 Domain 接口。
关键设计要点
| 要点 | 说明 |
|---|---|
| 分层物理隔离 | 单模块下靠包名隔离,ArchUnit 编译期校验 |
| Domain 充血模型 | 业务方法在实体内部,不依赖 Spring 注解 |
| Repository 接口 | 定义在 Domain 层,实现在 Infra 层 |
| Application 薄层 | 只做编排,不含 if/else 业务判断 |
| Interface 协议转换 | Controller 只负责 DTO 转换和参数校验 |
优点与局限
| 优点 | 局限 |
|---|---|
| 结构简单,学习曲线低 | 包名约束弱,依赖方向易被破坏 |
| 快速启动,适合 PoC | 所有层共享同一 classpath |
| 部署简单,一个 JAR | 无物理模块隔离,Domain 可能被污染 |
| 适合小团队快速迭代 | 规模增长后需拆分为多模块 |
演进路径
单模块四层 → 多模块四层(08)→ 微服务多模块(11/12)单体复杂项目 — 多聚合根 DDD 分层示例
适用场景
| 维度 | 描述 |
|---|---|
| 团队规模 | 5-15 人,有一定 DDD 经验 |
| 业务复杂度 | 3-5 个聚合根,多领域服务协作 |
| 部署方式 | 单体 Spring Boot JAR,单数据库或多 Schema |
| 技术栈 | Spring Boot + MyBatis/JPA + Maven |
| 典型业务 | 电商核心域(订单/商品/支付)、CRM 系统、进销存 |
关键特征:单应用承载多子域,Application 层做跨聚合编排,Domain 层按聚合内聚。
目录树
ecommerce/
├── pom.xml
└── src/
└── main/
└── java/
└── com/example/ecommerce/
├── EcommerceApplication.java
│
├── interface/ # 接口层(按聚合分子包)
│ ├── controller/
│ │ ├── order/
│ │ │ └── OrderController.java
│ │ ├── product/
│ │ │ └── ProductController.java
│ │ └── payment/
│ │ └── PaymentController.java
│ ├── dto/
│ │ ├── request/
│ │ └── response/
│ └── converter/
│ ├── OrderDtoConverter.java
│ ├── ProductDtoConverter.java
│ └── PaymentDtoConverter.java
│
├── application/ # 应用层(跨聚合编排)
│ ├── service/
│ │ ├── order/
│ │ │ └── OrderApplicationService.java
│ │ ├── product/
│ │ │ └── ProductApplicationService.java
│ │ ├── payment/
│ │ │ └── PaymentApplicationService.java
│ │ └── orchestration/ # 跨聚合编排服务
│ │ └── CheckoutOrchestrationService.java
│ ├── command/
│ │ ├── CreateOrderCommand.java
│ │ ├── DeductStockCommand.java
│ │ └── CreatePaymentCommand.java
│ ├── query/
│ │ ├── OrderDetailQuery.java
│ │ └── ProductListQuery.java
│ ├── assembler/
│ │ └── OrderAssembler.java # 跨聚合数据组装
│ └── event/
│ └── handler/
│ ├── OrderPaidHandler.java # 支付后扣库存
│ └── StockShortageHandler.java # 库存不足补偿
│
├── domain/ # 领域层 ★(按聚合分包)
│ ├── order/
│ │ ├── entity/
│ │ │ ├── Order.java # 订单聚合根
│ │ │ └── OrderItem.java
│ │ ├── valueobject/
│ │ │ ├── Money.java
│ │ │ ├── OrderStatus.java
│ │ │ ├── Address.java
│ │ │ └── OrderId.java
│ │ ├── service/
│ │ │ ├── OrderDomainService.java
│ │ │ └── OrderPricingService.java # 定价领域服务
│ │ ├── repository/
│ │ │ └── OrderRepository.java
│ │ └── event/
│ │ ├── OrderPlacedEvent.java
│ │ └── OrderCancelledEvent.java
│ │
│ ├── product/
│ │ ├── entity/
│ │ │ ├── Product.java # 商品聚合根
│ │ │ └── Category.java
│ │ ├── valueobject/
│ │ │ ├── Price.java
│ │ │ ├── Sku.java
│ │ │ └── ProductId.java
│ │ ├── service/
│ │ │ └── InventoryDomainService.java
│ │ ├── repository/
│ │ │ └── ProductRepository.java
│ │ └── event/
│ │ ├── StockDeductedEvent.java
│ │ └── StockReplenishedEvent.java
│ │
│ ├── payment/
│ │ ├── entity/
│ │ │ └── Payment.java # 支付聚合根
│ │ ├── valueobject/
│ │ │ ├── PaymentMethod.java
│ │ │ └── PaymentStatus.java
│ │ ├── service/
│ │ │ └── PaymentDomainService.java
│ │ ├── repository/
│ │ │ └── PaymentRepository.java
│ │ └── event/
│ │ ├── PaymentCompletedEvent.java
│ │ └── PaymentRefundedEvent.java
│ │
│ ├── factory/
│ │ └── OrderFactory.java # 复杂聚合创建
│ └── shared/
│ ├── BaseEntity.java
│ ├── BaseValueObject.java
│ ├── AggregateRoot.java
│ └── DomainEvent.java
│
└── infrastructure/ # 基础设施层
├── repository/
│ ├── order/
│ │ └── MyBatisOrderRepository.java
│ ├── product/
│ │ └── MyBatisProductRepository.java
│ └── payment/
│ └── MyBatisPaymentRepository.java
├── persistence/
│ ├── OrderPO.java
│ ├── OrderItemPO.java
│ ├── ProductPO.java
│ ├── ProductSkuPO.java
│ └── PaymentPO.java
├── messaging/ # 领域事件发布
│ ├── DomainEventPublisher.java
│ └── SpringEventPublisher.java
├── external/ # 外部服务适配
│ ├── AlipayClient.java
│ └── SmsClient.java
└── config/
├── RepositoryConfig.java
└── EventConfig.java包结构
com.example.ecommerce
├── interface
│ └── 按聚合分子包(order/product/payment controller)
├── application
│ ├── 按聚合分 service (order/product/payment)
│ └── orchestration 层负责跨聚合编排
├── domain
│ ├── order/ — 订单聚合
│ ├── product/ — 商品聚合
│ ├── payment/ — 支付聚合
│ └── shared/ — 共享基类
└── infrastructure
├── repository/ — 按聚合分实现
├── messaging/ — 事件总线
└── external/ — 外部服务适配器模块依赖
<!-- 单模块 pom.xml -->
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 持久化 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- 事件发布(Application 层用) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 架构验证 -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>跨聚合编排示例:
// Application 层:CheckoutOrchestrationService(跨聚合编排)
@Service
public class CheckoutOrchestrationService {
private final OrderDomainService orderDomainService;
private final InventoryDomainService inventoryDomainService;
private final PaymentDomainService paymentDomainService;
@Transactional
public CheckoutResult checkout(CheckoutCommand command) {
// 1. 锁定库存(Product 聚合)
inventoryDomainService.deduct(command.getProductId(), command.getQuantity());
// 2. 创建订单(Order 聚合)
Order order = orderDomainService.createOrder(command);
// 3. 发起支付(Payment 聚合)
Payment payment = paymentDomainService.initiate(order);
return new CheckoutResult(order, payment);
}
}关键设计要点
| 要点 | 说明 |
|---|---|
| 聚合间引用 | 通过 ID 引用,不直接持有对象引用 |
| 跨聚合编排 | Application 层 orchestration 包集中管理 |
| 领域服务 | Domain 层中的 service 处理跨实体逻辑 |
| 领域事件 | 聚合内事件,同进程发布 |
| 事务边界 | Application 层 @Transactional 控制 |
优点与局限
| 优点 | 局限 |
|---|---|
| 单应用部署,运维简单 | 业务耦合度高,修改需全量回归 |
| 多聚合协作方便 | 事务边界大,性能瓶颈明显 |
| 共享基础设施(DB/缓存) | DB 耦合,无法独立伸缩 |
| 适合中等规模业务 | 团队扩大后易产生合并冲突 |
演进路径
单体复杂 → 多模块拆分(08)→ 按子域拆分微服务(09/10)单体多模块 — DDD 分层示例
适用场景
| 维度 | 描述 |
|---|---|
| 团队规模 | 8-20 人,有模块化经验 |
| 业务复杂度 | 中高,2-5 个聚合根 |
| 部署方式 | 单体 Spring Boot JAR,多 Maven 模块编译 |
| 技术栈 | Spring Boot + MyBatis/JPA + Maven 多模块 |
| 迁移起点 | 从单模块四层(06/07)拆分 |
典型业务:需要物理模块隔离的中型项目,团队多人并行开发。
目录树
order-system/
├── pom.xml # 父 POM(dependencyManagement)
│
├── order-interface/ # 用户接口层模块
│ ├── pom.xml
│ └── src/main/java/com/example/order/interface/
│ ├── controller/
│ │ └── OrderController.java
│ ├── dto/
│ │ ├── request/
│ │ │ ├── CreateOrderRequest.java
│ │ │ └── UpdateOrderRequest.java
│ │ └── response/
│ │ └── OrderResponse.java
│ └── converter/
│ └── OrderDtoConverter.java
│
├── order-application/ # 应用层模块
│ ├── pom.xml
│ └── src/main/java/com/example/order/application/
│ ├── service/
│ │ └── OrderApplicationService.java
│ ├── command/
│ │ ├── CreateOrderCommand.java
│ │ └── UpdateOrderCommand.java
│ ├── query/
│ │ └── OrderDetailQuery.java
│ └── assembler/
│ └── OrderAssembler.java
│
├── order-domain/ # 领域层模块 ★
│ ├── pom.xml
│ └── src/main/java/com/example/order/domain/
│ ├── order/
│ │ ├── entity/
│ │ │ ├── Order.java
│ │ │ └── OrderItem.java
│ │ ├── valueobject/
│ │ │ ├── Money.java
│ │ │ ├── OrderStatus.java
│ │ │ └── Address.java
│ │ ├── service/
│ │ │ └── OrderDomainService.java
│ │ ├── repository/
│ │ │ └── OrderRepository.java # 仓储接口(只定义)
│ │ └── event/
│ │ └── OrderPlacedEvent.java
│ └── shared/
│ ├── AggregateRoot.java
│ └── DomainEvent.java
│
└── order-infrastructure/ # 基础设施层模块
├── pom.xml
└── src/main/java/com/example/order/infrastructure/
├── repository/
│ └── MyBatisOrderRepository.java # 仓储实现
├── persistence/
│ ├── OrderPO.java
│ └── OrderItemPO.java
├── converter/
│ └── OrderPersistenceConverter.java # PO ↔ DO
└── config/
└── RepositoryConfig.java包结构
com.example.order
├── order-interface/ com.example.order.interface
├── order-application/ com.example.order.application
├── order-domain/ com.example.order.domain
└── order-infrastructure/ com.example.order.infrastructure每个模块独立基础包名:com.example.order.{layer}
模块依赖
父 POM(order-system/pom.xml)
<groupId>com.example</groupId>
<artifactId>order-system</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>order-interface</module>
<module>order-application</module>
<module>order-domain</module>
<module>order-infrastructure</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>order-interface/pom.xml
<artifactId>order-interface</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>order-application/pom.xml
<artifactId>order-application</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>order-domain/pom.xml
<artifactId>order-domain</artifactId>
<dependencies>
<!-- 零框架依赖,仅 JDK -->
<!-- 可引入 javax.annotation 等标注库(非 Spring) -->
</dependencies>order-infrastructure/pom.xml
<artifactId>order-infrastructure</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>依赖关系图
order-interface
↓
order-application
↓
order-domain ← order-infrastructure关键设计要点
| 要点 | 说明 |
|---|---|
| 物理隔离 | Maven 模块强制依赖方向,编译期阻止反向依赖 |
| Domain 纯净 | pom.xml 不含 Spring/JPA,可独立测试 |
| 运行组合 | Application 模块启动时同时依赖 interface + infra,运行时注入实现 |
| Infrastructure 独立 | 替换持久层只需改 infra 模块,不影响其他层 |
| 并行开发 | 不同模块可由不同开发者独立编译和测试 |
应用启动模块
通常主启动类放在 order-interface 或单独的 bootstrap 模块中:
order-bootstrap/
├── pom.xml
└── src/main/java/
└── com/example/order/
└── OrderApplication.javaorder-bootstrap/pom.xml 同时依赖所有四个模块:
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>order-interface</artifactId></dependency>
<dependency><groupId>com.example</groupId><artifactId>order-application</artifactId></dependency>
<dependency><groupId>com.example</groupId><artifactId>order-domain</artifactId></dependency>
<dependency><groupId>com.example</groupId><artifactId>order-infrastructure</artifactId></dependency>
</dependencies>优点与局限
| 优点 | 局限 |
|---|---|
| Maven 强制依赖方向 | 模块数量增多,构建时间增加 |
| Domain 物理纯净 | 仍为单体部署,可伸缩性有限 |
| 可独立编译测试 | 多次 pom.xml 维护成本 |
| 清晰的团队分工边界 | 适合中小型项目,超大项目仍需微服务 |
演进路径
单体多模块 → 微服务简单(09)→ 微服务多模块(11)微服务简单 — DDD 分层示例
适用场景
| 维度 | 描述 |
|---|---|
| 团队规模 | 10-30 人,分为 3-5 个子团队 |
| 业务复杂度 | 中高,各子域独立性强 |
| 部署方式 | 独立 Spring Boot JAR,Docker/K8s 部署 |
| 技术栈 | Spring Boot + Spring Cloud / REST + MyBatis/JPA |
| 通信方式 | REST API(同步)+ 轻量 MQ(异步通知) |
典型业务:电商系统已拆分为订单服务、商品服务、支付服务;每个服务内部是独立的 DDD 分层单体。
目录树
ecommerce-platform/
├── pom.xml # 根 POM
│
├── order-service/ # 订单服务
│ ├── pom.xml
│ └── src/main/java/com/example/order/
│ ├── OrderServiceApplication.java # 独立启动类
│ ├── interface/
│ │ ├── controller/
│ │ │ └── OrderController.java # REST API
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ └── response/
│ │ └── converter/
│ │ └── OrderDtoConverter.java
│ ├── application/
│ │ ├── service/
│ │ │ └── OrderApplicationService.java
│ │ ├── command/
│ │ │ ├── CreateOrderCommand.java
│ │ │ └── CancelOrderCommand.java
│ │ └── query/
│ │ └── OrderDetailQuery.java
│ ├── domain/
│ │ └── order/
│ │ ├── entity/
│ │ │ ├── Order.java
│ │ │ └── OrderItem.java
│ │ ├── valueobject/
│ │ │ ├── Money.java
│ │ │ ├── OrderStatus.java
│ │ │ └── Address.java
│ │ ├── service/
│ │ │ └── OrderDomainService.java
│ │ ├── repository/
│ │ │ └── OrderRepository.java
│ │ └── event/
│ │ └── OrderPlacedEvent.java
│ └── infrastructure/
│ ├── repository/
│ │ └── MyBatisOrderRepository.java
│ ├── persistence/
│ │ ├── OrderPO.java
│ │ └── OrderItemPO.java
│ ├── external/
│ │ ├── ProductServiceClient.java # 调用商品服务 REST API
│ │ ├── PaymentServiceClient.java # 调用支付服务 REST API
│ │ └── UserServiceClient.java # 调用用户服务 REST API
│ └── config/
│ └── RepositoryConfig.java
│
├── product-service/ # 商品服务
│ ├── pom.xml
│ └── src/main/java/com/example/product/
│ ├── ProductServiceApplication.java
│ ├── interface/
│ │ ├── controller/
│ │ │ └── ProductController.java
│ │ ├── dto/
│ │ └── converter/
│ ├── application/
│ │ ├── service/
│ │ │ └── ProductApplicationService.java
│ │ ├── command/
│ │ └── query/
│ ├── domain/
│ │ └── product/
│ │ ├── entity/
│ │ │ ├── Product.java
│ │ │ └── Category.java
│ │ ├── valueobject/
│ │ │ ├── Price.java
│ │ │ ├── Sku.java
│ │ │ └── Stock.java
│ │ ├── service/
│ │ │ └── InventoryDomainService.java
│ │ ├── repository/
│ │ │ └── ProductRepository.java
│ │ └── event/
│ │ └── StockDeductedEvent.java
│ └── infrastructure/
│ ├── repository/
│ │ └── MyBatisProductRepository.java
│ ├── persistence/
│ │ ├── ProductPO.java
│ │ └── CategoryPO.java
│ └── config/
│ └── RepositoryConfig.java
│
├── payment-service/ # 支付服务
│ ├── pom.xml
│ └── src/main/java/com/example/payment/
│ ├── PaymentServiceApplication.java
│ ├── interface/
│ │ ├── controller/
│ │ │ └── PaymentController.java
│ │ ├── dto/
│ │ └── converter/
│ ├── application/
│ │ ├── service/
│ │ │ └── PaymentApplicationService.java
│ │ ├── command/
│ │ └── query/
│ ├── domain/
│ │ └── payment/
│ │ ├── entity/
│ │ │ └── Payment.java
│ │ ├── valueobject/
│ │ │ ├── PaymentMethod.java
│ │ │ ├── PaymentStatus.java
│ │ │ └── Amount.java
│ │ ├── service/
│ │ │ └── PaymentDomainService.java
│ │ ├── repository/
│ │ │ └── PaymentRepository.java
│ │ └── event/
│ │ └── PaymentCompletedEvent.java
│ └── infrastructure/
│ ├── repository/
│ │ └── MyBatisPaymentRepository.java
│ ├── persistence/
│ │ └── PaymentPO.java
│ ├── external/
│ │ └── AlipayClient.java
│ └── config/
│ └── RepositoryConfig.java
│
└── api-gateway/ # API 网关(可选)
├── pom.xml
└── src/main/java/com/example/gateway/
└── GatewayApplication.java包结构
com.example.order — 订单微服务
com.example.product — 商品微服务
com.example.payment — 支付微服务
com.example.gateway — API 网关每个微服务内部是独立的 DDD 四层结构,与单体简单项目(06)完全一致。
模块依赖
根 POM
<groupId>com.example</groupId>
<artifactId>ecommerce-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>order-service</module>
<module>product-service</module>
<module>payment-service</module>
<module>api-gateway</module>
</modules>服务间依赖
order-service ──REST──▶ product-service
order-service ──REST──▶ payment-service
payment-service ──REST──▶ order-service (查询订单)服务间通过 Feign/OpenFeign 或 RestTemplate 通信,在 Infrastructure 层的 external/ 包中实现客户端:
// order-service/infrastructure/external/ProductServiceClient.java
@FeignClient(name = "product-service", url = "${product.service.url}")
public interface ProductServiceClient {
@GetMapping("/api/products/{id}")
ProductDTO getProduct(@PathVariable("id") Long id);
@PostMapping("/api/products/{id}/deduct-stock")
void deductStock(@PathVariable("id") Long id, @RequestBody DeductStockDTO request);
}服务间集成策略
| 场景 | 方式 | 说明 |
|---|---|---|
| 创建订单 → 校验商品 | 同步 REST | 需要即时返回结果 |
| 创建订单 → 查询用户信息 | 同步 REST | 查询操作 |
| 支付完成 → 更新订单状态 | 异步 MQ | 最终一致性 |
| 库存扣减 → 通知订单 | 异步 MQ | 解耦通知 |
| 商品下架 → 取消关联订单 | 异步 MQ | 批量处理 |
关键设计要点
| 要点 | 说明 |
|---|---|
| 服务自治 | 每个服务有独立数据库,不共享表 |
| 独立部署 | 各服务独立构建、独立部署、独立扩缩容 |
| 合同优先 | 服务间 API 版本化,向前兼容 |
| 防腐层 | 外部服务调用封装在 infrastructure/external |
| 最终一致性 | 跨服务业务通过 MQ 异步对齐 |
优点与局限
| 优点 | 局限 |
|---|---|
| 独立部署和扩缩容 | 分布式系统复杂性(网络/超时/重试) |
| 团队自治,并行开发 | 服务间耦合仍存在 |
| 故障隔离,单服务挂不影响全局 | 调试和排错困难 |
| 技术栈可异构 | 数据一致性需要分布式事务支持 |
演进路径
微服务简单 → 微服务复杂(10,事件驱动)→ 微服务多模块(11)微服务复杂 — 事件驱动 DDD 分层示例
适用场景
| 维度 | 描述 |
|---|---|
| 团队规模 | 15-40 人,多个自治团队 |
| 业务复杂度 | 高,跨服务业务流程(Saga) |
| 部署方式 | Docker/K8s,独立部署多服务 |
| 技术栈 | Spring Boot + Spring Cloud + Kafka/RabbitMQ + MyBatis/JPA |
| 通信方式 | REST(查询/命令) + 事件驱动(领域事件异步广播) |
典型业务:电商全流程(下单→扣库存→支付→发货→通知),金融交易系统(风控→授信→放款→对账)。
目录树
ecommerce-platform/
├── pom.xml
│
├── order-service/ # 订单服务
│ ├── pom.xml
│ └── src/main/java/com/example/order/
│ ├── OrderServiceApplication.java
│ ├── interface/
│ │ ├── controller/
│ │ │ └── OrderController.java
│ │ ├── dto/
│ │ └── converter/
│ ├── application/
│ │ ├── service/
│ │ │ └── OrderApplicationService.java
│ │ ├── command/
│ │ │ ├── CreateOrderCommand.java
│ │ │ └── ConfirmOrderCommand.java
│ │ ├── query/
│ │ └── event/ # 事件处理器(监听其他服务事件)
│ │ └── handler/
│ │ ├── PaymentCompletedHandler.java # 支付成功 → 确认订单
│ │ ├── ShipmentCreatedHandler.java # 发货 → 更新订单状态
│ │ └── InventoryDeductedHandler.java # 库存扣减 → 标记可支付
│ ├── domain/
│ │ └── order/
│ │ ├── entity/
│ │ │ ├── Order.java
│ │ │ └── OrderItem.java
│ │ ├── valueobject/
│ │ │ ├── Money.java
│ │ │ ├── OrderStatus.java
│ │ │ └── Address.java
│ │ ├── service/
│ │ │ └── OrderDomainService.java
│ │ ├── repository/
│ │ │ └── OrderRepository.java
│ │ └── event/
│ │ ├── OrderPlacedEvent.java # 领域事件(发给 MQ)
│ │ ├── OrderConfirmedEvent.java
│ │ └── OrderCancelledEvent.java
│ └── infrastructure/
│ ├── repository/
│ │ └── MyBatisOrderRepository.java
│ ├── persistence/
│ │ ├── OrderPO.java
│ │ └── OrderEventLogPO.java # 事件溯源/事件日志
│ ├── messaging/ # 事件总线
│ │ ├── DomainEventPublisher.java # 发布领域事件
│ │ ├── KafkaEventPublisher.java
│ │ ├── DomainEventSubscriber.java # 订阅外部事件
│ │ └── KafkaEventSubscriber.java
│ ├── external/
│ │ ├── ProductServiceClient.java
│ │ └── PaymentServiceClient.java
│ └── config/
│ ├── KafkaConfig.java
│ └── RepositoryConfig.java
│
├── product-service/
│ ├── pom.xml
│ └── src/main/java/com/example/product/
│ ├── ...
│ ├── application/
│ │ └── event/handler/
│ │ ├── OrderPlacedHandler.java # 下单 → 锁定库存
│ │ └── OrderCancelledHandler.java # 取消订单 → 释放库存
│ ├── domain/product/
│ │ └── event/
│ │ ├── InventoryDeductedEvent.java
│ │ └── InventoryReleasedEvent.java
│ └── infrastructure/
│ └── messaging/
│ ├── KafkaEventPublisher.java
│ └── KafkaEventSubscriber.java
│
├── payment-service/
│ ├── pom.xml
│ └── src/main/java/com/example/payment/
│ ├── ...
│ ├── application/
│ │ └── event/handler/
│ │ ├── OrderConfirmedHandler.java # 订单确认 → 发起支付
│ │ └── InventoryDeductedHandler.java # 库存扣减 → 创建支付单
│ ├── domain/payment/
│ │ └── event/
│ │ ├── PaymentInitiatedEvent.java
│ │ └── PaymentCompletedEvent.java
│ └── infrastructure/
│ └── messaging/
│ ├── KafkaEventPublisher.java
│ └── KafkaEventSubscriber.java
│
├── notification-service/ # 通知服务
│ ├── pom.xml
│ └── src/main/java/com/example/notification/
│ ├── interface/controller/
│ ├── application/
│ │ └── event/handler/
│ │ ├── OrderPlacedHandler.java
│ │ ├── PaymentCompletedHandler.java
│ │ └── ShipmentCreatedHandler.java
│ ├── domain/notification/
│ │ ├── entity/
│ │ │ └── Notification.java
│ │ └── repository/
│ │ └── NotificationRepository.java
│ └── infrastructure/
│ ├── repository/
│ ├── messaging/
│ │ └── KafkaEventSubscriber.java
│ └── external/
│ ├── SmsClient.java
│ └── EmailClient.java
│
└── shared-events/ # 共享事件定义
├── pom.xml
└── src/main/java/com/example/shared/
├── OrderPlacedEvent.java
├── OrderConfirmedEvent.java
├── OrderCancelledEvent.java
├── InventoryDeductedEvent.java
├── InventoryReleasedEvent.java
├── PaymentCompletedEvent.java
└── ShipmentCreatedEvent.java包结构
com.example.order — 订单微服务
com.example.product — 商品微服务
com.example.payment — 支付微服务
com.example.notification — 通知微服务
com.example.shared — 共享事件类型事件流设计
下单流程事件链
OrderPlaced → Product 锁定库存
InventoryDeducted → Payment 创建支付单
PaymentCompleted → Order 确认订单
OrderConfirmed → Notification 发送通知order-service ──OrderPlaced──▶ product-service
│
product-service ──InventoryDeducted──▶ payment-service
│
payment-service ──PaymentCompleted──▶ order-service
│
order-service ──OrderConfirmed──▶ notification-serviceSaga 补偿事件
OrderCancelled → Product 释放库存 + Payment 退款模块依赖
事件共享模块(shared-events)
<artifactId>shared-events</artifactId>
<dependencies>
<!-- 仅包含 POJO 事件定义,零 Spring 依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>服务依赖 shared-events
<!-- order-service/pom.xml -->
<dependency>
<groupId>com.example</groupId>
<artifactId>shared-events</artifactId>
<version>${project.version}</version>
</dependency>事件发布示例
// order-service/domain/order/event/OrderPlacedEvent.java
public class OrderPlacedEvent extends DomainEvent {
private final OrderId orderId;
private final Money totalAmount;
private final List<OrderItemSnapshot> items;
// 事件只含必要数据,不含完整聚合
}
// order-service/infrastructure/messaging/KafkaEventPublisher.java
@Component
public class KafkaEventPublisher implements DomainEventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
@Override
public void publish(DomainEvent event) {
String topic = resolveTopic(event);
String payload = serializeToJson(event);
kafkaTemplate.send(topic, payload);
}
}事件订阅示例
// product-service/application/event/handler/OrderPlacedHandler.java
@Component
public class OrderPlacedHandler {
@KafkaListener(topics = "order.placed")
public void handle(OrderPlacedEvent event) {
// 调用领域服务锁定库存
inventoryDomainService.reserveStock(
event.getItems().stream()
.map(i -> new StockReserveCommand(i.getProductId(), i.getQuantity()))
.toList()
);
// 发布 InventoryDeducted 事件到下一环节
domainEventPublisher.publish(new InventoryDeductedEvent(event.getOrderId()));
}
}关键设计要点
| 要点 | 说明 |
|---|---|
| 领域事件 | Domain 层定义事件(零框架),Infra 层负责发布 |
| 事件总线 | Kafka/RabbitMQ 实现异步解耦 |
| 共享事件 | 独立 shared-events 模块,避免服务间代码重复 |
| Saga 补偿 | 事件驱动 Saga,失败时发布补偿事件回滚 |
| 幂等消费 | 事件处理器检查业务状态,防止重复执行 |
| 事件日志 | Outbox 模式确保事件可靠投递 |
优点与局限
| 优点 | 局限 |
|---|---|
| 服务高度解耦 | 事件流追踪困难 |
| 最终一致性保证 | 需要补偿机制处理失败 |
| 可独立演进 | 事件 Schema 需要向前兼容 |
| 弹性伸缩 | 消息中间件运维成本高 |
演进路径
微服务复杂 → 微服务多模块(11)→ 微服务复杂多模块(12)Domain Layer — 领域层
领域层是 DDD 分层架构的核心,包含所有业务逻辑和规则,零外部依赖。
职责
- 表达业务概念、业务状态和业务规则
- 实现核心业务逻辑(充血模型)
- 定义仓储接口(依赖倒置的前提)
- 定义领域事件
包含的子模块
domain/{aggregate}/
├── entity/ # 实体 + 聚合根(充血模型)
│ ├── Order.java # 聚合根
│ └── OrderItem.java # 子实体
├── valueobject/ # 值对象(不可变)
│ ├── Money.java
│ ├── Address.java
│ └── OrderStatus.java
├── event/ # 领域事件
│ └── OrderPlacedEvent.java
├── service/ # 领域服务(多实体协作)
│ └── PricingService.java
├── repository/ # 仓储接口(只有定义)
│ └── OrderRepository.java
├── specification/ # 规约模式
├── factory/ # 工厂模式
├── policy/ # 业务策略
└── exception/ # 领域异常
├── DomainException.java
└── BusinessRuleException.java关键规则
1. 零框架依赖 — 不能 import Spring/JPA/MyBatis 任何注解 2. 纯业务逻辑 — 只能使用 JDK 原生类型和领域类型 3. 充血模型 — 实体必须有业务方法,不能只有 getter/setter 4. 值对象不可变 — 所有字段 final,构造器初始化 5. 聚合间 ID 引用 — 不能直接引用其他聚合的对象
聚合根代码示例
// 纯 POJO,无 @Entity、@Service 等注解
public class Order extends AggregateRoot<OrderId> {
private OrderStatus status;
private Money totalAmount;
private List<OrderItem> items;
public void pay() {
if (!status.canPay()) {
throw new OrderException("当前状态不可支付");
}
this.status = OrderStatus.PAID;
addDomainEvent(new OrderPaidEvent(this.id));
}
public void addItem(ProductId productId, Money price, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
this.items.add(new OrderItem(productId, price, quantity));
recalculateTotal();
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}值对象代码示例
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// 只有 getter,没有 setter
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}仓储接口代码示例
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(OrderId id);
}领域服务代码示例
// 当业务逻辑涉及多个实体时使用领域服务
public class PricingService {
public Money calculateTotal(List<OrderItem> items) {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
public Money applyDiscount(Money total, DiscountPolicy policy) {
return policy.apply(total);
}
}参考
- Eric Evans 《领域驱动设计》第 4-6 章
- Vaughn Vernon 《实现领域驱动设计》第 7-10 章
- ddd4j-layered-structure.md
- directory-structure.md
Application Layer — 应用层
应用层是"薄层",负责用例编排、事务管理和跨聚合协调,不包含业务逻辑。
职责
- 编排领域服务完成业务用例
- 管理事务边界(
@Transactional) - 转换 Command/Query 到领域对象
- 协调跨聚合的操作
- 安全认证和权限校验
- 发送/订阅领域事件
包含的子模块
application/
├── service/ # 应用服务(纯编排)
│ ├── OrderApplicationService.java
│ └── CustomerApplicationService.java
├── command/ # 命令对象(CQRS 写操作)
│ ├── CreateOrderCommand.java
│ └── PayOrderCommand.java
├── query/ # 查询对象(CQRS 读操作)
│ ├── GetOrderQuery.java
│ └── SearchOrdersQuery.java
├── assembler/ # DO ↔ DTO 转换
│ ├── OrderAssembler.java
│ └── CustomerAssembler.java
├── event/ # 应用级事件处理
│ ├── OrderPlacedEventHandler.java
│ └── OrderPaidEventHandler.java
├── dto/ # 数据传输对象
│ ├── OrderDTO.java
│ └── OrderItemDTO.java
├── validator/ # 应用验证器
│ └── OrderValidator.java
└── exception/ # 应用层异常
├── ApplicationException.java
└── CommandValidationException.java关键规则
1. 无业务逻辑 — Application 层不应该包含 if/else 业务判断 2. 只编排 — 调用领域服务完成业务,不自己实现业务 3. 事务边界 — @Transactional 在 Application Service 方法上 4. 依赖 Domain — Application 可以依赖 Domain 层
应用服务代码示例
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final PricingService pricingService;
private final DomainEventPublisher eventPublisher;
public OrderDTO createOrder(CreateOrderCommand command) {
// 1. 将 Command 转换为领域对象
List<OrderItem> items = command.getItems().stream()
.map(i -> new OrderItem(i.getProductId(), i.getPrice(), i.getQuantity()))
.toList();
// 2. 创建聚合根(领域逻辑在工厂或构造器中)
Order order = Order.create(command.getCustomerId(), items, pricingService);
// 3. 通过仓储持久化
orderRepository.save(order);
// 4. 发布领域事件
eventPublisher.publish(new OrderCreatedEvent(order.getId()));
// 5. 返回 DTO
return OrderAssembler.toDTO(order);
}
}命令对象示例
public class CreateOrderCommand {
@NotBlank(message = "客户ID不能为空")
private String customerId;
@NotEmpty(message = "订单项不能为空")
private List<OrderItemCommand> items;
// getters
}
public class OrderItemCommand {
private String productId;
private BigDecimal price;
private int quantity;
// getters
}编排原则
ApplicationService 的典型方法结构:
1. 参数校验(Command/Query 来自 Interface 层)
2. Command → DO 转换(Assembler/手动)
3. 调用领域服务(Domain Service)
4. 事务提交(@Transactional 自动)
5. 发布领域事件(EventPublisher)
6. DO → DTO 转换(Assembler)
7. 返回结果(DTO)好的 Application Service vs 坏的
| ✅ 好的(纯编排) | ❌ 坏的(含业务逻辑) |
|---|---|
调用 order.pay() | 写 if (status == DRAFT) 判断 |
调用 pricingService.calculate() | 写 price * quantity 计算 |
调用 orderRepository.save() | 直接操作 JPA EntityManager |
| 方法 < 20 行 | 方法 > 50 行 |
| 没有 if/else 业务分支 | 大量 if/else 业务规则 |