Skip to content

DDD经典分层架构

要理解DDD(领域驱动设计)的分层架构,我们需要先回到DDD的核心目标将业务逻辑与技术细节隔离,让领域模型(即对业务的抽象表达)成为系统的核心。分层的本质是通过“职责边界划分”,避免业务逻辑被技术细节(如数据库、接口协议)侵蚀,同时让各层的变化互不影响(比如换数据库不影响领域逻辑,改接口协议不影响应用流程)。 ddd分层架构图.png

一、DDD经典分层架构的核心层次

DDD的分层架构并非固定公式,但经典四层模型(用户接口层→应用层→领域层→基础设施层)是最广泛使用的框架,我们从上层到核心依次拆解:

1. 第一层:用户接口层(User Interface Layer)——「用户与系统的“翻译官”」

核心职责:处理用户(或外部系统)的输入/输出,将用户请求转换为应用层能理解的参数,再将应用层的结果转换为用户能理解的响应(如JSON、页面、消息)。
本质无业务逻辑,仅做“参数映射”和“渠道适配”。

  • 常见组件

    • API接口(如Spring Boot的@RestController)、RPC接口;
    • 前端页面控制器(如Thymeleaf的@Controller);
    • 消息消费者(如RabbitMQ的Listener)、定时任务入口。
  • 例子
    一个电商系统的“创建订单”接口:

    java
    @RestController
    public class OrderController {
        @Autowired
        private OrderApplicationService orderAppService; // 依赖应用层服务
    
        @PostMapping("/orders")
        public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
            // 1. 校验请求参数格式(如必填项、手机号格式)
            validateRequest(request);
            // 2. 转换参数:将前端的DTO转为应用层的Command(命令对象)
            CreateOrderCommand command = Convert.toCommand(request);
            // 3. 调用应用层服务执行核心流程
            OrderDTO orderDTO = orderAppService.createOrder(command);
            // 4. 转换结果:将应用层的DTO转为前端响应
            OrderResponse response = Convert.toResponse(orderDTO);
            return ResponseEntity.ok(response);
        }
    }
  • 关键原则

    • 绝对不做业务逻辑(如“判断用户是否有优惠券”不能放在这里);
    • 只处理“渠道相关”的逻辑(如HTTP状态码、JSON序列化、消息格式);
    • 依赖应用层的接口(而非具体实现),通过IoC容器注入。

2. 第二层:应用层(Application Layer)——「业务流程的“编排者”」

核心职责:协调领域对象(实体、值对象)完成具体的业务流程,不包含核心业务规则,仅负责“调用谁、按什么顺序调用”。
本质流程胶水层,将领域层的“原子业务能力”组装成用户需要的“完整业务场景”。

  • 常见组件

    • 应用服务(Application Service):如OrderApplicationServiceUserApplicationService
    • 命令/查询对象(Command/Query):封装用户请求的参数(如CreateOrderCommand包含商品ID、用户ID、地址);
    • DTO(数据传输对象):用于层间数据传递(如OrderDTO)。
  • 例子
    “创建订单”的应用层逻辑:

    java
    @Service
    public class OrderApplicationService {
        @Autowired
        private UserRepository userRepository; // 依赖领域层的仓储接口
        @Autowired
        private ProductRepository productRepository;
        @Autowired
        private OrderRepository orderRepository;
        @Autowired
        private InventoryDomainService inventoryDomainService; // 依赖领域层的服务
    
        public OrderDTO createOrder(CreateOrderCommand command) {
            // 1. 获取领域对象(从仓储中加载实体)
            User user = userRepository.findById(command.getUserId());
            Product product = productRepository.findById(command.getProductId());
            // 2. 调用领域服务校验业务规则(如库存是否充足)
            inventoryDomainService.checkStock(product.getId(), command.getQuantity());
            // 3. 执行领域对象的行为(如创建订单实体)
            Order order = Order.create(user, product, command.getQuantity(), command.getAddress());
            // 4. 调用领域事件(如订单创建后发送通知)
            order.publishEvent(new OrderCreatedEvent(order.getId()));
            // 5. 持久化领域对象(调用仓储接口)
            orderRepository.save(order);
            // 6. 转换为DTO返回
            return Convert.toDTO(order);
        }
    }
  • 关键原则

    • 不写“为什么要做”(核心业务规则),只写“怎么做”(流程步骤);
    • 依赖领域层的抽象(如UserRepository接口、InventoryDomainService),而非具体实现;
    • 避免直接操作数据库、缓存等技术细节(这些交给基础设施层);
    • 一个应用服务方法对应一个用户故事(如“创建订单”“取消订单”),保持单一职责。

