DDD 代码怎么写?

📖 前置阅读:本文假设读者已理解实体、值对象、聚合根、限界上下文、领域事件的核心概念。如果还不熟悉,建议先阅读 DDD 本质——领域驱动设计的核心概念

一、⚡ 概念都懂了——但代码从哪个 package 开始建?

上一篇搞清楚了实体和值对象的区别、聚合根是"一致性边界"——但回到 IDE 中:

现有项目结构(MVC——三层):
  controller/
  ├─ OrderController.java
  service/
  ├─ OrderService.java (3000 行——上帝类)
  mapper/
  ├─ OrderMapper.java
  ├─ UserMapper.java       ← 跨表调用——OrderMapper 也调 UserMapper
  ├─ ProductMapper.java    ← 跨表调用
  model/
  ├─ Order.java            ← 只有 getter/setter——贫血
  ├─ User.java
  ├─ Product.java

问题——现在要改成 DDD——应该怎么建目录?Repository 放哪?Domain Service 放哪?

这篇就是答案——从目录结构开始——到每一层的代码——完整的落地模板。

二、📂 项目结构——DDD 四层架构

2.1 四层——不是"三层 + 一层"

传统 MVC 三层:
  Controller → Service → Mapper
  → Service 层无限膨胀——3000 行——什么都往里塞

DDD 四层:
  interfaces(接口层)   → 接收请求、返回响应——薄薄一层
  application(应用层)  → 编排业务流程——调 Repository、发事件——没有业务逻辑
  domain(领域层)       → 业务逻辑——聚合根、值对象、Repository 接口、领域事件
  infrastructure(基础设施层)→ 技术实现——Repository 实现、数据库访问、MQ 发送
order-service/
├── interfaces/                          ← ① 接口层
│   ├── rest/
│   │   └── OrderController.java         # HTTP 接口——接受请求——转给 application 层
│   ├── dto/
│   │   ├── CreateOrderRequest.java      # 入参 DTO
│   │   └── OrderResponse.java           # 出参 DTO
│   └── mq/
│       └── OrderEventListener.java      # MQ 消息消费——转到 application 层
│
├── application/                         ← ② 应用层
│   ├── OrderApplicationService.java     # 编排——调 Repository + 发事件——不包含业务逻辑
│   ├── command/
│   │   └── CreateOrderCommand.java      # 应用层自己的命令对象——DTO 转换后的内部对象
│   └── event/
│       └── OrderEventPublisher.java     # 事件发布接口——实现在 infrastructure
│
├── domain/                              ← ③ 领域层——核心——不依赖任何外部框架
│   ├── model/
│   │   ├── aggregate/
│   │   │   └── Order.java               # 聚合根
│   │   ├── entity/
│   │   │   └── OrderItem.java           # 聚合内部实体
│   │   ├── valueobject/
│   │   │   ├── Money.java               # 值对象——金额
│   │   │   ├── Address.java             # 值对象——地址
│   │   │   └── OrderStatus.java         # 枚举——订单状态
│   │   └── event/
│   │       ├── OrderCreatedEvent.java   # 领域事件
│   │       └── OrderPaidEvent.java
│   ├── repository/
│   │   └── OrderRepository.java         # Repository 接口——只有接口——没有实现
│   └── service/
│       ├── OrderDomainService.java      # 领域服务——跨聚合的逻辑
│       └── PricingService.java          # 领域服务——价格计算策略
│
└── infrastructure/                      ← ④ 基础设施层
    ├── persistence/
    │   ├── OrderRepositoryImpl.java     # Repository 实现——调 JPA/MyBatis
    │   ├── mapper/
    │   │   ├── OrderMapper.java         # MyBatis Mapper
    │   │   └── OrderItemMapper.java
    │   └── converter/
    │       └── OrderConverter.java      # DO ↔ Domain 对象转换
    ├── messaging/
    │   └── RocketMQEventPublisher.java  # 事件发布实现——发到 RocketMQ
    └── external/
        └── UserServiceAdapter.java      # 防腐层——隔离外部 User 服务

依赖方向——只能是单向的

interfaces → application → domain ← infrastructure
                                  ↑
                              只有 infrastructure 依赖 domain
                              domain 不依赖任何其他层——纯 Java——不依赖 Spring/MyBatis/RocketMQ

