Java DDD领域驱动设计案例

wen java案例 3

从“大泥球”到“战略地图”:Java团队落地DDD领域驱动设计的实战拆解

目录导读

  1. 为什么你的Java单体项目正在腐烂?——DDD解决的核心痛点
  2. 限界上下文:不是“拆微服务”,而是“划国界”
  3. 战术设计实战:以“订单履约”为例的Entity/ValueObject建模
  4. 聚合设计避坑指南:事务边界与最终一致性的博弈
  5. 应用服务与领域服务:别把业务逻辑写在Controller里
  6. 从代码到架构:DDD与事件风暴的逆向驱动
  7. 常见误区问答:为什么你的DDD用起来像MVC?

为什么你的Java单体项目正在腐烂?

很多Java团队在项目初期图快,把订单、库存、支付逻辑全部塞进一个OrderService里,半年后,OrderService膨胀到8000行,每个方法都带着@Transactional,改一个需求要动五个模块——这就是典型的“大泥球架构”。

Java DDD领域驱动设计案例

DDD(领域驱动设计)的核心不是画UML图,而是重新定义业务边界,它迫使你先回答一个问题:“这个系统到底在解决谁的什么麻烦?” 在Java生态中,DDD特别适合业务复杂度高、规则多变的系统(如电商、金融、医疗),而不是简单CRUD后台。

限界上下文:不是“拆微服务”,而是“划国界”

很多团队把DDD误解为“微服务拆分指南”,结果拆分后到处都是分布式事务。限界上下文(Bounded Context) 的本质是“语言的边界”。

案例:在“商品中心”上下文中,“上架”意味着更改商品状态并通知搜索引擎,但在“订单中心”上下文中,“商品”只是一个快照(名称、价格、SKU),不关心上架逻辑。

在Java实现中,不要跨上下文共享Entity类,每个上下文应有自己的ProductSnapshot(值对象)或ProductSummary,通过防腐层(Anti-Corruption Layer)转换,这样,即使底层表是同一张,但业务语义彻底隔离。

战术设计实战:以“订单履约”为例的建模

假设我们要开发一个订单履约系统,传统写法是:

public void createOrder(OrderDTO dto) {
    // 校验库存
    // 计算价格
    // 扣减库存
    // 插入订单表
    // 发送消息
}

DDD写法是:

// 实体:Order(订单)
public class Order {
    private OrderId id;          // 值对象
    private List<OrderLine> items; // 值对象集合
    private Address shippingAddress; // 值对象
    private OrderStatus status;   // 枚举或状态机
    public Money calculateTotal() { ... } // 领域行为
    public void addItem(ProductSnapshot product, int count) {
        if (product.isOutOfStock()) throw new OutOfStockException();
        this.items.add(new OrderLine(product, count));
    }
}

关键点:业务规则(如“超过10件需拆单”)必须写在Entity内部,而不是放在Service里,这样Order对象在任何时刻都是自洽的,防止无效状态诞生。

聚合设计避坑指南:事务边界与最终一致性的博弈

聚合(Aggregate)是事务一致性的边界,在Java中,最常见错误是聚合过大——把Order和OrderItem都塞进一个聚合,导致每次更新都要锁整张表。

反例Order聚合内包含PaymentShipment,一次支付回调要更新三个表,事务锁冲突严重。

正解Order是独立聚合,Payment是另一个聚合,通过领域事件(如OrderPaidEvent)异步通知履约系统,在Java中,可以用ApplicationEventPublisher(Spring)或@DomainEvents触发事件,实现最终一致性。

应用服务与领域服务:别把业务逻辑写在Controller里

  • 应用服务(ApplicationService):负责协调用例,如参数校验、事务开启、调用仓储,它很薄,没有业务规则。
  • 领域服务(DomainService):处理跨聚合的复杂逻辑,如“计算折扣后价格”需要同时读用户等级和优惠券。

错误示范

// Controller里写业务
@PostMapping("/order")
public void create(@RequestBody OrderRequest req) {
    if (req.getAmount() > 1000) { // 业务规则泄漏到Web层
        ...
    }
}

正确姿势:Controller只负责HTTP序列化,调用OrderApplicationService.createOrder(req),所有状态变更必须经过聚合的公共方法。

从代码到架构:DDD与事件风暴的逆向驱动

落地DDD最难的是开始,推荐事件风暴(Event Storming) 工作坊:贴黄色便签(事件)、红色便签(命令)、蓝色便签(聚合),当团队在讨论“当库存不足时,订单应该进入什么状态?”时,自然会发现隐藏的边界。

在Java代码层面,逆向校验DDD落地是否成功,看三个信号:

  1. Repository接口是否有明确的find/save语义,而不是像通用Dao那样返回List<Map>
  2. 是否能在不打开Service的情况下,仅读聚合代码就能理解业务规则
  3. 是否有防腐层——当你调外部系统(如微信支付)时,是否在接口处做了异常翻译,而不是把HttpResponseException直接抛到领域层。

常见误区问答:为什么你的DDD用起来像MVC?

Q1:DDD一定要用CQRS和事件溯源吗? A:不是,CQRS是解决查询性能优化的手段,不是DDD的必需品,80%的场景只需要一个OrderQueryService(用MyBatis/JPA投影)即可,读写分离并不是必须的。

Q2:聚合根里可以有另一个聚合的Repository吗? A:严格意义上不可以,聚合根只能操作自己的内部对象,如果需要另一个聚合的数据,应该通过DomainService或应用服务传入值对象,创建订单时,应用服务先查库存(另一个聚合),然后调用order.addItem(productSnapshot)

Q3:Entity和ValueObject如何区分? A:看是否拥有“身份标识”。订单ID本身是值对象(因为不可变),而Order实体有生命周期变化(状态流转),再如Money是典型的值对象——price变了,那是一个新Money,不是原Money的属性变了。

Q4:团队水平一般,能否先不搞事件风暴,直接写代码? A:可以,但建议从一个高频变更的业务流程开始重构——比如从“改价格规则“开始,先定义好聚合边界,再写代码,若全项目大面积重写,容易陷入过度设计。


最后记一句话:DDD不是银弹,它救不了代码混乱的懒汉团队,但在Java单体走向崩溃前,它提供了一副清晰的手术刀——前提是你愿意花时间理解业务专家的语言,而不是只盯着SQL表结构。

抱歉,评论功能暂时关闭!