3. 第三层:领域层(Domain Layer)——「系统的“大脑”,DDD的核心」

核心职责:封装核心业务规则领域知识,是系统中最稳定、最有价值的部分(业务专家的知识都沉淀在这里)。
本质领域模型的载体,所有与业务相关的“是什么”(实体/值对象)和“怎么做”(领域服务/事件)都在这里。

领域层的核心组件是实体(Entity)值对象(Value Object)领域服务(Domain Service)领域事件(Domain Event)仓储接口(Repository Interface),我们先拆解前四个核心组件:

(1)实体(Entity):有唯一标识的“业务对象”

定义:具有唯一身份标识(ID),且其身份不依赖属性的对象(即使属性全变,ID不变则对象不变)。
核心特征

  • id字段(如orderIduserId);

  • 行为(而非单纯的“数据容器”)——实体的行为体现业务规则。

  • 例子:订单实体(Order):

    java
    public class Order {
        // 唯一标识(实体的核心)
        private OrderId id;
        // 关联的用户(实体)
        private UserId userId;
        // 商品信息(值对象)
        private ProductVO product;
        // 数量(基本类型)
        private int quantity;
        // 订单状态(枚举,体现业务规则)
        private OrderStatus status;
        // 总金额(值对象)
        private Money totalAmount;
    
        // 私有构造方法(强制通过工厂方法创建,保证对象合法性)
        private Order() {}
    
        // 工厂方法:创建订单(封装“订单必须包含用户、商品、数量”的规则)
        public static Order create(User user, Product product, int quantity, Address address) {
            Preconditions.checkNotNull(user, "用户不能为空");
            Preconditions.checkNotNull(product, "商品不能为空");
            Preconditions.checkArgument(quantity > 0, "数量必须大于0");
            
            Order order = new Order();
            order.id = OrderId.generate();
            order.userId = user.getId();
            order.product = ProductVO.from(product);
            order.quantity = quantity;
            order.status = OrderStatus.CREATED;
            // 计算总金额(值对象的行为)
            order.totalAmount = product.getPrice().multiply(quantity);
            return order;
        }
    
        // 行为:取消订单(封装“只有未支付的订单才能取消”的规则)
        public void cancel() {
            if (this.status != OrderStatus.CREATED) {
                throw new BusinessException("只有未支付的订单才能取消");
            }
            this.status = OrderStatus.CANCELED;
            // 发布领域事件(取消后恢复库存)
            this.publishEvent(new OrderCanceledEvent(this.id));
        }
    }
  • 关键原则

    • 实体不能是“贫血模型”(即只有getter/setter,没有行为)——所有与实体相关的业务规则都要封装在实体的方法中;
    • 通过工厂方法(如create)创建实体,避免外部直接new(保证对象创建时的合法性);
    • 实体的id应使用领域类型(如OrderId而非String),避免primitive obsession(基本类型偏执)。

(2)值对象(Value Object):无唯一标识的“不可变对象”

