领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南

领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南


一、什么是 DDD?

DDD(Domain-Driven Design,领域驱动设计)是 Eric Evans 在 2003 年提出的一种软件设计方法论。核心思想:以业务领域为中心组织代码,让代码结构直接反映业务结构。

传统分层(按技术分):

复制代码
src/
├── controller/      ← 所有 Controller 放一起
│   ├── OrderController.java
│   ├── UserController.java
│   └── ProductController.java
├── service/         ← 所有 Service 放一起
│   ├── OrderService.java
│   ├── UserService.java
│   └── ProductService.java
├── dao/             ← 所有 DAO 放一起
│   ├── OrderMapper.java
│   ├── UserMapper.java
│   └── ProductMapper.java
└── entity/          ← 所有 Entity 放一起
    ├── Order.java
    ├── User.java
    └── Product.java

DDD 分层(按业务域分):

复制代码
src/
├── domain/
│   ├── order/           ← 订单域:自包含一切
│   │   ├── api/
│   │   ├── controller/
│   │   ├── service/
│   │   ├── dao/
│   │   ├── entity/
│   │   └── dto/
│   ├── user/            ← 用户域
│   │   ├── api/
│   │   ├── controller/
│   │   ├── service/
│   │   ├── dao/
│   │   ├── entity/
│   │   └── dto/
│   └── product/         ← 商品域
│       ├── api/
│       ├── controller/
│       ├── service/
│       ├── dao/
│       ├── entity/
│       └── dto/
└── feign/               ← 跨域/跨服务调用

核心区别:传统分层是"找 OrderController 去 controller 包",DDD 是"找订单相关的一切去 order 包"。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、DDD 核心概念

2.1 战略设计(宏观层面)

概念 含义 类比
领域(Domain) 软件要解决的业务问题空间 电商、金融、物流
子域(Subdomain) 领域中的一个细分业务 订单、库存、支付、用户
限界上下文(Bounded Context) 一个子域的代码边界,内部自治 一个独立的模块/微服务
通用语言(Ubiquitous Language) 团队统一的业务术语 "寻源"="根据规则匹配仓库"
上下文映射(Context Map) 限界上下文之间的关系 订单域调用库存域

2.2 战术设计(代码层面)

概念 含义 Java 对应
实体(Entity) 有唯一标识的业务对象,可变 @Entity / @TableName
值对象(Value Object) 无唯一标识,通过属性相等判断 不可变的嵌套对象(Address、Money)
聚合(Aggregate) 一组相关对象的一致性边界 订单 + 订单明细 = 一个聚合
聚合根(Aggregate Root) 聚合的入口实体,外部只能通过它操作 Order 是聚合根,OrderItem 通过 Order 访问
领域服务(Domain Service) 不属于任何实体的业务逻辑 @Service
仓储(Repository) 聚合的持久化抽象 Mapper / DAO 接口
领域事件(Domain Event) 业务发生了什么事 Kafka/RocketMQ 消息
应用服务(Application Service) 编排领域对象完成用例 Controller 调用的 Service

2.3 限界上下文之间的关系

关系模式 含义 实现方式
防腐层(ACL) 隔离外部系统的模型差异 Feign + DTO 转换
开放主机服务(OHS) 通过 API 暴露能力 REST API
发布语言(PL) 共享的数据格式 公共 DTO/Proto
共享内核(SK) 两个上下文共享一部分模型 公共 JAR 包
客户-供应商(C-S) 上游提供 API,下游消费 Feign 调用

三、DDD 分层架构详解

3.1 标准四层架构

复制代码
┌─────────────────────────────────────────────────┐
│              接口层 (Interface / API)             │
│   REST Controller、GraphQL、gRPC、消息消费者      │
│   职责:接收请求、参数校验、调用应用层              │
└───────────────────────┬─────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│              应用层 (Application)                 │
│   Application Service、DTO 转换、事务编排         │
│   职责:用例编排、调用领域层,不含业务逻辑          │
└───────────────────────┬─────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│              领域层 (Domain)                      │
│   Entity、Value Object、Domain Service、Event    │
│   职责:核心业务逻辑,不依赖任何外部框架            │
└───────────────────────┬─────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│              基础设施层 (Infrastructure)           │
│   Repository 实现、Feign 调用、消息发送、缓存      │
│   职责:技术实现细节,对接外部系统                  │
└─────────────────────────────────────────────────┘

3.2 简化实践版(Spring Boot 微服务常用)

实际项目中,中小团队通常简化为:

