领域驱动设计(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
演进优势 域就是微服务拆分的天然边界
适用场景 中大型项目、多人协作、未来可能拆分
相关推荐
jarreyer3 小时前
【AI Agent】大模型自动化载体
运维·自动化
xixingzhe23 小时前
SpringBoot + Nginx 免鉴权接口安全防护方案
运维·nginx·安全
小明bishe183 小时前
计算机毕业设计之基于JAVA的植物科普网站
java·spring boot·spring·架构·课程设计
翼龙云_cloud4 小时前
阿里云国际代理商:阿里云 ECS 全维度监控配置教程
运维·阿里云·云计算·监控
笑锝没心没肺4 小时前
fail2ban工具安装配置及使用
linux·运维·服务器
zt1985q5 小时前
本地部署开源智能家居集成平台 ioBroker 并实现外部访问( Windows 版本)
运维·服务器·智能家居
万亿少女的梦1686 小时前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
万亿少女的梦1686 小时前
基于SpringBoot与Vue的历史博物馆展品展示与游客管理系统设计
spring boot·mysql·vue·系统设计·博物馆管理系统
大卡片6 小时前
linux内核驱动开发
linux·运维·驱动开发
心心喵7 小时前
[linux] nohup和pm2的区别 进程保活
linux·运维·服务器