【Java项目技术亮点】领域驱动设计DDD分层架构

写在前面

说实话,我刚接触DDD的时候,整个人是懵的。什么聚合根、限界上下文、领域事件,听起来像玄学。直到我接手了一个维护了三年的电商项目,Service层代码超过5000行,一个需求改下来要动十几个文件,我才真正理解DDD的价值。这篇文章把我踩过的坑和学到的经验分享给你,希望能帮你少走点弯路。

文章目录


一、为什么需要DDD?

1.1 传统三层架构的困境

我见过太多项目,一开始用三层架构(Controller → Service → DAO)跑得飞快,半年后就成了"代码坟场"。

java 复制代码
// 传统三层架构的典型代码
@Service
public class OrderService {
    
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private OrderItemDao orderItemDao;
    @Autowired
    private InventoryDao inventoryDao;
    @Autowired
    private PaymentDao paymentDao;
    // ... 还有十几个Dao
    
    public void createOrder(OrderDTO dto) {
        // 1. 保存订单
        // 2. 保存订单项
        // 3. 扣减库存
        // 4. 创建支付记录
        // 5. 发送消息
        // ... 几百行代码挤在一个方法里
    }
}

业务逻辑散落在Service层,订单逻辑、库存逻辑、支付逻辑全搅在一起。一个需求改动,要改十几个文件,改完还不敢保证没bug。

1.2 生活类比

传统三层架构就像杂货铺,所有东西堆在一起,找个螺丝钉要翻半天。

DDD就像超市,按品类分区,生鲜区、日用品区、电器区,找东西清晰,补货也方便。

1.3 DDD的核心价值

价值点 说明
业务与技术解耦 业务逻辑集中在领域层,不依赖框架
代码可维护性 修改只影响一个聚合,不会牵一发而动全身
便于单元测试 领域对象不依赖Spring,纯Java代码就能测
团队沟通 用业务语言写代码,产品和开发能聊到一起

二、DDD核心概念

2.1 领域与子域

领域(Domain):你要解决的业务问题范围。比如电商系统,领域就是"电商"。

子域(Subdomain):把大领域拆成小块。

子域类型 说明 例子
核心域 公司的核心竞争力,投入最多资源 电商里的下单、支付
支撑域 支撑核心域运转,但非核心 物流跟踪、库存管理
通用域 通用能力,可以外包或买现成的 用户认证、消息通知

2.2 限界上下文(Bounded Context)

限界上下文就是业务的边界。同一个词在不同上下文里意思可能完全不同。

举个例子:"商品"在商品上下文 里指SKU信息(名称、价格、图片),在库存上下文 里指库存数量,在物流上下文里指包裹里的物品。三个"商品"不是同一个东西,不要硬塞到一个类里。

通常一个限界上下文对应一个微服务。

2.3 实体与值对象

实体(Entity):有唯一标识,即使属性全变,只要ID不变还是同一个对象。

java 复制代码
// 订单是实体,orderId是唯一标识
public class Order {
    private OrderId orderId;  // 唯一标识
    private List<OrderItem> items;
    private Address address;
    private OrderStatus status;
    
    // 业务方法
    public void pay() {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new IllegalStateException("订单状态异常");
        }
        this.status = OrderStatus.PAID;
    }
    
    public void cancel() {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new IllegalStateException("只能取消待支付订单");
        }
        this.status = OrderStatus.CANCELLED;
    }
}

值对象(Value Object) :没有唯一标识,通过属性判断相等,不可变

java 复制代码
// 地址是值对象
public class Address {
    private final String province;
    private final String city;
    private final String detail;
    
    public Address(String province, String city, String detail) {
        this.province = province;
        this.city = city;
        this.detail = detail;
    }
    
    // 值对象不可变,修改时返回新对象
    public Address withCity(String newCity) {
        return new Address(this.province, newCity, this.detail);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(province, address.province) &&
               Objects.equals(city, address.city) &&
               Objects.equals(detail, address.detail);
    }
}

2.4 聚合与聚合根

聚合(Aggregate):一组相关对象的集合,对外是一个整体。

聚合根(Aggregate Root):聚合的入口,外部只能通过聚合根访问内部对象。

java 复制代码
// Order是聚合根,OrderItem在Order聚合内部
public class Order {
    private OrderId orderId;
    private List<OrderItem> items;  // 只能通过Order操作OrderItem
    private Address address;
    