定义:没有唯一身份标识,通过属性值唯一的对象。一旦创建,属性不可修改(不可变性)。
核心特征

  • id字段;

  • 不可变(所有属性都是final,没有setter);

  • Equality基于属性值(重写equalshashCode)。

  • 例子:金额值对象(Money):

    java
    public class Money {
        private final BigDecimal amount; // 金额
        private final Currency currency; // 币种(如人民币、美元)
    
        // 构造方法私有化,通过静态方法创建
        private Money(BigDecimal amount, Currency currency) {
            this.amount = amount.setScale(2, RoundingMode.HALF_UP); // 强制两位小数
            this.currency = currency;
        }
    
        // 静态工厂方法:创建Money对象
        public static Money of(BigDecimal amount, Currency currency) {
            Preconditions.checkNotNull(amount, "金额不能为空");
            Preconditions.checkArgument(amount.compareTo(BigDecimal.ZERO) >= 0, "金额不能为负");
            Preconditions.checkNotNull(currency, "币种不能为空");
            return new Money(amount, currency);
        }
    
        // 行为:金额相乘(返回新的Money对象,保持不可变性)
        public Money multiply(int factor) {
            Preconditions.checkArgument(factor > 0, "乘数必须大于0");
            return Money.of(this.amount.multiply(new BigDecimal(factor)), this.currency);
        }
    
        // 重写equals和hashCode(基于amount和currency)
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Money money = (Money) o;
            return Objects.equals(amount, money.amount) && Objects.equals(currency, money.currency);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(amount, currency);
        }
    }
  • 关键原则

    • 用值对象代替基本类型(如用Money代替BigDecimal+String(币种)),避免“魔法值”和逻辑分散;
    • 不可变性:值对象一旦创建,属性不能修改(避免并发问题和意外修改);
    • 适合封装“无身份、靠属性唯一”的概念(如金额、地址、颜色、时间范围)。

(3)领域服务(Domain Service):跨实体的业务规则

定义:当业务规则需要多个实体协作,或不适合放在任何实体/值对象中时,将其封装为领域服务。
核心特征

  • 无状态(不保存数据,只处理业务逻辑);

  • 方法名体现业务动作(如checkStockcalculateDiscount)。

  • 例子:库存领域服务(InventoryDomainService):

    java
    @Service
    public class InventoryDomainService {
        @Autowired
        private ProductRepository productRepository;
        @Autowired
        private InventoryRepository inventoryRepository;
    
        // 校验库存(需要商品实体和库存实体协作)
        public void checkStock(ProductId productId, int quantity) {
            Product product = productRepository.findById(productId);
            Inventory inventory = inventoryRepository.findByProductId(productId);
            
            if (inventory.getStock() < quantity) {
                throw new BusinessException("商品" + product.getName() + "库存不足,当前库存:" + inventory.getStock());
            }
        }
    }
  • 关键原则

    • 领域服务不是“工具类”:工具类是通用的(如DateUtils),而领域服务是业务相关的(如“校验库存”是电商领域的核心规则);
    • 领域服务只依赖领域层的组件(如实体、值对象、仓储接口),不依赖应用层或基础设施层;
    • 避免滥用:能放在实体/值对象中的逻辑,绝对不放在领域服务中(比如“计算订单总金额”应该放在Order实体中,而不是领域服务)。

(4)领域事件(Domain Event):领域内的“重要事实”

定义:领域内发生的对业务有影响的事件(如“订单创建成功”“库存不足”),用于解耦领域对象之间的依赖(通过事件通知而非直接调用)。
核心特征

  • 命名体现“已发生的事实”(如OrderCreatedEventStockInsufficientEvent);

  • 包含事件的关键信息(如订单ID、发生时间)。

  • 例子:订单创建事件(OrderCreatedEvent):

    java
    public class OrderCreatedEvent {
        private final OrderId orderId;
        private final LocalDateTime occurredAt;
    
        public OrderCreatedEvent(OrderId orderId) {
            this.orderId = orderId;
            this.occurredAt = LocalDateTime.now();
        }
    
        // getter方法(无setter,保持不可变性)
        public OrderId getOrderId() { return orderId; }
        public LocalDateTime getOccurredAt() { return occurredAt; }
    }
  • 使用场景

    • 订单创建后,通知库存系统减库存(OrderCreatedEvent→库存服务监听并执行reduceStock);
    • 订单支付成功后,通知积分系统加积分(OrderPaidEvent→积分服务监听并执行addPoints)。
  • 关键原则

    • 领域事件由领域对象发布(如Order实体的create方法发布OrderCreatedEvent);
    • 事件处理者异步执行(避免同步调用导致的性能问题和耦合);
    • 事件是不可变的(一旦发布,不能修改)。

