领域驱动设计(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│
└───────────────┘
拆分时只需:
- 把域包移到新项目
- 本地调用改为 Feign 远程调用(接口已经定义好了)
- 配置服务发现(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 |
| 演进优势 | 域就是微服务拆分的天然边界 |
| 适用场景 | 中大型项目、多人协作、未来可能拆分 |