⚠️ 新手提示:依赖方向是最容易搞反的——domain 层不能 import @Service@Autowired@Entity@Table——domain 层是纯 POJO——不依赖任何框架。你想在聚合根上加 @Table(name="t_order") → 这就错了——那是 infrastructure 层的事。

三、🏰 聚合根——完整的实现

3.1 Order 聚合根——完整代码

// domain/model/aggregate/Order.java
// 聚合根——纯 POJO——不依赖任何框架
// 所有业务逻辑都在这里——Service 层只做编排

public class Order {
    // ========== 字段 ==========
    private Long id;
    private String orderNo;
    private Long userId;                  // 引用 User 聚合——只存 ID
    private Money totalAmount;            // 值对象
    private Address deliveryAddress;      // 值对象
    private OrderStatus status;
    private List<OrderItem> items;        // 聚合内部实体
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // 领域事件——临时收集——Repository 保存后发布
    private List<DomainEvent> domainEvents = new ArrayList<>();

    // ========== 构造函数——创建聚合时必须满足不变量 ==========
    // 不对外暴露——通过静态工厂方法创建
    private Order(Long userId, Address deliveryAddress, List<OrderItem> items) {
        if (userId == null) {
            throw new IllegalArgumentException("用户 ID 不能为空");
        }
        if (deliveryAddress == null) {
            throw new IllegalArgumentException("收货地址不能为空");
        }
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("订单项不能为空");
        }

        this.userId = userId;
        this.deliveryAddress = deliveryAddress;
        this.items = new ArrayList<>(items);
        this.orderNo = generateOrderNo();
        this.status = OrderStatus.PENDING_PAY;
        this.totalAmount = calculateTotalAmount();
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();

        // 注册领域事件——"订单已创建"
        this.domainEvents.add(new OrderCreatedEvent(
                this.id, this.orderNo, this.userId, this.totalAmount));
    }

    // ========== 静态工厂方法——创建聚合的入口 ==========
    public static Order create(Long userId, Address deliveryAddress,
                                List<OrderItem> items) {
        return new Order(userId, deliveryAddress, items);
    }

    // ========== 业务方法——聚合根的核心——保护不变量 ==========

    // ① 修改收货地址——有业务规则
    public void changeDeliveryAddress(Address newAddress) {
        if (this.status != OrderStatus.PENDING_PAY) {
            throw new BusinessException("只有待支付订单才能修改收货地址");
        }
        if (Duration.between(this.createdAt, LocalDateTime.now()).toMinutes() > 10) {
            throw new BusinessException("下单超过 10 分钟不能修改收货地址");
        }
        this.deliveryAddress = Objects.requireNonNull(newAddress);
        this.updatedAt = LocalDateTime.now();
    }

    // ② 添加订单项——防止重复添加
    public void addItem(OrderItem item) {
        if (this.status != OrderStatus.PENDING_PAY) {
            throw new BusinessException("待支付状态才能修改订单项");
        }
        if (items.stream().anyMatch(i -> i.getProductId().equals(item.getProductId()))) {
            throw new BusinessException("该商品已在订单中");
        }
        this.items.add(item);
        this.totalAmount = calculateTotalAmount();
        this.updatedAt = LocalDateTime.now();
    }

    // ③ 支付——聚合状态的迁移
    public void pay(Money paidAmount) {
        if (this.status != OrderStatus.PENDING_PAY) {
            throw new BusinessException("只有待支付订单才能支付——当前状态:" + this.status);
        }
        if (!this.totalAmount.equals(paidAmount)) {
            throw new BusinessException("支付金额不匹配——应付:" + this.totalAmount
                    + "——实付:" + paidAmount);
        }
        this.status = OrderStatus.PAID;
        this.updatedAt = LocalDateTime.now();

        // 注册领域事件——"订单已支付"
        this.domainEvents.add(new OrderPaidEvent(this.id, this.orderNo, this.userId));
    }

    // ④ 取消——不同状态有不同的取消规则
    public void cancel(String reason) {
        if (this.status == OrderStatus.SHIPPED
                || this.status == OrderStatus.DELIVERED) {
            throw new BusinessException("已发货的订单不能取消——可走退货流程");
        }
        if (this.status == OrderStatus.CANCELLED) {
            throw new BusinessException("订单已取消——不能重复取消");
        }
        this.status = OrderStatus.CANCELLED;
        this.updatedAt = LocalDateTime.now();

        // 注册领域事件——"订单已取消"
        this.domainEvents.add(new OrderCancelledEvent(this.id, this.orderNo, reason));
    }

    // ⑤ 发货——标记发货
    public void ship(String trackingNumber) {
        if (this.status != OrderStatus.PAID) {
            throw new BusinessException("只有已支付订单才能发货");
        }
        this.status = OrderStatus.SHIPPED;
        this.updatedAt = LocalDateTime.now();
        this.domainEvents.add(new OrderShippedEvent(this.id, this.orderNo, trackingNumber));
    }

    // ========== 领域事件收集——Repository 在 save 后调用 ==========
    public List<DomainEvent> pollDomainEvents() {
        List<DomainEvent> events = new ArrayList<>(this.domainEvents);
        this.domainEvents.clear();
        return events;
    }

    // ========== 查询方法——不影响状态 ==========
    public boolean canBeCancelled() {
        return this.status == OrderStatus.PENDING_PAY
                || this.status == OrderStatus.PAID;
    }

    public int getTotalItemCount() {
        return items.stream().mapToInt(OrderItem::getQuantity).sum();
    }

    // ========== 内部辅助 ==========
    private String generateOrderNo() {
        return "ORD" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                + String.format("%04d", new Random().nextInt(10000));
    }

    private Money calculateTotalAmount() {
        return items.stream()
                .map(OrderItem::getSubTotal)
                .reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add);
    }

    // ========== getter——只读——没有 setter ==========
    // 外部不能随意修改聚合的属性——只能通过聚合根的业务方法
    public Long getId() { return id; }
    public String getOrderNo() { return orderNo; }
    public Long getUserId() { return userId; }
    public Money getTotalAmount() { return totalAmount; }
    public Address getDeliveryAddress() { return deliveryAddress; }
    public OrderStatus getStatus() { return status; }
    public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
    public LocalDateTime getCreatedAt() { return createdAt; }

    // 只有 package-private 的 setter——给 Repository 实现用(在 infrastructure 层)
    void setId(Long id) { this.id = id; }
}