(5)仓储接口(Repository Interface)

在DDD中,仓储(Repository)是领域层的核心组件之一,它的作用是封装领域对象的持久化细节,让领域层不用关心“数据存在哪里”“怎么存”,只需要通过仓储的抽象接口操作领域对象。

(5.1)仓储的核心职责
  • 存储:将领域对象(如OrderUser)保存到持久化介质(数据库、缓存等);
  • 获取:根据领域条件(而非技术条件)查询领域对象(如“根据订单ID获取订单”“查询用户的未支付订单”);
  • 聚合根边界:仓储仅针对**聚合根(Aggregate Root)**设计(聚合根是领域模型的“大颗粒”对象,包含多个实体和值对象,比如Order是聚合根,包含OrderItem实体)。
(5.2)为什么仓储接口属于领域层?

仓储的定义(即“需要哪些存储/查询方法”)是领域知识的一部分。比如“订单仓储需要支持根据用户ID查询未支付订单”是业务需求,而非技术需求,因此仓储接口必须放在领域层,由业务专家参与设计。

而仓储的实现(比如用MySQL存订单、用Redis缓存用户)是技术细节,属于基础设施层(后面会讲)。

(5.3)示例:订单仓储接口
java
// 领域层的仓储接口(属于领域层)
public interface OrderRepository {
    // 保存订单(聚合根)
    void save(Order order);
    // 根据订单ID获取订单
    Order findById(OrderId orderId);
    // 根据用户ID查询未支付的订单(领域术语,无技术细节)
    List<Order> findUnpaidOrdersByUserId(UserId userId);
}
(5.4)关键原则
  • 仓储接口只暴露领域相关的方法:避免用findByCreateTimeBetween这样的技术查询条件(这是数据库的细节),而要用findRecentCreatedOrders(最近创建的订单)这样的领域术语;
  • 仓储仅操作聚合根:比如不能直接保存OrderItem(订单条目),必须通过Order聚合根保存(保证聚合内的一致性);
  • 仓储无业务逻辑:只做“存/取”,不做“校验库存”“计算金额”这样的业务规则。

4. 第四层:基础设施层(Infrastructure Layer)——「技术细节的“实现者”」

核心职责:实现领域层的抽象(如仓储接口、领域服务的依赖),提供通用的技术服务(数据库、缓存、消息队列、文件存储等),是系统的“地基”

简单来说,基础设施层是“做脏活累活的”:所有与技术相关的细节都在这里实现,上层(应用层、领域层)不需要关心。

(1)常见组件

  • 仓储实现:如OrderRepositoryImpl(实现领域层的OrderRepository接口,用JPA/MyBatis访问MySQL);
  • 技术服务:数据库连接池(HikariCP)、缓存客户端(RedisTemplate)、消息队列客户端(RabbitMQListener)、文件存储(MinIO);
  • 工具类:通用的技术工具(如JsonUtilsDateUtils);
  • 领域事件实现:如用RabbitMQ实现领域事件的发布/订阅(OrderCreatedEvent的生产者/消费者)。

(2)示例:订单仓储的实现