复制代码
┌──────────────────────────────────────┐
│  API 层 (api/)                       │
│  接口定义 + OpenAPI 注解              │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│  Controller 层 (controller/)          │
│  请求处理 + 日志 + 异常捕获           │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│  Service 层 (service/)               │
│  业务逻辑编排(应用层 + 领域层合并)   │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│  DAO 层 (dao/mybatis/)               │
│  数据访问(仓储层实现)               │
└──────────────────────────────────────┘

这种方式保留了 DDD 的"按域分包"思想,但省去了严格的四层拆分,更适合 CRUD 居多的业务系统。


四、包结构设计

4.1 完整的域内包结构

复制代码
com.example.myapp/
├── Application.java                    # 启动类
├── common/                             # 全局共享
│   ├── exception/                      # 异常定义
│   │   ├── BusinessException.java
│   │   └── ErrorCode.java
│   └── util/                           # 工具类
├── config/                             # 全局配置
│   ├── DataSourceConfig.java
│   ├── RedisConfig.java
│   └── SecurityConfig.java
├── domain/                             # 业务域
│   ├── order/                          # 订单域(限界上下文)
│   │   ├── api/                        # API 接口定义
│   │   │   └── OrderApi.java
│   │   ├── controller/                 # 控制器
│   │   │   └── OrderController.java
│   │   ├── service/                    # 业务逻辑
│   │   │   ├── OrderService.java       # 接口
│   │   │   └── impl/
│   │   │       └── OrderServiceImpl.java
│   │   ├── dao/                        # 数据访问
│   │   │   └── mybatis/
│   │   │       └── OrderMapper.java
│   │   ├── entity/                     # 数据模型
│   │   │   ├── Order.java
│   │   │   └── OrderItem.java
│   │   ├── dto/                        # 数据传输对象
│   │   │   ├── CreateOrderRequest.java
│   │   │   ├── OrderResponse.java
│   │   │   └── feign/                  # Feign 调用的 DTO
│   │   │       └── StockDeductRequest.java
│   │   ├── enums/                      # 业务枚举
│   │   │   └── OrderStatusEnum.java
│   │   ├── event/                      # 领域事件
│   │   │   ├── OrderCreatedEvent.java
│   │   │   └── OrderEventPublisher.java
│   │   └── feign/                      # 域内 Feign 客户端
│   │       └── StockFeignClient.java
│   ├── product/                        # 商品域
│   │   ├── api/
│   │   ├── controller/
│   │   ├── service/
│   │   ├── dao/
│   │   ├── entity/
│   │   └── dto/
│   └── user/                           # 用户域
│       ├── api/
│       ├── controller/
│       ├── service/
│       ├── dao/
│       ├── entity/
│       └── dto/
└── feign/                              # 跨服务 Feign 客户端(调用其他微服务)
    ├── StockServiceFeign.java
    ├── PaymentServiceFeign.java
    └── LogisticsServiceFeign.java

4.2 各层职责边界

职责 禁止
api/ 定义接口契约(方法签名 + OpenAPI 注解) 不包含实现逻辑
controller/ 接收请求、调用 Service、封装响应、记录日志 不包含业务逻辑
service/ 实现业务逻辑、编排多个 DAO/Feign 调用 不直接操作 HTTP Request/Response
dao/ SQL 查询、数据持久化 不包含业务判断逻辑
entity/ 数据库表映射 不包含业务方法(贫血模型)
dto/ 接口传输数据、不同层间数据转换 不与数据库表绑定
enums/ 业务状态、类型定义 ---
feign/ 调用其他域/服务的接口定义 不包含实现

4.3 依赖方向规则

复制代码
Controller → Service → DAO
     ↓          ↓
    DTO       Entity
     ↓
   Feign(调用其他域)

关键原则

  • 上层可以依赖下层,下层不能依赖上层
  • 域之间不能直接调用对方的 Service,必须通过 Feign/API
  • Entity 不出 DAO 层,对外暴露用 DTO

五、跨域通信

5.1 同一微服务内的跨域调用

当两个域在同一个服务(同一个 JVM)中时:

复制代码
方式一(推荐):通过 Spring 依赖注入
  订单 Service → 注入商品 Service → 直接调用

方式二:通过领域事件
  订单 Service → 发布事件 → 商品 Service 监听并处理
// 订单域 Service 调用商品域 Service(同一服务内)
@Service
public class OrderServiceImpl implements OrderService {

    // 直接注入另一个域的 Service
    private final ProductService productService;

    public OrderServiceImpl(ProductService productService) {
        this.productService = productService;
    }