3.2 聚合根设计的四个检查点

检查点验证Order 是怎么做的
不变量保护构造函数参数非法时拒绝创建userId 不能为 null——items 不能为空
状态迁移状态只能通过业务方法改变不能 order.setStatus(PAID)——必须 order.pay(amount)
跨聚合引用只存 ID——不存对象private Long userId——不是 private User user
领域事件聚合状态变化后注册事件pay() 后注册 OrderPaidEvent

四、📦 Repository——接口在 domain——实现在 infrastructure

4.1 Repository 接口——定义在 domain 层

// domain/repository/OrderRepository.java
// 接口在 domain 层——只定义"领域需要的操作"——不暴露数据库细节
// 不依赖 MyBatis、JPA——纯 Java 接口

public interface OrderRepository {

    // ① 按 ID 查询
    Optional<Order> findById(Long id);

    // ② 按订单号查询
    Optional<Order> findByOrderNo(String orderNo);

    // ③ 查询用户的订单——返回领域对象——不是 DO
    List<Order> findByUserId(Long userId);

    // ④ 保存聚合——保存的是聚合根——连带保存聚合内部的实体
    void save(Order order);

    // ⑤ 删除
    void delete(Order order);

    // ⑥ 查询待支付的超时订单——领域关注的查询条件
    List<Order> findPendingPaymentBefore(LocalDateTime deadline);
}
// domain/repository/ProductRepository.java
// Product 是另一个聚合——有自己的 Repository

public interface ProductRepository {
    Optional<Product> findById(Long productId);
    List<Product> findByIds(List<Long> productIds);
    void save(Product product);
}

⚠️ 新手提示:Repository 不要定义 findByNameLike(String keyword) 这种通配符查询方法——那是 DAO 的思维。Repository 是"聚合的仓库"——提供领域需要的入口——不是"能执行的所有 SQL 查询集合"。

4.2 Repository 实现——在 infrastructure 层

// infrastructure/persistence/OrderRepositoryImpl.java
// 实现在 infrastructure 层——依赖 MyBatis
// 做两件事:① DO ↔ Domain 转换 ② 调 Mapper

@Repository  // ← Spring 注解只能在 infrastructure 层
public class OrderRepositoryImpl implements OrderRepository {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;