    // 外部不能直接new OrderItem,只能通过Order添加
    public void addItem(Product product, int quantity, Money price) {
        OrderItem item = new OrderItem(product.getId(), quantity, price);
        this.items.add(item);
        // 维护聚合内的一致性规则
        if (getTotalAmount().compareTo(Money.ZERO) <= 0) {
            throw new IllegalArgumentException("订单金额必须大于0");
        }
    }
    
    public Money getTotalAmount() {
        return items.stream()
            .map(OrderItem::getSubTotal)
            .reduce(Money.ZERO, Money::add);
    }
}

2.5 领域事件

领域事件 = 领域内发生的有意义的事情。

java 复制代码
// 订单创建事件
public class OrderCreatedEvent {
    private final OrderId orderId;
    private final List<OrderItem> items;
    private final LocalDateTime occurredOn;
    
    public OrderCreatedEvent(OrderId orderId, List<OrderItem> items) {
        this.orderId = orderId;
        this.items = items;
        this.occurredOn = LocalDateTime.now();
    }
    
    // getters...
}

2.6 仓储与领域服务

仓储(Repository) :聚合的持久化接口,只操作聚合根

java 复制代码
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
    List<Order> findByUserId(UserId userId);
}

领域服务(Domain Service):跨实体的业务逻辑,不属于任何一个实体。

java 复制代码
// 促销计算涉及订单、优惠券、会员多个实体,放在领域服务里
public class PromotionService {
    
    public Money calculateDiscount(Order order, Coupon coupon, Member member) {
        // 复杂的促销计算逻辑
        Money discount = coupon.apply(order);
        if (member.isVip()) {
            discount = discount.add(order.getTotalAmount().multiply(0.05));
        }
        return discount;
    }
}

三、DDD四层架构详解

3.1 架构分层

复制代码
┌─────────────────────────────────────┐
│      用户接口层 (User Interface)      │  ← Controller、DTO转换、参数校验
│         Adapter / Web层              │
├─────────────────────────────────────┤
│        应用层 (Application)           │  ← 用例编排、事务控制、事件发布
├─────────────────────────────────────┤
│         领域层 (Domain)              │  ← 实体、值对象、领域服务、领域事件
│                                      │     核心业务逻辑,不依赖任何框架
├─────────────────────────────────────┤
│       基础设施层 (Infrastructure)     │  ← 数据库访问、消息队列、外部API
└─────────────────────────────────────┘

3.2 依赖关系

核心原则:领域层不依赖任何其他层。

  • 用户接口层 → 依赖应用层、领域层
  • 应用层 → 依赖领域层
  • 领域层 → 不依赖任何层
  • 基础设施层 → 依赖领域层(实现领域层的接口)

这就是依赖倒置原则的体现。领域层定义接口(如Repository),基础设施层实现接口。

3.3 各层职责

层级 职责 包含内容
用户接口层 接收请求、返回响应、参数校验 Controller、DTO、Validator
应用层 编排用例、控制事务、转换DTO ApplicationService、事务注解
领域层 核心业务逻辑 Entity、ValueObject、DomainService、DomainEvent
基础设施层 技术实现 RepositoryImpl、MQSender、HttpClient

3.4 代码分层示例

复制代码
com.example.order
├── application          // 应用层
│   ├── OrderApplicationService.java
│   └── dto
│       ├── OrderCreateRequest.java
│       └── OrderResponse.java
├── domain               // 领域层(最核心,不依赖Spring)
│   ├── model
│   │   ├── Order.java           // 聚合根
│   │   ├── OrderItem.java       // 实体
│   │   ├── Address.java         // 值对象
│   │   └── OrderStatus.java     // 枚举
│   ├── service
│   │   └── OrderDomainService.java
│   ├── event
│   │   └── OrderCreatedEvent.java
│   └── repository
│       └── OrderRepository.java // 接口
├── infrastructure       // 基础设施层
│   ├── repository
│   │   └── OrderRepositoryImpl.java  // MyBatis/JPA实现
│   └── messaging
│       └── OrderEventPublisher.java
└── interfaces           // 用户接口层
    └── OrderController.java

四、DDD vs 传统三层架构对比

4.1 对比表格

对比维度 传统三层架构 DDD分层架构
关注点 以技术为中心(数据表驱动) 以业务为中心(领域模型驱动)
代码组织 按技术分层(Service、Dao) 按业务领域(Order、Payment)
可维护性 业务复杂后难以维护 业务逻辑清晰,修改影响范围小
学习成本 低,大家熟悉 高,需要理解领域建模
团队沟通 开发说"表",产品说"业务" 统一语言,代码即文档
适用场景 简单CRUD、快速原型 复杂业务、长期维护项目
单元测试 依赖数据库,难测 领域层纯Java,好测