    @Override
    public OrderResponse createOrder(CreateOrderRequest request) {
        // 调用商品域查询商品信息
        ProductInfo product = productService.getProductById(request.getProductId());
        // ... 创建订单逻辑
    }
}

5.2 跨微服务的调用(Feign)

当两个域在不同的微服务中时,通过 HTTP 调用:

复制代码
订单服务                              库存服务
┌────────────────┐                ┌────────────────┐
│ OrderService   │  ── Feign ──→  │ StockController │
│                │  (HTTP POST)   │                │
│ StockFeign ────┤                │ StockService   │
└────────────────┘                └────────────────┘
Feign 客户端定义
java 复制代码
/**
 * 库存服务 Feign 客户端.
 * 用于订单域调用库存域的扣减库存接口.
 */
@FeignClient(
    name = "stock-service",           // 服务名(Nacos 注册名)
    url = "${feign.stock.url:}",      // 或直接指定 URL
    path = "/api/stock"               // 基础路径
)
public interface StockServiceFeign {

    /**
     * 扣减库存.
     *
     * @param request 扣减请求
     * @return 扣减结果
     */
    @PostMapping("/deduct")
    Result<StockDeductResponse> deductStock(@RequestBody StockDeductRequest request);

    /**
     * 查询库存.
     *
     * @param productId 商品ID
     * @param warehouseId 仓库ID
     * @return 库存数量
     */
    @GetMapping("/query")
    Result<Integer> queryStock(
        @RequestParam("productId") Long productId,
        @RequestParam("warehouseId") Long warehouseId);
}
在 Service 中使用 Feign
java 复制代码
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    private final OrderMapper orderMapper;
    private final StockServiceFeign stockServiceFeign;

    public OrderServiceImpl(OrderMapper orderMapper, StockServiceFeign stockServiceFeign) {
        this.orderMapper = orderMapper;
        this.stockServiceFeign = stockServiceFeign;
    }

    @Override
    @Transactional
    public OrderResponse createOrder(CreateOrderRequest request) {
        log.info("创建订单开始, userId={}, productId={}", 
            request.getUserId(), request.getProductId());

        // 1. 调用库存服务扣减库存(跨服务调用)
        StockDeductRequest deductRequest = new StockDeductRequest();
        deductRequest.setProductId(request.getProductId());
        deductRequest.setQuantity(request.getQuantity());

        Result<StockDeductResponse> stockResult = stockServiceFeign.deductStock(deductRequest);
        if (!stockResult.isSuccess()) {
            throw new BusinessException("库存扣减失败:" + stockResult.getMessage());
        }

        // 2. 创建订单(本域操作)
        Order order = buildOrder(request);
        orderMapper.insert(order);

        log.info("创建订单成功, orderId={}", order.getId());
        return toResponse(order);
    }
}

5.3 Feign 的配置

yaml 复制代码
# application.yml
spring:
  cloud:
    openfeign:
      client:
        config:
          default:                    # 全局默认配置
            connect-timeout: 5000     # 连接超时 5s
            read-timeout: 10000       # 读取超时 10s
            logger-level: basic       # 日志级别
          stock-service:              # 针对特定服务的配置
            connect-timeout: 3000
            read-timeout: 30000       # 库存接口可能慢,设长一些

# Feign URL 配置(开发环境直连,生产环境走服务发现)
feign:
  stock:
    url: http://localhost:3117        # 本地开发时直接指定

5.4 跨域通信方式对比

方式 适用场景 优点 缺点
直接注入 同一服务内的跨域 简单、无网络开销 域间耦合
Feign(同步 HTTP) 跨服务调用,需实时结果 简单直观、有返回值 有延迟、需处理超时
消息队列(异步) 不需要实时结果的场景 解耦、削峰、可靠 延迟、复杂度高
领域事件(本地) 同服务内的松耦合通知 解耦、可扩展 调试稍复杂

六、DTO 与 Entity 的分离

6.1 为什么要分离?

Entity DTO
对应数据库表 对应 API 请求/响应
包含所有字段(含敏感字段如密码) 只暴露需要的字段
内部使用 面向外部
可能有数据库注解 有校验/文档注解

直接暴露 Entity 的风险:

  • 泄漏敏感字段(password、内部 ID)
  • API 字段变动导致数据库改动
  • 无法针对不同场景返回不同字段

6.2 DTO 分类