    @Override
    public Optional<Order> findById(Long id) {
        OrderDO orderDO = orderMapper.selectById(id);
        if (orderDO == null) return Optional.empty();

        List<OrderItemDO> itemDOs = orderItemMapper.selectByOrderId(id);
        return Optional.of(OrderConverter.toDomain(orderDO, itemDOs));
    }

    @Override
    public void save(Order order) {
        // ① 转换为 DO
        OrderDO orderDO = OrderConverter.toDO(order);
        List<OrderItemDO> itemDOs = OrderConverter.toItemDOs(order);

        // ② 如果 ID 为 null——新增;否则——更新
        if (order.getId() == null) {
            orderMapper.insert(orderDO);
            // 回填 ID——聚合根需要 ID
            order.setId(orderDO.getId());
            for (OrderItemDO itemDO : itemDOs) {
                itemDO.setOrderId(orderDO.getId());
                orderItemMapper.insert(itemDO);
            }
        } else {
            orderMapper.updateById(orderDO);
            // 订单项的更新——先删后插(简单实现——生产可以用 merge)
            orderItemMapper.deleteByOrderId(order.getId());
            for (OrderItemDO itemDO : itemDOs) {
                itemDO.setOrderId(order.getId());
                orderItemMapper.insert(itemDO);
            }
        }
    }

    @Override
    public void delete(Order order) {
        orderItemMapper.deleteByOrderId(order.getId());
        orderMapper.deleteById(order.getId());
    }

    // ... 其他方法
}

4.3 DO ↔ Domain 转换器

// infrastructure/persistence/converter/OrderConverter.java
// 专门负责 DO ↔ Domain 对象的转换
// 转换逻辑集中在一处——不散落在 Service 或 Mapper 中

public class OrderConverter {

    public static OrderDO toDO(Order order) {
        OrderDO orderDO = new OrderDO();
        orderDO.setId(order.getId());
        orderDO.setOrderNo(order.getOrderNo());
        orderDO.setUserId(order.getUserId());
        orderDO.setTotalAmount(order.getTotalAmount().getAmount());  // Money → BigDecimal
        orderDO.setCurrency(order.getTotalAmount().getCurrency());
        orderDO.setProvince(order.getDeliveryAddress().getProvince()); // Address → 字段
        orderDO.setCity(order.getDeliveryAddress().getCity());
        orderDO.setDistrict(order.getDeliveryAddress().getDistrict());
        orderDO.setDetail(order.getDeliveryAddress().getDetail());
        orderDO.setZipCode(order.getDeliveryAddress().getZipCode());
        orderDO.setStatus(order.getStatus().name());
        orderDO.setCreatedAt(order.getCreatedAt());
        return orderDO;
    }

    public static Order toDomain(OrderDO orderDO, List<OrderItemDO> itemDOs) {
        Address address = new Address(
                orderDO.getProvince(), orderDO.getCity(),
                orderDO.getDistrict(), orderDO.getDetail(),
                orderDO.getZipCode());

        List<OrderItem> items = itemDOs.stream()
                .map(OrderConverter::toDomainItem)
                .toList();

        // 用反射或 package-private 构造器重建聚合根
        // 这里可以用 Builder 模式——或者给 Repository 留一个 package-private 的重建方法
        Order order = Order.reconstruct(
                orderDO.getId(), orderDO.getOrderNo(), orderDO.getUserId(),
                new Money(orderDO.getTotalAmount(), orderDO.getCurrency()),
                address, OrderStatus.valueOf(orderDO.getStatus()),
                items, orderDO.getCreatedAt()
        );
        return order;
    }

    private static OrderItem toDomainItem(OrderItemDO itemDO) {
        return new OrderItem(
                itemDO.getId(), itemDO.getProductId(), itemDO.getProductName(),
                new Money(itemDO.getPrice(), itemDO.getCurrency()),
                itemDO.getQuantity());
    }
}
// 在 Order 聚合根中——给 Repository 实现留一个重建方法
// domain/model/aggregate/Order.java
public class Order {

    // ... 之前的代码

    // package-private——只给 Repository 实现用——外部不能调
    // DDD 官方称为 "reconstitution"(重建)
    static Order reconstruct(Long id, String orderNo, Long userId,
                              Money totalAmount, Address deliveryAddress,
                              OrderStatus status, List<OrderItem> items,
                              LocalDateTime createdAt) {
        Order order = new Order(userId, deliveryAddress, items);
        order.id = id;
        order.orderNo = orderNo;
        order.totalAmount = totalAmount;
        order.status = status;
        order.createdAt = createdAt;
        return order;
    }
}

