SpringBoot DDD 实战入门文档

1、简介

  • 目标:基于 SpringBoot 演示 DDD 四层架构落地;不是框架,是代码组织方式
  • 示例业务:简易订单限界上下文,完成下单。
  • 技术栈:SpringBoot + MyBatis‑Plus(基础设施层实现仓储)。
  • 重要提醒:简单 CRUD 业务没必要硬套 DDD;DDD 适合复杂业务。

2、四层架构与 SpringBoot 映射

DDD 分层 职责 SpringBoot 对应位置 依赖规则
接口层 (Presentation) http 接口,接收入参,返回 DTO,基础参数校验 controller 只调用应用层
应用层 (Application) 流程编排、事务、发布事件,无业务规则 application 调用领域层、仓储接口;不依赖数据库
领域层 (Domain) 核心:实体、值对象、聚合根、领域服务、领域事件、仓储接口 domain 不依赖 Spring、不依赖数据库、不依赖 MyBatis
基础设施层 (Infrastructure) 仓储实现、MyBatis、MQ、第三方调用、防腐层 infrastructure 实现领域层定义的 Repository 接口;依赖数据库组件

关键约束:

  1. 领域层不能引用任何数据库、mybatis 相关类,只定义仓储接口。
  2. 仓储接口定义在 domain,实现在 infrastructure。
  3. 业务规则全部写在领域对象 / 领域服务,不能写在 Controller、Application。

3. SpringBoot 包结构(完整可直接复制)

复制代码
com.example.dddorder
├── controller                  # 接口层 Presentation
│   └── OrderController.java
├── application                 # 应用层 Application
│   ├── dto                     # 入参出参DTO
│   │   ├── CreateOrderCommand.java
│   │   └── OrderDTO.java
│   └── OrderApplicationService.java
├── domain                      # 领域层 Domain(核心!无技术依赖)
│   ├── event                   # 领域事件
│   │   └── OrderCreatedDomainEvent.java
│   ├── model                   # 领域模型:聚合、实体、值对象
│   │   ├── aggregate
│   │   │   └── Order.java         # 聚合根 Order
│   │   ├── entity
│   │   │   └── OrderItem.java     # 子实体(订单行,属于Order聚合内部)
│   │   └── vo
│   │       ├── AddressVO.java     # 值对象:收货地址
│   │       ├── MoneyVO.java       # 值对象:金额
│   │       └── OrderStatusVO.java # 值对象:订单状态
│   ├── repository              # 仓储【接口】,只定义,不实现
│   │   └── OrderRepository.java
│   └── service                 # 领域服务,承载业务规则
│       └── OrderDomainService.java
└── infrastructure              # 基础设施层 Infrastructure
    ├── config                  # spring配置
    ├── persistence             # 持久化实现
    │   ├── do                  # 数据库DO对象,和表一一对应
    │   │   ├── OrderDO.java
    │   │   └── OrderItemDO.java
    │   ├── mapper              # Mybatis Mapper
    │   │   ├── OrderMapper.java
    │   │   └── OrderItemMapper.java
    │   └── impl                # 仓储接口实现类
    │       └── OrderRepositoryImpl.java
    └── event                   # 领域事件发送实现(MQ)
        └── DomainEventPublisherImpl.java

重要区分三种对象:

  • Domain Model(domain 下):业务模型,DDD 模型,不感知数据库。
  • DO(infrastructure/persistence/do):数据库表映射对象。
  • DTO(application/dto):对外接口出入参。 ❗禁止:领域实体直接作为返回 DTO,禁止直接返回 DO 给前端。

4、分层代码示例

4.1 领域层(Domain)

4.1.1 值对象 MoneyVO.java(不可变)

值对象:全部 final,无 id,通过属性判等。

复制代码
package com.example.dddorder.domain.model.vo;

import java.math.BigDecimal;

// 值对象:金额,不可变
public class MoneyVO {
    private final BigDecimal amount;
    private final String currency;

    public MoneyVO(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    // 领域行为:金额相加
    public MoneyVO add(MoneyVO other) {
        return new MoneyVO(this.amount.add(other.amount), this.currency);
    }

    // getter,不要setter!值对象禁止修改
    public BigDecimal getAmount() { return amount; }
    public String getCurrency() { return currency; }
}

4.1.2 领域事件 OrderCreatedDomainEvent.java

复制代码
package com.example.dddorder.domain.event;

import java.time.LocalDateTime;

// 领域事件:订单已创建,代表已经发生的业务事实
public class OrderCreatedDomainEvent {
    private final Long orderId;
    private final Long userId;
    private final LocalDateTime createTime;