复制代码
dto/
├── api/                # API 层 DTO(面向前端/外部调用者)
│   ├── CreateOrderRequest.java
│   ├── UpdateOrderRequest.java
│   └── OrderResponse.java
├── service/            # Service 层 DTO(内部编排用)
│   └── OrderCalculationDto.java
└── feign/              # Feign 调用 DTO(调用其他服务的请求/响应)
    ├── StockDeductRequest.java
    └── StockDeductResponse.java

6.3 转换方式

java 复制代码
// 手动转换(简单场景推荐)
private OrderResponse toResponse(Order order) {
    OrderResponse response = new OrderResponse();
    response.setOrderId(order.getId());
    response.setUserId(order.getUserId());
    response.setTotalAmount(order.getTotalAmount());
    response.setStatus(order.getStatus());
    response.setCreateTime(order.getCreateTime());
    return response;
}

// 或使用 BeanUtils(字段名一致时)
OrderResponse response = new OrderResponse();
BeanUtils.copyProperties(order, response);

// 或使用 MapStruct(编译时生成,性能最好)
@Mapper(componentModel = "spring")
public interface OrderConverter {
    OrderResponse toResponse(Order order);
    Order toEntity(CreateOrderRequest request);
}

七、完整通用示例

7.1 场景描述

一个简单的电商系统,包含三个域:

  • 订单域(Order):创建订单、查询订单
  • 商品域(Product):商品管理、库存查询
  • 用户域(User):用户信息管理

7.2 订单域完整代码

api/OrderApi.java
java 复制代码
package com.example.shop.domain.order.api;

import com.example.shop.common.Result;
import com.example.shop.domain.order.dto.CreateOrderRequest;
import com.example.shop.domain.order.dto.OrderResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 订单API接口定义.
 */
@Tag(name = "订单管理", description = "订单的创建和查询")
@RestController
@RequestMapping("/api/order")
public interface OrderApi {

  @Operation(summary = "创建订单")
  @PostMapping("/create")
  Result<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request);

  @Operation(summary = "查询订单详情")
  @GetMapping("/{orderId}")
  Result<OrderResponse> getOrder(@PathVariable Long orderId);

  @Operation(summary = "查询用户的订单列表")
  @GetMapping("/list")
  Result<List<OrderResponse>> listOrders(@RequestParam Long userId);
}
controller/OrderController.java
java 复制代码
package com.example.shop.domain.order.controller;

import com.example.shop.common.Result;
import com.example.shop.domain.order.api.OrderApi;
import com.example.shop.domain.order.dto.CreateOrderRequest;
import com.example.shop.domain.order.dto.OrderResponse;
import com.example.shop.domain.order.service.OrderService;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;

/**
 * 订单控制器.
 */
@Slf4j
@RestController
public class OrderController implements OrderApi {

  private final OrderService orderService;

  public OrderController(OrderService orderService) {
    this.orderService = orderService;
  }

  @Override
  public Result<OrderResponse> createOrder(CreateOrderRequest request) {
    log.info("创建订单请求, userId={}, productId={}", 
        request.getUserId(), request.getProductId());
    try {
      OrderResponse response = orderService.createOrder(request);
      log.info("创建订单成功, orderId={}", response.getOrderId());
      return Result.success(response);
    } catch (Exception e) {
      log.error("创建订单失败, userId={}, error={}", 
          request.getUserId(), e.getMessage(), e);
      return Result.fail(e.getMessage());
    }
  }

  @Override
  public Result<OrderResponse> getOrder(Long orderId) {
    OrderResponse response = orderService.getOrderById(orderId);
    return response != null ? Result.success(response) : Result.fail("订单不存在");
  }

  @Override
  public Result<List<OrderResponse>> listOrders(Long userId) {
    return Result.success(orderService.listByUserId(userId));
  }
}
service/OrderService.java
java 复制代码
package com.example.shop.domain.order.service;

import com.example.shop.domain.order.dto.CreateOrderRequest;
import com.example.shop.domain.order.dto.OrderResponse;
import java.util.List;

/**
 * 订单服务接口.
 */
public interface OrderService {

  /**
   * 创建订单.
   */
  OrderResponse createOrder(CreateOrderRequest request);

  /**
   * 根据ID查询订单.
   */
  OrderResponse getOrderById(Long orderId);

  /**
   * 查询用户的订单列表.
   */
  List<OrderResponse> listByUserId(Long userId);
}
service/impl/OrderServiceImpl.java
java 复制代码
package com.example.shop.domain.order.service.impl;