4.2 什么时候用DDD?

  • 业务逻辑复杂,规则经常变化
  • 项目需要长期维护(一年以上)
  • 团队规模较大,需要清晰的边界
  • 微服务架构,需要明确服务边界

4.3 什么时候不用DDD?

这个坑我踩过。给一个内部管理系统硬上DDD,结果领域层就几个getter/setter,完全是过度设计。

  • 简单CRUD,没什么业务逻辑
  • 快速原型,验证想法
  • 一次性脚本、数据迁移工具

五、DDD实战:电商订单系统

5.1 领域建模

java 复制代码
// ==================== 值对象 ====================

public class Money {
    private final BigDecimal amount;
    private final String currency;
    
    public static final Money ZERO = new Money(BigDecimal.ZERO, "CNY");
    
    public Money(BigDecimal amount, String currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负数");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money add(Money other) {
        checkCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money multiply(double factor) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
    }
    
    private void checkCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("币种不一致");
        }
    }
    
    // getters...
}

public class Address {
    private final String province;
    private final String city;
    private final String district;
    private final String detail;
    
    public Address(String province, String city, String district, String detail) {
        this.province = Objects.requireNonNull(province);
        this.city = Objects.requireNonNull(city);
        this.district = district;
        this.detail = Objects.requireNonNull(detail);
    }
    
    // getters...
}

// ==================== 实体 ====================

public class OrderItem {
    private ProductId productId;
    private int quantity;
    private Money unitPrice;
    
    public OrderItem(ProductId productId, int quantity, Money unitPrice) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("数量必须大于0");
        }
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }
    
    public Money getSubTotal() {
        return unitPrice.multiply(quantity);
    }
    
    // getters...
}

// ==================== 聚合根 ====================

public class Order {
    private OrderId orderId;
    private UserId userId;
    private List<OrderItem> items = new ArrayList<>();
    private Address address;
    private OrderStatus status;
    private Money totalAmount;
    private LocalDateTime createTime;
    
    // 私有构造器,只能通过工厂方法创建
    private Order() {}
    
    public static Order create(UserId userId, Address address) {
        Order order = new Order();
        order.orderId = OrderId.generate();
        order.userId = userId;
        order.address = address;
        order.status = OrderStatus.PENDING_PAYMENT;
        order.createTime = LocalDateTime.now();
        return order;
    }
    
    // 业务方法:添加商品项
    public void addItem(Product product, int quantity, Money price) {
        ensureCanModify();
        OrderItem item = new OrderItem(product.getId(), quantity, price);
        items.add(item);
        recalculateTotal();
    }
    
    // 业务方法:支付
    public void pay(Money amount) {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new IllegalStateException("订单状态异常,当前状态:" + status);
        }
        if (!amount.equals(totalAmount)) {
            throw new IllegalArgumentException("支付金额不匹配");
        }
        this.status = OrderStatus.PAID;
        // 发布领域事件
        registerEvent(new OrderPaidEvent(this.orderId, this.totalAmount));
    }
    
    // 业务方法:取消订单
    public void cancel() {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new IllegalStateException("只能取消待支付订单");
        }
        this.status = OrderStatus.CANCELLED;
        registerEvent(new OrderCancelledEvent(this.orderId, this.items));
    }
    
    // 业务方法:发货
    public void ship(String trackingNumber) {
        if (status != OrderStatus.PAID) {
            throw new IllegalStateException("未支付订单不能发货");
        }
        this.status = OrderStatus.SHIPPED;
        registerEvent(new OrderShippedEvent(this.orderId, trackingNumber));
    }
    
    private void ensureCanModify() {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new IllegalStateException("订单已不可修改");
        }
    }
    
    private void recalculateTotal() {
        this.totalAmount = items.stream()
            .map(OrderItem::getSubTotal)
            .reduce(Money.ZERO, Money::add);
    }
    
    private void registerEvent(DomainEvent event) {
        // 事件暂存,事务提交后统一发布
        // 具体实现由基础设施层完成
    }
    
    // getters...
}

5.2 领域事件与仓储

java 复制代码
// ==================== 领域事件 ====================