    public OrderCreatedDomainEvent(Long orderId, Long userId, LocalDateTime createTime) {
        this.orderId = orderId;
        this.userId = userId;
        this.createTime = createTime;
    }

    // getter
    public Long getOrderId() { return orderId; }
    public Long getUserId() { return userId; }
    public LocalDateTime getCreateTime() { return createTime; }
}

4.1.3 聚合根 Order.java

Order 是聚合根;OrderItem 是内部子实体,外部不能直接访问 OrderItem。

复制代码
package com.example.dddorder.domain.model.aggregate;

import com.example.dddorder.domain.model.entity.OrderItem;
import com.example.dddorder.domain.model.vo.AddressVO;
import com.example.dddorder.domain.model.vo.MoneyVO;
import com.example.dddorder.domain.model.vo.OrderStatusVO;
import com.example.dddorder.domain.event.OrderCreatedDomainEvent;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * 聚合根:Order
 * 聚合边界:Order + OrderItem
 * 外部只能操作Order,不能直接操作OrderItem
 */
public class Order {
    // 唯一标识:实体ID
    private Long id;
    private Long userId;
    private OrderStatusVO status;
    private AddressVO address;
    private MoneyVO totalAmount;
    private LocalDateTime createTime;

    // 聚合内部子实体,外部不能直接访问
    private List<OrderItem> items = new ArrayList<>();

    // 领域事件:本聚合产生的事件,暂存此处,应用层负责发布
    private List<Object> domainEvents = new ArrayList<>();

    /**
     * 领域行为:创建订单(业务规则写在这里,不是写在service)
     */
    public static Order create(Long userId, AddressVO address, List<OrderItem> items){
        Order order = new Order();
        order.userId = userId;
        order.address = address;
        order.status = OrderStatusVO.CREATED;
        order.items.addAll(items);
        order.createTime = LocalDateTime.now();

        // 计算总金额(领域内业务逻辑)
        MoneyVO total = new MoneyVO(BigDecimal.ZERO,"CNY");
        for (OrderItem item : items) {
            total = total.add(item.getSubTotal());
        }
        order.totalAmount = total;

        // 记录领域事件
        order.domainEvents.add(new OrderCreatedDomainEvent(order.id, userId, order.createTime));
        return order;
    }

    // 取出并清空领域事件
    public List<Object> popDomainEvents(){
        List<Object> events = new ArrayList<>(this.domainEvents);
        this.domainEvents.clear();
        return events;
    }

    // getter,内部业务方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Long getUserId() { return userId; }
    public OrderStatusVO getStatus() { return status; }
    public MoneyVO getTotalAmount() { return totalAmount; }
    public List<OrderItem> getItems() { return items; }
}

4.1.4 仓储接口 OrderRepository.java(domain 层,只定义)

domain 层只定义接口,不引入 mybatis。

复制代码
package com.example.dddorder.domain.repository;

import com.example.dddorder.domain.model.aggregate.Order;

/**
 * 仓储接口:只操作聚合根Order
 */
public interface OrderRepository {
    // 保存聚合根,内部会保存Order+OrderItem
    Order save(Order order);
    Order findById(Long orderId);
}

4.1.5 领域服务 OrderDomainService.java

当业务逻辑不属于单个聚合,多个对象协作时使用领域服务;无状态。

复制代码
package com.example.dddorder.domain.service;

import com.example.dddorder.domain.model.aggregate.Order;
import com.example.dddorder.domain.model.entity.OrderItem;
import com.example.dddorder.domain.model.vo.AddressVO;

import java.util.List;

/**
 * 领域服务:存放不属于Order聚合本身的业务规则
 * 本示例模拟下单前校验商品合法性
 */
public class OrderDomainService {

    // 领域业务规则校验
    public void validateOrderCreate(Long userId, List<OrderItem> items, AddressVO address){
        if(items == null || items.isEmpty()){
            throw new IllegalArgumentException("订单项不能为空");
        }
        if(address == null){
            throw new IllegalArgumentException("收货地址不能为空");
        }
        // 可以写更多业务规则:价格校验、商品限购等
    }