五、🎯 Domain Service vs Application Service——最难的分界线

5.1 判断标准——逻辑归谁

Application Service(应用服务)——编排——没有业务逻辑
  ① 接收请求——转成领域对象
  ② 调 Repository 查聚合
  ③ 调聚合根的业务方法(业务逻辑在聚合根中)
  ④ 调 Repository 保存聚合
  ⑤ 发布领域事件
  ⑥ 返回结果

Domain Service(领域服务)——业务逻辑——当逻辑不属于任何一个聚合时
  ① 跨聚合的复杂计算
  ② 调用外部服务的编排——但需要领域知识
  ③ 多聚合的协调——但不在一个事务中

黄金判断——问自己三个问题

问题 1:这个逻辑能放到现有的聚合根里吗?
  → 能 → 放聚合根——不要建 Domain Service
  → "判断用户是否首单" → 可以放在 OrderDomainService.isFirstOrder(userId) —— 因为需要查 OrderRepository 的历史订单——Order 自己不知道

问题 2:这个逻辑涉及多个聚合吗?
  → 是 → Domain Service
  → "计算订单总价时——是否应用首单折扣" → 需要查 OrderRepository + PricingStrategy → Domain Service

问题 3:这个逻辑是"编排"还是"计算/判断"?
  → 编排(第一步做 A 第二步做 B)→ Application Service
  → 计算/判断(有业务规则)→ Domain Service

5.2 完整示例——同一场景中三者的分工

// ========== Application Service——编排 ==========
// application/OrderApplicationService.java
@Service
public class OrderApplicationService {

    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private OrderDomainService orderDomainService;
    @Autowired
    private OrderEventPublisher eventPublisher;

    @Transactional
    public OrderResponse createOrder(CreateOrderCommand command) {
        // ① 加载聚合——通过 Repository
        List<Product> products = productRepository.findByIds(
                command.getItems().stream()
                        .map(CreateOrderCommand.OrderItemCommand::getProductId)
                        .toList());

        // ② 调用 Domain Service——跨聚合的领域逻辑
        List<OrderItem> orderItems = orderDomainService.createOrderItems(
                command.getItems(), products);

        // ③ 调用 Domain Service——计算价格(可能涉及折扣策略)
        Money totalPrice = orderDomainService.calculatePrice(
                command.getUserId(), orderItems);

        // ④ 创建聚合根——业务逻辑在聚合根构造函数中
        Order order = Order.create(
                command.getUserId(),
                command.getDeliveryAddress(),
                orderItems);

        // ⑤ 持久化——调 Repository
        orderRepository.save(order);

        // ⑥ 发布领域事件——异步
        for (DomainEvent event : order.pollDomainEvents()) {
            eventPublisher.publish(event);
        }

        // ⑦ 返回结果——DTO 转换
        return OrderResponse.from(order);
    }
}
// ========== Domain Service——跨聚合的领域逻辑 ==========
// domain/service/OrderDomainService.java
// 注意:Domain Service 是纯 POJO——不加 @Service——不依赖 Spring
// 如果需要 Spring 管理——加 @Service 也可以——但逻辑上它是 domain 层的东西

public class OrderDomainService {

    /**
     * 创建订单项——校验商品状态、库存、生成 OrderItem
     * 这是领域逻辑——因为涉及"商品是否可售"、"价格快照"等业务规则
     * 不属于任何一个聚合根——所以放在 Domain Service
     */
    public List<OrderItem> createOrderItems(
            List<CreateOrderCommand.OrderItemCommand> commands,
            List<Product> products) {

        // 把 Product 列表转为 Map——方便查找
        Map<Long, Product> productMap = products.stream()
                .collect(Collectors.toMap(Product::getId, Function.identity()));

        List<OrderItem> items = new ArrayList<>();
        for (CreateOrderCommand.OrderItemCommand cmd : commands) {
            Product product = productMap.get(cmd.getProductId());
            if (product == null) {
                throw new BusinessException("商品不存在——ID:" + cmd.getProductId());
            }
            if (!product.isOnSale()) {
                throw new BusinessException("商品 " + product.getName() + " 已下架");
            }
            if (!product.hasEnoughStock(cmd.getQuantity())) {
                throw new BusinessException("商品 " + product.getName() + " 库存不足");
            }

            // 创建 OrderItem——价格拍照(快照)——防止商品涨价后历史订单金额被影响
            OrderItem item = new OrderItem(
                    product.getId(),
                    product.getName(),     // ← 快照——商品改名不影响订单
                    product.getPrice(),    // ← 快照——商品涨价不影响订单
                    cmd.getQuantity());
            items.add(item);
        }
        return items;
    }