public interface DomainEvent {
    LocalDateTime getOccurredOn();
}

public class OrderCreatedEvent implements DomainEvent {
    private final OrderId orderId;
    private final List<OrderItem> items;
    private final LocalDateTime occurredOn;
    
    public OrderCreatedEvent(OrderId orderId, List<OrderItem> items) {
        this.orderId = orderId;
        this.items = new ArrayList<>(items);
        this.occurredOn = LocalDateTime.now();
    }
    
    @Override
    public LocalDateTime getOccurredOn() {
        return occurredOn;
    }
    
    // getters...
}

// ==================== 仓储接口(领域层定义) ====================

public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
    List<Order> findByUserId(UserId userId);
}

// ==================== 仓储实现(基础设施层) ====================

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    
    @Autowired
    private OrderMapper orderMapper;  // MyBatis Mapper
    @Autowired
    private OrderItemMapper orderItemMapper;
    
    @Override
    public Order findById(OrderId id) {
        // 从数据库加载并组装聚合
        OrderPO orderPO = orderMapper.selectById(id.getValue());
        List<OrderItemPO> itemPOs = orderItemMapper.selectByOrderId(id.getValue());
        return convertToDomain(orderPO, itemPOs);
    }
    
    @Override
    public void save(Order order) {
        // 保存聚合根和内部实体
        OrderPO orderPO = convertToPO(order);
        orderMapper.insert(orderPO);
        for (OrderItem item : order.getItems()) {
            orderItemMapper.insert(convertToPO(item));
        }
    }
    
    // 转换方法...
}

5.3 应用层编排

java 复制代码
@Service
@Transactional
public class OrderApplicationService {
    
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private DomainEventPublisher eventPublisher;
    
    public OrderId createOrder(CreateOrderCommand command) {
        // 1. 创建聚合根
        Order order = Order.create(
            new UserId(command.getUserId()),
            new Address(command.getProvince(), command.getCity(), 
                       command.getDistrict(), command.getDetail())
        );
        
        // 2. 添加商品项
        for (ItemCommand item : command.getItems()) {
            order.addItem(
                new Product(new ProductId(item.getProductId())),
                item.getQuantity(),
                new Money(item.getPrice(), "CNY")
            );
        }
        
        // 3. 保存聚合
        orderRepository.save(order);
        
        // 4. 发布领域事件(事务提交后)
        eventPublisher.publish(order.getDomainEvents());
        
        return order.getOrderId();
    }
    
    public void payOrder(OrderId orderId, Money amount) {
        Order order = orderRepository.findById(orderId);
        if (order == null) {
            throw new OrderNotFoundException(orderId);
        }
        order.pay(amount);
        orderRepository.save(order);
        eventPublisher.publish(order.getDomainEvents());
    }
}

六、踩坑指南

贫血领域模型

我见过太多"伪DDD"项目,领域层里全是getter/setter,业务逻辑还在Service里。这叫贫血领域模型,是DDD的大忌。判断标准很简单:如果你的Entity里只有getter/setter,没有业务方法,那就是贫血模型。
聚合设计过大

这个坑我踩过。一开始我把订单、订单项、物流、支付全塞到一个聚合里,结果加载一个订单要join七八张表。聚合应该尽量小,只包含真正需要强一致性的对象。
过度设计

一个只有增删改查的后台管理,硬上DDD就是给自己找麻烦。DDD是有成本的,简单项目用传统三层就够了。
领域事件丢失

领域事件必须在事务提交后 发送。如果在事务内发送,消息出去了但事务回滚了,就会导致数据不一致。可以用Spring的@TransactionalEventListener或者事务同步器来解决。


七、问题与解答

Q1:DDD和微服务是什么关系?

A:DDD是微服务划分的理论基础。限界上下文(Bounded Context)通常对应一个微服务。但DDD不等于微服务,单体应用也可以用DDD。反过来,微服务也不一定用DDD,很多微服务就是简单的CRUD服务。

Q2:聚合根和实体有什么区别?

A:所有的聚合根都是实体,但不是所有实体都是聚合根。聚合根是聚合的入口,外部只能通过聚合根访问聚合内部的对象。比如Order是聚合根,OrderItem是实体,你不能直接操作OrderItem,必须通过Order.addItem()。

Q3:值对象一定要不可变吗?

A:理论上值对象应该是不可变的,这样能保证线程安全和数据一致性。实践中如果性能敏感,也可以可变,但要确保不会共享引用导致意外修改。我的建议是:先按不可变写,真有性能瓶颈再优化。