    public Order createOrder(Long userId, List<OrderItem> items, AddressVO address){
        validateOrderCreate(userId, items, address);
        return Order.create(userId, address, items);
    }
}

4.2 应用层 Application

职责:流程编排、事务控制、调用领域、发布事件;不写业务规则

复制代码
package com.example.dddorder.application;

import com.example.dddorder.application.dto.CreateOrderCommand;
import com.example.dddorder.domain.event.OrderCreatedDomainEvent;
import com.example.dddorder.domain.model.aggregate.Order;
import com.example.dddorder.domain.model.entity.OrderItem;
import com.example.dddorder.domain.model.vo.AddressVO;
import com.example.dddorder.domain.repository.OrderRepository;
import com.example.dddorder.domain.service.OrderDomainService;
import com.example.dddorder.infrastructure.event.DomainEventPublisherImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class OrderApplicationService {

    private final OrderDomainService orderDomainService;
    private final OrderRepository orderRepository;
    private final DomainEventPublisherImpl eventPublisher;

    public OrderApplicationService(OrderDomainService orderDomainService,
                                   OrderRepository orderRepository,
                                   DomainEventPublisherImpl eventPublisher) {
        this.orderDomainService = orderDomainService;
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    /**
     * 应用服务方法:只编排流程,没有业务规则
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(CreateOrderCommand command){
        // 1.组装参数为领域对象
        AddressVO address = new AddressVO(command.getReceiver(), command.getPhone(), command.getAddressDetail());
        List<OrderItem> itemList = command.getItems();

        // 2.调用领域服务得到聚合根
        Order order = orderDomainService.createOrder(command.getUserId(), itemList, address);

        // 3.仓储保存聚合根
        Order savedOrder = orderRepository.save(order);

        // 4.取出领域事件,发布(如发送MQ,通知库存、支付服务)
        List<Object> events = savedOrder.popDomainEvents();
        for (Object event : events) {
            if(event instanceof OrderCreatedDomainEvent){
                eventPublisher.publish((OrderCreatedDomainEvent) event);
            }
        }

        return savedOrder.getId();
    }
}

4.3 接口层 Controller

复制代码
package com.example.dddorder.controller;

import com.example.dddorder.application.OrderApplicationService;
import com.example.dddorder.application.dto.CreateOrderCommand;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    private final OrderApplicationService orderApplicationService;

    public OrderController(OrderApplicationService orderApplicationService) {
        this.orderApplicationService = orderApplicationService;
    }

    @PostMapping("/create")
    public Long create(@RequestBody CreateOrderCommand command){
        return orderApplicationService.createOrder(command);
    }
}

4.4 基础设施层:仓储实现 OrderRepositoryImpl

  • 实现 domain 定义的 Repository 接口;做【领域模型 ↔ DO】转换;调用 Mapper。

  • 领域模型和数据库 DO 必须互相转换,不能直接用领域对象操作数据库。

    package com.example.dddorder.infrastructure.persistence.impl;

    import com.example.dddorder.domain.model.aggregate.Order;
    import com.example.dddorder.domain.model.entity.OrderItem;
    import com.example.dddorder.domain.repository.OrderRepository;
    import com.example.dddorder.infrastructure.persistence.do.OrderDO;
    import com.example.dddorder.infrastructure.persistence.do.OrderItemDO;
    import com.example.dddorder.infrastructure.persistence.mapper.OrderItemMapper;
    import com.example.dddorder.infrastructure.persistence.mapper.OrderMapper;
    import org.springframework.stereotype.Repository;

    import java.util.List;
    import java.util.stream.Collectors;

    @Repository
    public class OrderRepositoryImpl implements OrderRepository {

    复制代码
      private final OrderMapper orderMapper;
      private final OrderItemMapper orderItemMapper;
    
      public OrderRepositoryImpl(OrderMapper orderMapper, OrderItemMapper orderItemMapper) {
          this.orderMapper = orderMapper;
          this.orderItemMapper = orderItemMapper;
      }
    
      @Override
      public Order save(Order order) {
          // 1.领域模型转DO
          OrderDO orderDO = toOrderDO(order);
          if(order.getId() == null){
              orderMapper.insert(orderDO);
              order.setId(orderDO.getId());
          }else {
              orderMapper.updateById(orderDO);
          }
    
          // 保存子实体OrderItem
          List<OrderItemDO> itemDos = order.getItems().stream()
                  .map(this::toOrderItemDO)
                  .peek(i -> i.setOrderId(order.getId()))
                  .collect(Collectors.toList());
          // 先删旧,再批量插入
          orderItemMapper.deleteByOrderId(order.getId());
          for (OrderItemDO itemDO : itemDos) {
              orderItemMapper.insert(itemDO);
          }
    
          return order;
      }
    
      @Override
      public Order findById(Long orderId) {
          OrderDO orderDO = orderMapper.selectById(orderId);
          List<OrderItemDO> itemDOS = orderItemMapper.selectByOrderId(orderId);
          // DO转领域聚合Order
          return toOrderAggregate(orderDO, itemDOS);
      }
    
      // ------------------- 对象转换器 -------------------
      private OrderDO toOrderDO(Order order){
          // 领域模型 → DO
          OrderDO orderDO = new OrderDO();
          orderDO.setId(order.getId());
          orderDO.setUserId(order.getUserId());
          orderDO.setStatus(order.getStatus().getCode());
          orderDO.setTotalAmount(order.getTotalAmount().getAmount());
          return orderDO;
      }
    
      private OrderItemDO toOrderItemDO(OrderItem item){
          OrderItemDO itemDO = new OrderItemDO();
          itemDO.setProductId(item.getProductId());
          itemDO.setQuantity(item.getQuantity());
          itemDO.setSubAmount(item.getSubTotal().getAmount());
          return itemDO;
      }
    
      private Order toOrderAggregate(OrderDO orderDO, List<OrderItemDO> itemDOS){
          // DO还原聚合根Order,省略实现
          return null;
      }

    }

5. 完整调用链路

复制代码
Controller(接口层)
    → OrderApplicationService(应用层:事务、编排)
        → OrderDomainService(领域服务:执行业务规则校验)
            → Order聚合根.create() (领域模型内部执行业务逻辑,生成领域事件)
        → OrderRepository.save() 【调用接口】
            → OrderRepositoryImpl(基础设施实现,DO转换,Mybatis保存Order+OrderItem)
        → popDomainEvents(),发布OrderCreatedDomainEvent事件

6. 关键落地注意事项(SpringBoot DDD 常见坑)

  1. ❌ 不要把领域层注入 Spring Bean:聚合根、实体、值对象是 POJO,不要加 @Service/@Component。只有领域服务、应用服务、仓储实现是 Spring Bean。
  2. ❌ 禁止在领域模型中直接操作数据库;领域层完全不依赖 mybatis。
  3. ❌ 禁止外部直接操作聚合内部子实体(本案例不能直接修改 OrderItem,必须通过 Order 聚合根)。
  4. ❌ 不要把 DO 直接返回给 Controller;不要把领域实体直接作为 DTO 返回。三层对象:DTO <-> DomainModel <-> DO,需要互相转换。
  5. ❌ 应用层不要写 if‑else 业务规则,业务规则下沉到领域对象 / 领域服务。
  6. ✅ 领域事件:领域只生产事件对象;事件发布交给应用层,领域层不感知 MQ。
  7. ✅ 聚合 = 事务边界;一个 Repository.save 操作,一个聚合一次持久化。

7. 什么时候不要上 DDD

  • 简单后台 CRUD 系统:直接 MVC 即可,DDD 会增加大量样板代码。
  • 业务需求经常变动,没有稳定业务领域。

DDD 收益来自复杂业务;代价是样板代码变多。

参考文档:

DDD领域驱动设计入门文档

相关推荐
爱敲代码的小杨.3 小时前
【Spring】Spring Web MVC
前端·spring·mvc
杨运交3 小时前
[069][公共模块]Spring Boot 全局异常处理与参数校验实战(下):校验异常精细化处理与 WebFlux 适配
java·spring boot·后端
行者-全栈开发3 小时前
Spring Boot + FFmpeg 视频批量处理实战:压缩、HLS切片与异步任务引擎
spring boot·ffmpeg·异步处理·视频压缩·hls切片·批量任务·redis队列
Andya_net4 小时前
Spring Boot | 条件注解完全指南:从 @Conditional 到 @ConditionalOnExpression 的原理、实践与避坑
spring boot·后端·python
ly76894 小时前
Spring 中的 @Configuration 与 @Component 差异:为何代理时机决定 Bean 生命周期行为
java·后端·spring·注解·代理·bean生命周期
Wang's Blog5 小时前
Java框架快速入门: Spring Security+OAuth2之安全配置基础及函数式风格对比
java·安全·spring
小荷才露尖尖角,早有蜻蜓立上头5 小时前
class java.util.LinkedHashMap cannot be cast to xxx
java·spring boot
rolt5 小时前
用UML表示的行业标准02汽车-AUTOSAR
软件工程·ddd·uml·领域驱动设计·ontology·本体
霸道流氓气质5 小时前
Spring AI Alibaba 系列总结:Java 工程师 AI 能力全景图谱
java·人工智能·spring