    /**
     * 计算订单总价——可能包含折扣策略
     * 折扣策略本身是另一个 Domain Service:PricingService
     */
    public Money calculatePrice(Long userId, List<OrderItem> items) {
        Money basePrice = items.stream()
                .map(OrderItem::getSubTotal)
                .reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add);

        // 折扣由 PricingService 处理——PricingService 也是 Domain Service
        return basePrice;  // 这里简化——下一篇讲折扣策略
    }
}
// ========== 聚合根——保护不变量 ==========
// domain/model/aggregate/Order.java
// 业务逻辑都在聚合根里——Application Service 不包含业务判断
public class Order {

    public void pay(Money paidAmount) {
        // 业务逻辑:只有待支付才能支付、金额必须匹配
        if (this.status != OrderStatus.PENDING_PAY) {
            throw new BusinessException("只有待支付订单才能支付");
        }
        if (!this.totalAmount.equals(paidAmount)) {
            throw new BusinessException("支付金额不匹配");
        }
        this.status = OrderStatus.PAID;
        this.domainEvents.add(new OrderPaidEvent(this.id, this.orderNo, this.userId));
    }
}

三层分工——一句话

职责一句话示例
Application Service编排“第一步做什么、第二步做什么”查用户 → 查商品 → 创建订单 → 保存 → 发事件
Domain Service跨聚合计算“这个计算涉及多个聚合——放不进任何一个里面”计算首单折扣(需要查历史订单 + 商品 + 价格策略)
聚合根保护不变量“我这个聚合的数据不能变成非法状态”支付时校验状态 + 金额——状态转移

六、🛡️ 防腐层(ACL)——隔离外部系统

6.1 问题——外部 UserService 的模型入侵

场景:OrderService 创建订单时需要查用户信息——用户是在 UserService(另一个服务)中的

不用防腐层——直接依赖外部模型:
  @Autowired
  private UserClient userClient;  // Feign 接口——外部定义的

  UserDTO user = userClient.getUser(userId);  // UserDTO 是外部定义的——100 个字段
  // 你只用了 userId 和 membershipLevel 两个字段
  // 但哪天 UserDTO 加了 10 个字段——你的编译没问题——运行时却可能受影响
  // 哪天 UserDTO 删了一个字段——你编译失败——虽然你不用这个字段

6.2 防腐层的实现

// ========== 订单上下文自己的 Buyer 模型 ==========
// domain/model/valueobject/Buyer.java
// 注意:这是 OrderContext 中的 Buyer——不是 UserContext 中的 User
// 只包含订单上下文关心的字段

public class Buyer {
    private Long userId;
    private String nickname;
    private MembershipLevel membershipLevel;  // 值对象——会员等级
    private Address defaultAddress;           // 值对象——默认收货地址

    // 订单上下文的 Buyer 只需要这些——不需要 password、phone、email 等认证信息

    public boolean isVip() {
        return membershipLevel == MembershipLevel.VIP
                || membershipLevel == MembershipLevel.SVIP;
    }
}
// ========== 防腐层——翻译外部模型 ==========
// infrastructure/external/UserServiceAdapter.java
// 在 infrastructure 层——依赖外部 Feign 接口
// 把外部 UserDTO 翻译成订单上下文中的 Buyer

@Component
public class UserServiceAdapter {

    @Autowired
    private UserClient userClient;  // Feign——外部服务的接口

    /**
     * 从外部 UserService 获取用户信息——翻译成订单上下文的 Buyer
     * 外部 UserDTO 有 100 个字段 → Buyer 只保留订单需要的 4 个字段
     */
    public Buyer getBuyer(Long userId) {
        UserDTO userDTO = userClient.getUser(userId);
        if (userDTO == null) {
            throw new BusinessException("用户不存在——ID:" + userId);
        }

        // 翻译——外部模型 → 领域模型
        return new Buyer(
                userDTO.getId(),
                userDTO.getNickname(),
                MembershipLevel.fromString(userDTO.getMembershipLevel()),
                new Address(
                        userDTO.getDefaultProvince(),
                        userDTO.getDefaultCity(),
                        userDTO.getDefaultDistrict(),
                        userDTO.getDefaultDetail(),
                        userDTO.getDefaultZipCode()
                )
        );
    }
}
// ========== 在 Application Service 中使用防腐层 ==========
// application/OrderApplicationService.java
@Service
public class OrderApplicationService {

    @Autowired
    private UserServiceAdapter userServiceAdapter;  // 防腐层——不是直接注入 UserClient
    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public OrderResponse createOrder(CreateOrderCommand command) {
        // 通过防腐层获取 Buyer——不直接接触外部 UserDTO
        Buyer buyer = userServiceAdapter.getBuyer(command.getUserId());

        if (!buyer.isVip()) {
            // 非 VIP 的某些限制...
        }

        // ... 其余逻辑
    }
}

防腐层的价值:外部系统变了——只改防腐层内部——Application Service 和 Domain 层的代码不受影响。外部 UserDTO 加了 50 个字段或删了 10 个字段——你只改 UserServiceAdapter 中的翻译逻辑——业务逻辑不受影响。

七、📢 领域事件的事务性发布

7.1 问题——事务还没提交就发了事件

@Transactional
public void createOrder(CreateOrderCommand command) {
    Order order = Order.create(...);
    orderRepository.save(order);     // ← 还在事务中——还没 commit

    // ❌ 如果在这里直接发事件——消费者可能读到未提交的数据
    for (DomainEvent event : order.pollDomainEvents()) {
        eventPublisher.publish(event);  // 消费者此时查不到这条订单——还没 commit
    }
}  // ← 事务在这里 commit

7.2 解决方案一——Spring 的 @TransactionalEventListener

// application/OrderApplicationService.java
@Service
public class OrderApplicationService {

    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private ApplicationEventPublisher springEventPublisher;

    @Transactional
    public void createOrder(CreateOrderCommand command) {
        Order order = Order.create(...);
        orderRepository.save(order);

        // 用 Spring 的 ApplicationEventPublisher——发到 Spring 内部的事件总线
        for (DomainEvent event : order.pollDomainEvents()) {
            springEventPublisher.publishEvent(event);
        }
    }
}

// ========== 事件处理器——在事务提交后才执行 ==========
@Component
public class OrderEventDispatcher {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent event) {
        // 事务已经提交——订单已经入库——安全发送到 MQ
        rocketMQTemplate.syncSend("order-created-topic", event);
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderPaid(OrderPaidEvent event) {
        rocketMQTemplate.syncSend("order-paid-topic", event);
    }
}

7.3 解决方案二——Outbox 模式(更可靠——生产推荐)

// Domain 层——事件存储接口
// domain/repository/EventStore.java
public interface EventStore {
    void save(DomainEvent event);
    List<DomainEvent> findUnpublished();
    void markPublished(Long eventId);
}

// Infrastructure 层——事件存储实现
// infrastructure/persistence/EventStoreImpl.java
@Repository
public class EventStoreImpl implements EventStore {

    @Autowired
    private EventMapper eventMapper;

    @Override
    public void save(DomainEvent event) {
        EventDO eventDO = new EventDO();
        eventDO.setEventType(event.getClass().getSimpleName());
        eventDO.setPayload(JsonUtil.toJson(event));  // 序列化事件
        eventDO.setStatus("UNPUBLISHED");
        eventMapper.insert(eventDO);
    }
}

// Application Service——保存事件和聚合在同一个事务中
@Transactional
public void createOrder(CreateOrderCommand command) {
    Order order = Order.create(...);
    orderRepository.save(order);

    // 领域事件和聚合一起保存在同一个数据库中——同一个事务
    for (DomainEvent event : order.pollDomainEvents()) {
        eventStore.save(event);  // ← 存在同一数据库——事务保证一致性
    }
}

// 定时任务——异步扫描未发布的事件——发送到 MQ
@Scheduled(fixedDelay = 1000)
public void publishUnpublishedEvents() {
    List<DomainEvent> events = eventStore.findUnpublished();
    for (DomainEvent event : events) {
        try {
            rocketMQTemplate.syncSend(getTopic(event), event);
            eventStore.markPublished(event.getId());
        } catch (Exception e) {
            log.error("事件发送失败——eventId={}", event.getId(), e);
            // 下次定时任务重试——at-least-once——消费者需要幂等
        }
    }
}

⚠️ 新手提示:Outbox 模式的本质是——事件和聚合存在一起(同一个数据库——同一个事务——保证了原子性),然后异步从 Outbox 表中读出事件发送到 MQ。比直接发 MQ 可靠——因为如果 MQ 挂了——事务回滚——聚合也没插入——不会出现"聚合已存、事件丢失"的中间状态。

八、🏭 Factory——封装复杂的聚合创建

8.1 问题——聚合的创建逻辑散落各处

Order 的聚合根构造函数是 private 的——通过 static factory method create() 创建

但如果创建逻辑很复杂呢?
  → 先查 User 服务——获取 Buyer 信息
  → 再查 Product 服务——获取商品价格
  → 再查 Pricing 服务——获取折扣策略
  → 计算运费
  → 生成订单号(如果订单号不是随机——而是从序列服务获取)

这些调用放在 Application Service 中——Application Service 就膨胀了
Factory 封装复杂的创建过程——Application Service 只需要一行调用

8.2 Factory 实现

// domain/factory/OrderFactory.java
// Factory 在 domain 层——但可能依赖 infrastructure 层的防腐层
// 所以其实现在 infrastructure 层——接口在 domain 层

public interface OrderFactory {
    Order createOrder(Long userId, Address deliveryAddress,
                       List<OrderItemCommand> itemCommands);
}

// infrastructure/factory/OrderFactoryImpl.java
@Component
public class OrderFactoryImpl implements OrderFactory {

    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private UserServiceAdapter userServiceAdapter;
    @Autowired
    private OrderDomainService orderDomainService;
    @Autowired
    private OrderNoGenerator orderNoGenerator;  // 订单号生成——可能调 Redis 自增

    @Override
    public Order createOrder(Long userId, Address deliveryAddress,
                              List<OrderItemCommand> itemCommands) {
        // ① 获取买家信息——防腐层
        Buyer buyer = userServiceAdapter.getBuyer(userId);

        // ② 查询商品——校验库存
        List<Long> productIds = itemCommands.stream()
                .map(OrderItemCommand::getProductId).toList();
        List<Product> products = productRepository.findByIds(productIds);

        // ③ 创建订单项——Domain Service
        List<OrderItem> orderItems = orderDomainService.createOrderItems(
                itemCommands, products);

        // ④ 创建订单——调用聚合根的静态工厂方法
        Order order = Order.create(userId, deliveryAddress, orderItems);

        return order;
    }
}
// Application Service——用 Factory 简化
@Transactional
public OrderResponse createOrder(CreateOrderCommand command) {
    // 一行——创建聚合
    Order order = orderFactory.createOrder(
            command.getUserId(),
            command.getDeliveryAddress(),
            command.getItems());

    orderRepository.save(order);
    publishDomainEvents(order.pollDomainEvents());
    return OrderResponse.from(order);
}

🎯 总结

  1. 四层架构——依赖方向是铁律:interfaces → application → domain ← infrastructure。domain 层是纯 POJO——不依赖 Spring、MyBatis、RocketMQ。聚合根、值对象、Repository 接口、领域事件——全在 domain 层——纯 Java。

  2. Repository 接口在 domain——实现在 infrastructure:domain 定义了"领域需要的聚合入口"(findById/save/delete),infrastructure 用 MyBatis/JPA 实现——并做 DO ↔ Domain 对象转换。不是在 Service 中直接调 Mapper——那样领域逻辑又散落了。

  3. Application Service 编排——Domain Service 跨聚合计算——聚合根保护不变量:判断"逻辑归谁"的三个问题——“能放聚合根吗?““涉及多个聚合吗?““是编排还是计算/判断?"——答完就知道放哪。

  4. 防腐层隔离外部系统——Outbox 保证事件可靠发布:外部模型通过 Adapter 翻译成领域模型——外部变了只改防腐层。领域事件用 Outbox 模式——和聚合在同一个事务中持久化——定时任务异步发送到 MQ——消费者幂等。

📖 下一步阅读:代码模板有了——回头看我们之前的 order-service / user-service / product-service——用 DDD 的视角重构它们——什么时候该用 DDD、什么时候 MVC 就够了?继续阅读 回顾微服务——用 DDD 重构已有代码