八、面试高频考点汇总

考点1:什么是贫血领域模型和充血领域模型?

答案

  • 贫血模型:实体只有getter/setter,业务逻辑全部在Service层。优点是简单,缺点是业务逻辑分散,难以复用。
  • 充血模型:实体包含业务方法和状态变更逻辑,Service层只做编排。DDD推荐充血模型,业务内聚在领域对象中。

考点2:DDD中的聚合设计原则是什么?

答案

  1. 聚合内强一致性:聚合内的事务必须原子完成
  2. 聚合间最终一致性:通过领域事件异步同步
  3. 聚合尽量小:只包含真正需要一起修改的对象
  4. 聚合根是唯一入口:外部只能通过聚合根访问内部对象

考点3:限界上下文和微服务的关系?

答案

限界上下文是业务边界,微服务是技术边界。通常一个限界上下文对应一个微服务,但不是绝对的。如果两个上下文数据耦合很强、调用频繁,也可以放在同一个服务里。划分微服务时要考虑业务边界,也要考虑团队规模、网络开销等实际因素。

考点4:领域事件和普通事件的区别?

答案

  • 领域事件:由领域层触发,表达业务领域发生的有意义的事情(如OrderCreatedEvent)。是DDD的核心概念,不依赖具体框架。
  • 普通事件:可以是技术事件(如应用启动事件)或其他层的事件。领域事件强调的是"业务含义"。

考点5:DDD四层架构中,领域层为什么不能依赖其他层?

答案

这是依赖倒置原则的体现。领域层包含最核心的业务逻辑,应该独立于框架和技术细节。如果领域层依赖Spring或MyBatis,换框架时就要改领域层代码。通过接口(如Repository)定义契约,由基础设施层实现,领域层只依赖接口,实现技术细节的隔离。


九、模拟面试官提问和参考答案

场景题1:你接手一个遗留项目,Service层有5000行代码,怎么用DDD重构?

参考答案

  1. 先识别领域:和业务方沟通,搞清楚核心业务概念(订单、商品、库存等)
  2. 提取实体:从Service里找出有状态、有标识的对象,封装成Entity
  3. 提取值对象:把地址、金额等无标识的对象抽成ValueObject
  4. 识别聚合:看哪些对象总是一起修改,组成聚合
  5. 拆分应用层:把事务编排、DTO转换移到ApplicationService
  6. 渐进式重构:不要一次性全改,按模块逐个迁移,保证测试覆盖

场景题2:订单和库存应该是同一个聚合还是不同聚合?

参考答案

应该是不同聚合。订单和库存虽然有关联(下单要扣库存),但它们有各自独立的生命周期。库存可能被线下门店、其他渠道修改,不需要和订单强一致。通过领域事件(OrderCreatedEvent → 扣减库存)实现最终一致性,这样两个聚合可以独立扩展。

场景题3:如何防止领域事件丢失?

参考答案

  1. 事务内持久化事件:把事件和领域数据存在同一个事务里(如事件表)
  2. 事务提交后发送 :用@TransactionalEventListener或事务同步器
  3. 消息发送确认:MQ发送失败要重试,消费端要幂等
  4. 事件日志:记录所有事件,便于排查和补偿

场景题4:DDD项目中,一个需求改了,通常要改哪些文件?

参考答案

理想情况下只改领域层的相关实体或领域服务。比如修改订单支付规则,只需要改Order.pay()方法。用户接口层、应用层、基础设施层都不需要动。这就是DDD的价值------业务逻辑内聚在领域层,修改影响范围小。

场景题5:如何向团队推广DDD?

参考答案

  1. 先小范围试点:找一个核心模块实践,做出效果再推广
  2. 统一语言:让产品和开发用同一套术语沟通
  3. 代码评审:重点检查贫血模型、跨聚合直接引用等问题
  4. 不要追求完美:DDD是渐进的过程,允许有过渡期的"不纯粹"
  5. 培训分享:定期组织DDD案例分享,让大家看到好处

十、互动话题

你在项目中用过DDD吗?遇到过最大的困难是什么?是团队不理解、业务方不配合,还是不知道怎么划分聚合?欢迎在评论区聊聊你的经历。


参考资料

领域驱动设计:软件核心复杂性应对之道(Eric Evans著)


如果这篇文章对你有帮助,欢迎点赞收藏。有问题也可以在评论区留言,我会尽力回复。