import com.example.shop.common.exception.BusinessException;
import com.example.shop.domain.order.dao.mybatis.OrderMapper;
import com.example.shop.domain.order.dto.CreateOrderRequest;
import com.example.shop.domain.order.dto.OrderResponse;
import com.example.shop.domain.order.entity.Order;
import com.example.shop.domain.order.enums.OrderStatusEnum;
import com.example.shop.domain.order.feign.ProductFeign;
import com.example.shop.domain.order.feign.StockFeign;
import com.example.shop.domain.order.service.OrderService;
import com.example.shop.common.Result;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 订单服务实现.
 */
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

  private static final int MAX_QUANTITY = 99;

  private final OrderMapper orderMapper;
  private final ProductFeign productFeign;
  private final StockFeign stockFeign;

  public OrderServiceImpl(OrderMapper orderMapper, 
                           ProductFeign productFeign,
                           StockFeign stockFeign) {
    this.orderMapper = orderMapper;
    this.productFeign = productFeign;
    this.stockFeign = stockFeign;
  }

  @Override
  @Transactional
  public OrderResponse createOrder(CreateOrderRequest request) {
    // 1. 参数校验
    validateRequest(request);

    // 2. 查询商品信息(跨域调用 - 商品服务)
    Result<ProductFeign.ProductInfo> productResult = 
        productFeign.getProduct(request.getProductId());
    if (!productResult.isSuccess()) {
      throw new BusinessException("商品查询失败");
    }
    ProductFeign.ProductInfo product = productResult.getData();

    // 3. 检查库存(跨域调用 - 库存服务)
    Result<Integer> stockResult = 
        stockFeign.queryStock(request.getProductId(), request.getWarehouseId());
    if (!stockResult.isSuccess() || stockResult.getData() < request.getQuantity()) {
      throw new BusinessException("库存不足");
    }

    // 4. 创建订单
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setProductId(request.getProductId());
    order.setProductName(product.getName());
    order.setQuantity(request.getQuantity());
    order.setUnitPrice(product.getPrice());
    order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity())));
    order.setStatus(OrderStatusEnum.CREATED.getCode());
    order.setCreateTime(LocalDateTime.now());

    orderMapper.insert(order);

    // 5. 扣减库存(跨域调用 - 库存服务)
    stockFeign.deductStock(request.getProductId(), request.getQuantity());

    return toResponse(order);
  }

  @Override
  public OrderResponse getOrderById(Long orderId) {
    if (orderId == null) {
      throw new BusinessException("订单ID不能为空");
    }
    Order order = orderMapper.selectById(orderId);
    return order != null ? toResponse(order) : null;
  }

  @Override
  public List<OrderResponse> listByUserId(Long userId) {
    if (userId == null) {
      throw new BusinessException("用户ID不能为空");
    }
    return orderMapper.selectByUserId(userId)
        .stream()
        .map(this::toResponse)
        .toList();
  }

  private void validateRequest(CreateOrderRequest request) {
    if (request.getUserId() == null) {
      throw new BusinessException("用户ID不能为空");
            }
    if (request.getProductId() == null) {
      throw new BusinessException("商品ID不能为空");
    }
    if (request.getQuantity() == null || request.getQuantity() <= 0) {
      throw new BusinessException("购买数量必须大于0");
    }
    if (request.getQuantity() > MAX_QUANTITY) {
      throw new BusinessException("单次购买数量不能超过" + MAX_QUANTITY);
    }
  }

  private OrderResponse toResponse(Order order) {
    OrderResponse response = new OrderResponse();
    response.setOrderId(order.getId());
    response.setUserId(order.getUserId());
    response.setProductId(order.getProductId());
    response.setProductName(order.getProductName());
    response.setQuantity(order.getQuantity());
    response.setUnitPrice(order.getUnitPrice());
    response.setTotalAmount(order.getTotalAmount());
    response.setStatus(order.getStatus());
    response.setStatusDesc(OrderStatusEnum.getDescByCode(order.getStatus()));
    response.setCreateTime(order.getCreateTime());
    return response;
  }
}
dto/CreateOrderRequest.java
java 复制代码
package com.example.shop.domain.order.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

/**
 * 创建订单请求.
 */
@Data
@Schema(description = "创建订单请求参数")
public class CreateOrderRequest {

  @Schema(description = "用户ID", example = "1001", requiredMode = Schema.RequiredMode.REQUIRED)
  @NotNull(message = "用户ID不能为空")
  private Long userId;

  @Schema(description = "商品ID", example = "2001", requiredMode = Schema.RequiredMode.REQUIRED)
  @NotNull(message = "商品ID不能为空")
  private Long productId;