java
// 基础设施层的仓储实现(依赖领域层的接口)
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    // 依赖JPA的Repository(技术细节)
    @Autowired
    private JpaOrderRepository jpaOrderRepository;
    // 依赖转换器(将领域对象转为JPA实体)
    @Autowired
    private OrderConverter orderConverter;

    @Override
    public void save(Order order) {
        // 将领域对象(Order)转为JPA实体(JpaOrder)
        JpaOrder jpaOrder = orderConverter.toJpaEntity(order);
        // 调用JPA保存(技术细节)
        jpaOrderRepository.save(jpaOrder);
    }

    @Override
    public Order findById(OrderId orderId) {
        // 调用JPA查询
        Optional<JpaOrder> jpaOrder = jpaOrderRepository.findById(orderId.getValue());
        // 将JPA实体转为领域对象
        return jpaOrder.map(orderConverter::toDomainEntity).orElse(null);
    }

    @Override
    public List<Order> findUnpaidOrdersByUserId(UserId userId) {
        // 调用JPA查询(技术细节:根据user_id和status查询)
        List<JpaOrder> jpaOrders = jpaOrderRepository.findByUserIdAndStatus(userId.getValue(), OrderStatus.CREATED);
        // 转为领域对象列表
        return jpaOrders.stream().map(orderConverter::toDomainEntity).collect(Collectors.toList());
    }
}

// JPA的Repository(纯技术细节,属于基础设施层)
public interface JpaOrderRepository extends JpaRepository<JpaOrder, String> {
    List<JpaOrder> findByUserIdAndStatus(String userId, OrderStatus status);
}

(3)关键原则

  • 依赖领域层的抽象:基础设施层的实现类必须依赖领域层的接口(如OrderRepositoryImpl实现OrderRepository),而非上层依赖基础设施层的实现;
  • 隐藏技术细节:上层(应用层、领域层)看不到数据库、缓存的存在,所有技术细节都被封装在基础设施层;
  • 可替换性:如果需要换数据库(比如从MySQL到PostgreSQL),只需要修改基础设施层的OrderRepositoryImpl,上层代码完全不变。

三、DDD分层架构的核心约束:依赖规则(Dependency Rule)

DDD分层架构的灵魂依赖倒置原则(DIP),即:

上层只能依赖下层的抽象(接口),不能依赖下层的具体实现;下层不能依赖上层

用“依赖箭头”表示各层的关系:
用户接口层 ← 应用层 ← 领域层 → 基础设施层
(注:“←”表示“依赖于”,即应用层依赖领域层的抽象,基础设施层依赖领域层的抽象)

(1)具体约束细节

  1. 用户接口层:只能依赖应用层的接口(如OrderApplicationService),不能依赖领域层或基础设施层;
  2. 应用层:只能依赖领域层的抽象(如UserRepositoryInventoryDomainService),不能依赖基础设施层的实现;
  3. 领域层:不依赖任何上层(用户接口层、应用层),也不依赖基础设施层的具体实现;
  4. 基础设施层:可以依赖领域层的抽象(如实现OrderRepository),但不能依赖上层(应用层、用户接口层)。

(2)为什么要遵守依赖规则?

  • 解耦:上层不依赖下层的具体实现,修改下层(如换数据库)不会影响上层;
  • 可测试性:上层可以用Mock替换下层的实现(如用MockOrderRepository测试OrderApplicationService);
  • 业务聚焦:领域层不会被技术细节侵蚀,始终保持“纯业务”的核心地位。

四、DDD分层架构的实践误区

很多团队学DDD时,容易陷入以下误区,导致分层失去意义:

1. 贫血领域模型(Anemic Domain Model)

症状:实体(如Order)只有getter/setter,没有任何行为,所有业务逻辑都放在应用层(如OrderApplicationService)。
危害:领域模型失去了“封装业务规则”的作用,变成了“数据容器”,业务逻辑分散在应用层,难以维护。
反例

java
// 错误:贫血的Order实体(只有getter/setter)
public class Order {
    private String id;
    private String userId;
    private int quantity;
    private OrderStatus status;
    // 只有getter/setter,没有行为
}

