DDD 分层架构深度解析

面向中级开发者的领域驱动设计(Domain-Driven Design)实战指南。
从战略设计到代码落地,构建清晰、可维护的复杂业务系统。

00. 快速入门指南

🚀 5分钟理解 DDD

DDD(领域驱动设计)不是一个复杂的框架,而是一种思考方式。核心思想可以用三句话概括:

1. 业务逻辑应该在领域层,不是 Service 层
2. 数据库结构应该服从领域模型,不是相反
3. 代码应该说业务的语言,不是技术的语言

从一个简单例子开始

传统写法(事务脚本) DDD 写法(领域模型)
// Service 里充斥业务逻辑
public void withdraw(Long accountId, BigDecimal amount) {
  Account account = dao.findById(accountId);
  if (account.getBalance().compareTo(amount) < 0) {
    throw new Exception("余额不足");
  }
  account.setBalance(account.getBalance().subtract(amount));
  dao.update(account);
}
// 业务逻辑在实体内部
public class Account {
  public void withdraw(Money amount) {
    if (balance.lessThan(amount)) {
      throw new InsufficientBalanceException();
    }
    this.balance = balance.subtract(amount);
    addEvent(new WithdrawalMadeEvent());
  }
}

📋 学习路径

  • 初级(1-2周):理解聚合、实体、值对象的区别,能写充血模型
  • 中级(1-2月):掌握仓储模式、领域事件、防腐层,能设计限界上下文
  • 高级(3-6月):能进行事件风暴、上下文映射,处理复杂业务建模
⚠️ 新手常见误区
  • 把 DDD 当成框架来学,想找一个"DDD.jar"直接用
  • 所有项目都用 DDD,包括简单的增删改查
  • 只关注战术设计(代码层面),忽视战略设计(业务建模)

01. 引入:DDD 核心理念

战略设计:限界上下文 (Bounded Context)

在深入代码之前,必须理解 DDD 的战略设计。核心在于将复杂的业务领域划分为多个独立的限界上下文(Bounded Context)。 每个上下文都有明确的边界,内部拥有统一的通用语言(Ubiquitous Language)。

上下文映射(Context Map)则定义了不同上下文之间的集成关系(如防腐层 ACL、开放主机服务 OHS 等)。

示例场景:电商系统
我们将聚焦于"订单上下文"。在这个上下文中,"商品"可能只关注 ID、名称和价格快照,而在"商品上下文"中,商品则包含库存、供应商等丰富信息。

02. 何时使用 DDD?

🎯 DDD 适用场景诊断

并非所有项目都需要 DDD。以下清单帮助你判断:

判断维度 适合 DDD ✅ 不适合 DDD ❌
业务复杂度 复杂业务规则、多状态流转、跨多个业务实体的决策逻辑 简单的 CRUD、数据报表、工具类系统
项目生命周期 长期维护(3年+)、需求持续变化 一次性项目、原型验证、短期活动页面
团队规模 5人以上、有业务专家配合 1-2人小团队、无业务专家
系统规模 中大型系统、多子域、微服务架构 小型系统、单体应用、后台管理系统

典型适用场景

✅ 推荐使用 DDD:
  • 电商平台:订单、库存、促销、支付等多个子域,复杂的业务规则
  • 金融系统:账户、交易、风控、合规等,强一致性要求
  • 供应链管理:采购、仓储、物流等多环节协作
  • SaaS 平台:租户管理、权限、计费等复杂模型
❌ 不推荐使用 DDD:
  • 内容管理系统(CMS):主要是数据存取,业务逻辑简单
  • 数据分析平台:重数据处理,轻业务规则
  • 配置管理后台:纯粹的增删改查
  • 活动页面:生命周期短,不需要长期维护

决策树

项目是否有复杂业务逻辑?
  ├─ 否 → 使用事务脚本或 CRUD 框架(如 Spring Data JPA)
  └─ 是 → 业务规则是否频繁变化?
      ├─ 否 → 可以使用传统三层架构
      └─ 是 → 团队是否有 DDD 经验?
          ├─ 否 → 先学习,小模块试点
          └─ 是 → 全面采用 DDD

03. 四层架构详解

DDD 推荐的分层架构旨在将领域逻辑技术实现分离。核心原则是依赖倒置(DIP):高层模块不应依赖低层模块,二者都应依赖其抽象。

用户接口层 (User Interface) Controller, DTO, Facade 应用层 (Application Layer) Orchestration, Transaction, DTO Conversion 领域层 (Domain Layer) Entity, Aggregate, Value Object, Domain Service, Repo Interface 基础设施层 (Infrastructure Layer) DB, Cache, Repo Implementation, External API 实现接口 (DIP)

图 1:DDD 四层架构及其依赖关系(注意基础设施层对领域层的依赖倒置)

04. 各层职责与代码实现

我们将通过“创建订单”这一业务场景,展示每一层的具体实现。

1. 用户接口层 (User Interface)

职责:负责向用户展示信息和解释用户指令。它不包含业务逻辑,仅进行简单的数据校验和格式转换。

@RestController
@RequestMapping("/orders")
public class OrderController {
    
    private final OrderAppService orderAppService;

    // DTO (Data Transfer Object) 用于层间数据传输,避免暴露领域模型
    @PostMapping
    public Result<String> createOrder(@RequestBody CreateOrderRequest request) {
        // 调用应用层服务
        String orderId = orderAppService.createOrder(
            request.getCustomerId(), 
            request.getItems()
        );
        return Result.success(orderId);
    }
}

2. 应用层 (Application Layer)

职责:协调领域对象完成业务任务。它不包含业务规则,只负责编排流程(如事务控制、权限校验、发送事件)。

@Service
public class OrderAppService {
    
    private final OrderRepository orderRepository;
    private final ProductService productService; // 外部服务

    @Transactional
    public String createOrder(String customerId, List<OrderItemDTO> items) {
        // 1. 准备数据 (可能涉及其他上下文调用)
        Address address = customerService.getDefaultAddress(customerId);
        
        // 2. 调用领域层创建聚合根
        Order order = Order.create(customerId, address);
        
        // 3. 执行业务逻辑
        for (OrderItemDTO item : items) {
            order.addItem(item.getProductId(), item.getQuantity(), item.getPrice());
        }
        
        // 4. 持久化 (通过 Repository 接口)
        orderRepository.save(order);
        
        return order.getId().getValue();
    }
}

3. 领域层 (Domain Layer) - 核心

职责:包含所有业务逻辑和领域知识。是业务软件的核心。

  • 实体 (Entity):拥有唯一标识,具有生命周期(如 Order)。
  • 值对象 (Value Object):无唯一标识,通过属性描述特征,不可变(如 Address, Money)。
  • 聚合 (Aggregate):一组相关对象的集合,通过聚合根(Aggregate Root)保证一致性。
  • 仓储接口 (Repository Interface):定义资源访问的契约,不包含实现。
// 聚合根
public class Order extends AggregateRoot {
    private OrderId id;
    private OrderStatus status;
    private List<OrderItem> items; // 实体列表
    private Address shippingAddress; // 值对象

    // 构造方法私有,通过工厂方法创建
    private Order(String customerId, Address address) { ... }

    // 业务行为:添加订单项
    public void addItem(String productId, int quantity, BigDecimal price) {
        if (status != OrderStatus.DRAFT) {
            throw new DomainException("只能在草稿状态下添加商品");
        }
        this.items.add(new OrderItem(productId, quantity, price));
        // 领域事件
        addDomainEvent(new OrderItemAddedEvent(this.id, productId));
    }
}

// 仓储接口 (定义在领域层)
public interface OrderRepository {
    void save(Order order);
    Order findById(OrderId id);
}

4. 基础设施层 (Infrastructure Layer)

职责:为其他层提供技术能力(数据库持久化、消息队列、Redis、第三方 API)。

关键点:通过实现领域层的接口,完成依赖倒置。

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    
    private final JpaOrderDAO dao; // 具体 ORM 实现

    @Override
    public void save(Order order) {
        // 将领域模型转换为数据模型 (Data Model / PO)
        OrderPO po = OrderConverter.toPO(order);
        dao.save(po);
    }
}

05. 传统架构 vs DDD 架构:实战对比

📊 同一需求的两种实现

业务需求:用户下单,需检查库存、计算价格(含优惠券)、扣库存、创建订单、发送通知

❌ 传统方式的问题:业务逻辑分散在 Service 层、实体只是数据容器、单元测试困难、魔法字符串易出错
✅ DDD 方式的优势:业务内聚、充血模型、易于测试、通用语言、易于维护

06. 层间协作与数据流

下图展示了“创建订单”请求在各层之间的流转过程。注意控制流与依赖关系的区别。

Controller AppService Order (Domain) Repository 1. createOrder(DTO) 2. create() 3. addItem() 4. save(order) return result

07. 重构案例:从贫血到充血

🔄 逐步重构实战(账户转账)

假设我们接手一个遗留系统,需要将贫血模型重构为充血模型

❌ 贫血模型

  • Entity 只有 getter/setter,无业务逻辑
  • Service 层充斥业务规则
  • 使用魔法字符串和 BigDecimal

✅ 重构步骤

  • 第一步:创建值对象(Money、AccountStatus)封装基本逻辑
  • 第二步:将业务规则迁移到 Account 实体(withdraw、deposit方法)
  • 第三步:引入领域服务处理跨聚合操作(MoneyTransferDomainService)
  • 第四步:简化应用层,只负责加载聚合和持久化
  • 第五步:添加领域事件,实现解耦
重构效果:Service 从 50 行降到 15 行、业务逻辑内聚在实体、可独立单元测试
⚠️ 注意事项:逐步重构、先写测试、从核心聚合开始、团队达成共识

08. 架构变体与最佳实践

DDD 分层架构并非一成不变。以下是两种常见的演进架构:

架构模式 核心思想 适用场景
传统三层架构 数据驱动,自顶向下依赖 (UI -> Logic -> Data) 简单的 CRUD 业务,无复杂逻辑
DDD 分层架构 领域驱动,依赖倒置 (Infra 依赖 Domain) 中大型复杂业务系统
六边形架构 (Ports & Adapters) 内外分离,核心逻辑通过端口与外部交互 需要高度解耦,支持多种输入输出适配器
整洁架构 (Clean Arch) 同心圆结构,依赖指向圆心 (Entities) 追求极致的可测试性和框架无关性

⚠️ 常见误区

  • 领域模型贫血:Entity 中只有 getter/setter,业务逻辑全部在 Service 层。这是反模式。
  • 层间依赖混乱:Domain 层直接依赖 Infrastructure 层(如直接调用 DAO),破坏了 DIP。
  • DTO 泛滥:在所有层之间都进行严格的 DTO 转换,导致大量样板代码。建议灵活处理(如 App 层可直接返回 Entity 给 Controller 转换为 View Model)。

09. 进阶主题

领域事件 (Domain Events)

定义:领域事件是领域模型中发生的、业务专家关心的重要状态变更。它是实现最终一致性和系统解耦的关键机制。

// 领域事件定义
public class OrderCreatedEvent extends DomainEvent {
    private final OrderId orderId;
    private final String customerId;
    private final BigDecimal totalAmount;
    private final Instant occurredOn;
}

// 在聚合根中发布事件
public class Order extends AggregateRoot {
    public static Order create(String customerId, Address address) {
        Order order = new Order(customerId, address);
        order.addDomainEvent(new OrderCreatedEvent(order.id, customerId));
        return order;
    }
}

// 事件处理器
@EventHandler
public class OrderEventHandler {
    public void handle(OrderCreatedEvent event) {
        // 发送邮件、更新库存、记录日志等
        emailService.sendOrderConfirmation(event.getCustomerId());
    }
}

聚合的边界与一致性

核心原则:

  • 事务边界:一个事务只能修改一个聚合实例
  • 聚合之间通过 ID 引用:避免对象图过大,防止级联加载
  • 最终一致性:聚合间的一致性通过领域事件异步实现
❌ 错误示例:Order 聚合中包含完整的 Customer 对象
✅ 正确示例:Order 聚合中只存储 customerId (String/Value Object)

仓储模式深入

仓储 vs DAO:

维度 Repository (仓储) DAO (数据访问对象)
层次 领域层概念,面向聚合 持久化层概念,面向数据表
操作对象 聚合根 (Aggregate Root) 数据模型 (PO/DO)
方法命名 业务语言 (findByOrderNumber) 技术语言 (selectById)
返回值 领域对象 数据对象

防腐层 (Anti-Corruption Layer)

当与外部系统或遗留系统集成时,防腐层充当翻译器,保护领域模型不受外部模型污染。

// 防腐层实现
@Component
public class LegacySystemACL {
    private final LegacyApiClient legacyClient;

    // 将外部模型转换为领域模型
    public Product getProduct(String productId) {
        LegacyProductDTO dto = legacyClient.fetchProduct(productId);
        return Product.builder()
            .id(new ProductId(dto.getSkuCode()))
            .name(dto.getItemName())
            .price(new Money(dto.getPriceInCents() / 100))
            .build();
    }
}

10. 实战最佳实践

1. 充血模型 vs 贫血模型

贫血模型 (反模式):
// ❌ Entity 只有数据,无行为
public class Order {
    private String id;
    private String status;
    // 只有 getter/setter...
}

// 业务逻辑全在 Service 层
public class OrderService {
    public void cancel(Order order) {
        if (order.getStatus().equals("SHIPPED")) {
            throw new Exception();
        }
        order.setStatus("CANCELLED");
    }
}
充血模型 (推荐):
// ✅ Entity 包含业务逻辑
public class Order {
    private OrderId id;
    private OrderStatus status;

    // 业务行为封装在实体内部
    public void cancel() {
        if (status == OrderStatus.SHIPPED) {
            throw new DomainException("已发货订单不能取消");
        }
        this.status = OrderStatus.CANCELLED;
        addDomainEvent(new OrderCancelledEvent(this.id));
    }
}

2. 值对象的不可变性

值对象应该是不可变的,所有字段都是 final,通过构造函数或工厂方法创建。

public final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负");
        }
        this.amount = amount;
        this.currency = currency;
    }

    // 不提供 setter,通过新对象返回操作结果
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("货币类型不匹配");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    // 重写 equals 和 hashCode,基于值相等
    @Override
    public boolean equals(Object o) { ... }
}

3. 领域服务的使用时机

何时使用领域服务:

  • 操作涉及多个聚合,无法自然归属于某一个实体
  • 业务规则需要访问外部资源(如调用外部定价引擎)
  • 操作是无状态的业务算法
// 领域服务示例:转账操作涉及两个账户聚合
@Service
public class MoneyTransferDomainService {
    
    public void transfer(Account from, Account to, Money amount) {
        from.withdraw(amount);
        to.deposit(amount);
        // 发布领域事件
        eventPublisher.publish(new TransferCompletedEvent(from.id, to.id, amount));
    }
}

4. 工厂模式在 DDD 中的应用

当实体或聚合的创建逻辑复杂时,使用工厂模式封装创建细节。

public class OrderFactory {
    
    public Order createOrder(OrderCreationRequest request) {
        // 复杂的验证逻辑
        validateCustomer(request.getCustomerId());
        
        // 从多个来源组装数据
        Address address = addressService.getAddress(request.getAddressId());
        OrderId orderId = idGenerator.nextId();
        
        // 创建订单
        Order order = new Order(orderId, request.getCustomerId(), address);
        
        // 添加订单项
        request.getItems().forEach(item -> 
            order.addItem(item.getProductId(), item.getQuantity())
        );
        
        return order;
    }
}

5. 避免的常见陷阱

  • 过度设计:不是所有项目都需要 DDD,简单的 CRUD 系统使用事务脚本模式即可
  • 技术驱动建模:不要让数据库结构或框架限制影响领域模型设计
  • 忽视通用语言:代码中的类名、方法名应该直接来自业务术语
  • 聚合过大:聚合应该尽可能小,只包含必须保证强一致性的部分
  • 层间耦合:领域层依赖基础设施层是严重违反 DIP 原则的

11. 性能优化建议

🚀 DDD 架构中的性能考量

1. 聚合加载优化

  • ❌ 错误:级联加载整个聚合,导致 N+1 问题
  • ✅ 正确:聚合间只通过 ID 引用,按需加载

2. 查询与命令分离(CQRS 轻量版)

场景 命令模型(写) 查询模型(读)
目的 修改数据,保证业务规则 读取数据,性能优先
模型 领域模型(聚合) DTO / View Model
数据源 主库(强一致性) 从库 / 缓存 / ES

3. 仓储层缓存策略

  • findById 先查 Redis 缓存,未命中再查数据库
  • save 时更新缓存,设置合理的过期时间
  • 使用 Redis 集群保证高可用

4. 领域事件异步化

  • 使用 @Async 注解异步处理事件
  • 或发送到消息队列(RabbitMQ、Kafka)
  • 避免同步事件阻塞主流程

5. 性能监控指标

  • 聚合大小:单个聚合的实体数量应 < 10 个
  • 仓储查询:单次请求的仓储调用 < 5 次
  • 事务时长:业务事务应在 100ms 内完成
  • 领域事件:同步事件处理 < 50ms
💡 经验法则
  • 80% 的查询场景直接查 DTO,绕过领域模型
  • 20% 的命令场景使用完整领域模型
  • 性能瓶颈通常在数据库,不是 DDD 本身
  • 合理使用缓存、索引、读写分离