  @Schema(description = "仓库ID", example = "301")
  private Long warehouseId;

  @Schema(description = "购买数量", example = "2", requiredMode = Schema.RequiredMode.REQUIRED)
  @NotNull(message = "购买数量不能为空")
  @Min(value = 1, message = "购买数量必须大于0")
  private Integer quantity;

  @Schema(description = "收货地址", example = "北京市朝阳区xxx路")
  private String address;
}
dto/OrderResponse.java
java 复制代码
package com.example.shop.domain.order.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;

/**
 * 订单响应.
 */
@Data
@Schema(description = "订单响应信息")
public class OrderResponse {

  @Schema(description = "订单ID")
  private Long orderId;

  @Schema(description = "用户ID")
  private Long userId;

  @Schema(description = "商品ID")
  private Long productId;

  @Schema(description = "商品名称")
  private String productName;

  @Schema(description = "购买数量")
  private Integer quantity;

  @Schema(description = "单价")
  private BigDecimal unitPrice;

  @Schema(description = "总金额")
  private BigDecimal totalAmount;

  @Schema(description = "订单状态编码")
  private String status;

  @Schema(description = "订单状态描述")
  private String statusDesc;

  @Schema(description = "创建时间")
  private LocalDateTime createTime;
}
entity/Order.java
java 复制代码
package com.example.shop.domain.order.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;

/**
 * 订单实体.
 */
@Data
@TableName("t_order")
public class Order {

  @TableId(type = IdType.AUTO)
  private Long id;

  private Long userId;
  private Long productId;
  private String productName;
  private Integer quantity;
  private BigDecimal unitPrice;
  private BigDecimal totalAmount;
  private String status;
  private String address;
  private LocalDateTime createTime;
  private LocalDateTime updateTime;

  @TableLogic
  private Integer deleted;
}
dao/mybatis/OrderMapper.java
java 复制代码
package com.example.shop.domain.order.dao.mybatis;

import com.example.shop.domain.order.entity.Order;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 订单 Mapper.
 */
@Mapper
public interface OrderMapper {

  /**
   * 插入订单.
   */
  int insert(Order order);

  /**
   * 根据ID查询.
   */
  Order selectById(@Param("id") Long id);

  /**
   * 根据用户ID查询订单列表.
   */
  List<Order> selectByUserId(@Param("userId") Long userId);

  /**
   * 更新订单状态.
   */
  int updateStatus(@Param("id") Long id, @Param("status") String status);
}
enums/OrderStatusEnum.java
java 复制代码
package com.example.shop.domain.order.enums;

import java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 订单状态枚举.
 */
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {

  CREATED("CREATED", "待支付"),
  PAID("PAID", "已支付"),
  SHIPPED("SHIPPED", "已发货"),
  DELIVERED("DELIVERED", "已签收"),
  COMPLETED("COMPLETED", "已完成"),
  CANCELLED("CANCELLED", "已取消");

  private final String code;
  private final String description;

  /**
   * 根据code获取描述.
   */
  public static String getDescByCode(String code) {
    return Arrays.stream(values())
        .filter(e -> e.getCode().equals(code))
        .map(OrderStatusEnum::getDescription)
        .findFirst()
        .orElse("未知状态");
  }

  /**
   * 校验code是否有效.
   */
  public static boolean isValid(String code) {
    return Arrays.stream(values())
        .anyMatch(e -> e.getCode().equals(code));
  }
}
feign/ProductFeign.java
java 复制代码
package com.example.shop.domain.order.feign;

import com.example.shop.common.Result;
import java.math.BigDecimal;
import lombok.Data;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * 商品服务 Feign 客户端.
 * 订单域通过此接口调用商品域获取商品信息.
 */
@FeignClient(name = "product-service", url = "${feign.product.url:}")
public interface ProductFeign {

  @GetMapping("/api/product/{productId}")
  Result<ProductInfo> getProduct(@PathVariable("productId") Long productId);

  /**
   * 商品信息(Feign 响应 DTO).
   */
  @Data
  class ProductInfo {
    private Long productId;
    private String name;
    private BigDecimal price;
    private String category;
  }
}
feign/StockFeign.java
java 复制代码
package com.example.shop.domain.order.feign;

import com.example.shop.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * 库存服务 Feign 客户端.
 * 订单域通过此接口调用库存域进行库存查询和扣减.
 */
@FeignClient(name = "stock-service", url = "${feign.stock.url:}")
public interface StockFeign {

  @GetMapping("/api/stock/query")
  Result<Integer> queryStock(
      @RequestParam("productId") Long productId,
      @RequestParam("warehouseId") Long warehouseId);