// 错误:应用层做了领域层的事(判断订单是否可取消)
@Service
public class OrderApplicationService {
    public void cancelOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        // 业务规则放在应用层(应该放在Order实体的cancel方法里)
        if (order.getStatus() != OrderStatus.CREATED) {
            throw new BusinessException("不能取消已支付的订单");
        }
        order.setStatus(OrderStatus.CANCELED);
        orderRepository.save(order);
    }
}

正确做法:将业务规则封装在实体的行为中(如Order.cancel()方法),应用层只调用实体的行为。

2. 仓储接口泄漏技术细节

症状:领域层的Repository接口暴露了数据库的查询条件(如findByCreateTimeBetween)。
危害:领域层被技术细节污染,换数据库时需要修改领域层代码。
反例

java
// 错误:仓储接口暴露数据库细节(between是SQL的关键字)
public interface OrderRepository {
    List<Order> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end);
}

正确做法:用领域术语代替技术条件:

java
// 正确:用领域术语(最近7天创建的订单)
public interface OrderRepository {
    List<Order> findOrdersCreatedInLast7Days();
}

3. 应用层承担领域职责

症状:应用层处理核心业务规则(如“计算订单折扣”“校验用户权限”)。
危害:应用层变成“业务逻辑大杂烩”,领域层失去核心地位。
正确做法:核心业务规则必须放在领域层(实体、值对象、领域服务),应用层只做流程编排。

4. 依赖倒置未落地

症状:应用层直接依赖基础设施层的实现(如@Autowired OrderRepositoryImpl),而非领域层的接口。
危害:换数据库时需要修改应用层代码,违反开闭原则。
正确做法:应用层依赖领域层的接口(如@Autowired OrderRepository),通过IoC容器注入实现类。

五、DDD分层架构的优势

DDD分层架构的所有设计,都是为了解决**复杂业务系统的“可维护性”和“扩展性”**问题,具体优势如下:

1. 业务与技术分离

领域层沉淀了纯业务知识(业务专家能直接看懂),技术细节被封装在基础设施层,避免“业务逻辑被技术代码淹没”。

2. 高内聚、低耦合

各层职责明确:

  • user接口层:只处理输入输出;
  • 应用层:只做流程编排;
  • 领域层:只做业务规则;
  • 基础设施层:只做技术实现。
    修改某一层的代码,不会影响其他层(如换前端框架不影响领域逻辑,换数据库不影响应用流程)。

3. 可测试性强

  • 领域层的组件(实体、值对象、领域服务)可以单独测试(不需要启动数据库或服务器);
  • 应用层可以用Mock替换领域层的实现(如用MockOrderRepository测试OrderApplicationService);
  • 基础设施层的实现可以单独测试(如测试OrderRepositoryImpl是否正确保存订单)。

4. 应对业务变化

当业务需求变化时(如“新增优惠券抵扣规则”),只需要修改领域层的Order实体或CouponDomainService,应用层和用户接口层不需要修改;当技术需求变化时(如“从MySQL换成PostgreSQL”),只需要修改基础设施层的OrderRepositoryImpl,上层完全不变。

5. 团队协作高效

  • 业务团队(产品、业务专家)专注于领域层的设计;
  • 技术团队(后端、前端)专注于各层的实现;
  • 分层明确后,团队分工更清晰,避免“互相干扰”。

六、总结:DDD分层架构的本质

DDD分层架构不是“银弹”,但它是复杂业务系统的“设计方法论”——通过分层将“业务核心”与“技术细节”隔离,让系统的稳定性(核心业务规则不变)和灵活性(技术细节可替换)达到平衡。

最后用一句话总结DDD分层的核心:

领域层是“大脑”,决定“做什么”;应用层是“手脚”,决定“怎么做”;用户接口层是“眼睛和嘴巴”,负责“和外界沟通”;基础设施层是“身体”,负责“提供能量和支持”