  @PostMapping("/api/stock/deduct")
  Result<Void> deductStock(
      @RequestParam("productId") Long productId,
      @RequestParam("quantity") Integer quantity);
}

7.3 公共模块

common/Result.java
java 复制代码
package com.example.shop.common;

import lombok.Data;

/**
 * 统一响应包装.
 */
@Data
public class Result<T> {

  private String code;
  private String message;
  private T data;

  public static <T> Result<T> success(T data) {
    Result<T> result = new Result<>();
    result.setCode("0");
    result.setMessage("success");
    result.setData(data);
    return result;
  }

  public static <T> Result<T> fail(String message) {
    Result<T> result = new Result<>();
    result.setCode("500");
    result.setMessage(message);
    return result;
  }

  public static <T> Result<T> fail(String code, String message) {
    Result<T> result = new Result<>();
    result.setCode(code);
    result.setMessage(message);
    return result;
  }

  public boolean isSuccess() {
    return "0".equals(this.code);
  }
}
common/exception/BusinessException.java
java 复制代码
package com.example.shop.common.exception;

/**
 * 业务异常.
 */
public class BusinessException extends RuntimeException {

  private final String code;

  public BusinessException(String message) {
    super(message);
    this.code = "BIZ_ERROR";
  }

  public BusinessException(String code, String message) {
    super(message);
    this.code = code;
  }

  public String getCode() {
    return code;
  }
}

7.4 MyBatis XML 映射

src/main/resources/mapper/order/OrderMapper.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.shop.domain.order.dao.mybatis.OrderMapper">

  <resultMap id="BaseResultMap" type="com.example.shop.domain.order.entity.Order">
    <id column="id" property="id"/>
    <result column="user_id" property="userId"/>
    <result column="product_id" property="productId"/>
    <result column="product_name" property="productName"/>
    <result column="quantity" property="quantity"/>
    <result column="unit_price" property="unitPrice"/>
    <result column="total_amount" property="totalAmount"/>
    <result column="status" property="status"/>
    <result column="address" property="address"/>
    <result column="create_time" property="createTime"/>
    <result column="update_time" property="updateTime"/>
    <result column="deleted" property="deleted"/>
  </resultMap>

  <sql id="Base_Column_List">
    id, user_id, product_id, product_name, quantity,
    unit_price, total_amount, status, address,
    create_time, update_time, deleted
  </sql>

  <insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO t_order (user_id, product_id, product_name, quantity,
        unit_price, total_amount, status, address, create_time)
    VALUES (#{userId}, #{productId}, #{productName}, #{quantity},
        #{unitPrice}, #{totalAmount}, #{status}, #{address}, #{createTime})
  </insert>

  <select id="selectById" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List"/>
    FROM t_order
    WHERE id = #{id} AND deleted = 0
  </select>

  <select id="selectByUserId" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List"/>
    FROM t_order
    WHERE user_id = #{userId} AND deleted = 0
    ORDER BY create_time DESC
  </select>

  <update id="updateStatus">
    UPDATE t_order
    SET status = #{status}, update_time = NOW()
    WHERE id = #{id} AND deleted = 0
  </update>

</mapper>

八、DDD 与传统分层的对比

维度 传统按技术分层 DDD 按域分包
组织方式 controller/、service/、dao/ domain/order/、domain/product/
修改范围 加一个功能要改多个顶层包 改动集中在一个域包内
团队协作 多人容易冲突(都改 service 包) 各域独立,减少冲突
可理解性 要跨多个包才能理解一个业务 打开一个包就能看到完整业务
可拆分性 难拆分成微服务 域天然是微服务的边界
复杂度 简单项目很直观 简单项目可能过度设计
适用规模 小项目(<10 个实体) 中大型项目(>10 个实体)

九、从单体到微服务的演进

DDD 的一大优势是域就是微服务的天然拆分边界

复制代码
阶段一:单体(所有域在一个服务中)
┌─────────────────────────────────┐
│ my-shop-service                 │
│  ├── domain/order/              │
│  ├── domain/product/            │
│  └── domain/user/               │
└─────────────────────────────────┘

阶段二:域间通过 Feign 接口隔离(为拆分做准备)
┌─────────────────────────────────┐
│ my-shop-service                 │
│  ├── domain/order/              │
│  │   └── feign/ProductFeign     │  ← 虽在同一服务,但已通过接口解耦
│  ├── domain/product/            │
│  └── domain/user/               │
└─────────────────────────────────┘

阶段三:拆分成独立微服务
┌───────────────┐  ┌───────────────┐  ┌───────────────┐
│ order-service │  │product-service│  │ user-service  │
│ domain/order/ │  │domain/product/│  │ domain/user/  │
└──────┬────────┘  └───────────────┘  └───────────────┘
       │
       │ Feign HTTP 调用(之前的本地接口现在变成远程调用)
       ↓
┌───────────────┐
│product-service│
└───────────────┘

拆分时只需:

  1. 把域包移到新项目
  2. 本地调用改为 Feign 远程调用(接口已经定义好了)
  3. 配置服务发现(Nacos/Eureka)

十、常见问题 FAQ

Q1: 小项目也要用 DDD 吗?

不一定。如果项目只有 3-5 个实体、2-3 个 API,传统分层更简单。DDD 的价值在复杂业务中体现:

  • 超过 10 个实体
  • 超过 3 个业务域
  • 多人协作
  • 未来可能拆分微服务

Q2: Entity 能不能有业务方法?

DDD 理论上推崇充血模型 (Entity 包含行为),但 Java 企业开发中普遍使用贫血模型(Entity 只有 getter/setter,逻辑放 Service)。两种都可以:

java 复制代码
// 贫血模型(常见做法)
@Data
public class Order {
  private String status;
  // 纯数据,无逻辑
}

// 充血模型(DDD 推崇)
public class Order {
  private String status;
  
  public void cancel() {
    if (!"CREATED".equals(this.status)) {
      throw new BusinessException("只有待支付订单可以取消");
    }
    this.status = "CANCELLED";
  }
  
  public boolean canShip() {
    return "PAID".equals(this.status);
  }
}

Q3: 域内的 Service 能直接调用另一个域的 Mapper 吗?

不能。 这违反了限界上下文的隔离原则。正确做法:

  • 同服务内:调用另一个域的 Service 接口
  • 跨服务:通过 Feign 调用
java 复制代码
// ❌ 错误:订单域直接访问商品域的 Mapper
@Resource
private ProductMapper productMapper; // 违反域边界

// ✅ 正确:通过商品域的 Service 或 Feign
@Resource
private ProductService productService; // 同服务内
// 或
@Resource
private ProductFeign productFeign;     // 跨服务

Q4: dto/api/ 和 dto/feign/ 能共用吗?

建议不共用。虽然字段可能相似,但它们面向不同消费者,变化原因不同:

  • dto/api/ 面向前端,字段会随 UI 需求变化
  • dto/feign/ 面向其他服务,字段随对方 API 变化

共用会导致"改一个 DTO 影响两个方向"的耦合。

Q5: feign/ 放在域内还是域外?

两种方式:

位置 适用场景
domain/order/feign/ 只被订单域使用的 Feign 客户端
顶层 feign/ 被多个域共用的 Feign 客户端

如果 StockFeign 只在订单域用,放 domain/order/feign/ 更内聚。如果多个域都要调库存,放顶层 feign/

Q6: 如何处理跨域事务?

单服务内跨域:用 Spring @Transactional 即可(同一个数据库连接)。

跨微服务的分布式事务:

  • 最终一致性(推荐):通过消息队列 + 补偿机制
  • Saga 模式:编排多个服务的本地事务
  • TCC:Try-Confirm-Cancel 三阶段
  • Seata:阿里开源的分布式事务框架

十一、总结

核心要点 一句话
DDD 的本质 按业务域组织代码,而非按技术层
限界上下文 每个域是独立自治的,有清晰的边界
域内结构 api → controller → service → dao → entity
跨域规则 禁止直接访问别人的内部,通过接口/Feign 通信
DTO 分离 Entity 不出域,对外只暴露 DTO
演进优势 域就是微服务拆分的天然边界
适用场景 中大型项目、多人协作、未来可能拆分
相关推荐
于先生吖2 小时前
SpringBoot对接大模型开发AI命理测算系统:八字排盘与AI解析接口源码全解
人工智能·spring boot·后端
Flittly2 小时前
【AgentScope Java新手村系列】(10)实战-多Agent天气助手
java·spring boot·spring
Inhand陈工2 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智3 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_3 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
施努卡机器视觉4 小时前
SNK施努卡侧滑门锁上滑轮总成自动化装配线,从零件到组件,全流程精密制造方案
运维·自动化·制造
AC赳赳老秦4 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
星落zx5 小时前
Spring Boot 多模型集成:优雅调用全球主流大模型
人工智能·spring boot·chatgpt
java_cj5 小时前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes