DDD(领域驱动设计)篇三:实战对照——同一个订单业务,两种架构怎么写

DDD(领域驱动设计)篇一:DDD 到底在解决什么问题?

DDD(领域驱动设计)篇二:DDD 和三层架构到底差在哪?

这一篇是全系列的高潮。好消息是:不需要假设,我们用一套完整落地的 DDD 订单域来举例,我们直接看真代码。

你平时排查支付渠道问题,那就选你最有感觉的链路:支付回调 ,外加它牵出的订单状态流转(支付、超时、取消、退款)。

两个主角:

  • 三层版 :patient 模块,OrderController → OrderServiceImpl → OrderMapper;

  • DDD 版 :mpc 中台 obs 模块,OrderBaseSalesRuntimeContext → OrderBasedSalesEntity + 状态机 + Handler。

第一站:三层版现状

OrderServiceImpl.createOrder 的"五合一"上一篇已经解剖过了(业务校验、远程调用、业务分支、Redisson 锁、重试策略)。这里只补一刀:这套写法下,"订单状态"是一个谁都能改的字段 。支付回调改它、定时任务改它、客服接口也改它,每一处都是一句 order.setStatus(...)。想问"订单到底有哪些状态、什么时候能变",答案散在十几个文件里。

第二站:DDD 版------同样的业务拆成四个角色

角色一:状态机描述符------先立"交通规则"

OrderDescriptorLoader.java 用纯声明的方式写清了订单领域的全部规则:8 个状态、10 个事件、每条"状态+事件→新状态"的转移:

java 复制代码
domainDescriptor.description("购买快速问诊订单领域")
        .type(Constants.ORDER_DOMAIN_DESCRIPTOR)
        .label("purchase_quickmed_service")
        .version("v1");
​
// 注册状态机:支付中 + 支付成功事件 → 服务中
domainDescriptor.registerStateMachine(paying, paymentSuccess, running);
// 支付失败 / 支付超时,回到待支付
domainDescriptor.registerStateMachine(paying, paymentFail, nothing);
domainDescriptor.registerStateMachine(paying, payingTimeout, nothing);
// 每条转移绑定干活的 Handler
domainDescriptor.registerEventTrigger(paying, paymentSuccess, "salesOrderPaymentSuccHandler");

整个订单生命周期长这样(真实状态名就用的 nothing/paying/running/...):

主流程是支付链路,支线是退款链路;注意退款失败、部分退款成功都会退回"服务中",而不是停在退款中。

这份描述符就是订单领域的"宪法":所有合法的状态流转全在这里,别处不许私改状态。

角色二:领域实体------规则长在订单身上

OrderBasedSalesEntity.java 继承框架的 GenericDomain<SalesOrder>,自带业务判断:

java 复制代码
// 订单自己知道"我能不能被支付"
public Result<String> isOrderPayable() {
    if (UNPAID != getValueObject().getPaymentStatus()) {
        return Result.error("订单状态不是未支付,不可重复支付。状态:" + ...);
    }
    return Result.success();
}
// 订单自己会算"我已经退了多少钱"
public BigDecimal totalRefundAmount() { ... }
// 订单自己知道"我的分布式锁 key 长什么样"
public String getOrderLockKey() { ... }
角色三:应用层编排------领班只安排,不做饭

支付回调进来,OrderBaseSalesRuntimeContext.paymentNotify 的骨架:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public Result<PaymentNotifyDto> paymentNotify(PaymentResultEventValueObject param) {
    OrderPayment payment = orderPaymentRepo.getById(param.getPaymentId());
    // 幂等判断:已成功的回调直接返回(幂等管的是回调本身,不是订单状态)
    if (PaymentSucceedEnum.YES == payment.getSucceed()) {
        return Result.success();
    }
    OrderBasedSalesEntity orderEntity = orderEntityRepo.findOrder(orderId, ...).get();
    // 锁交给适配器
    return iDomainLockAdapter.logicWithLock(orderEntity.getOrderLockKey(), () -> {
        // 按渠道回调结果选事件------注意:选事件,不是改状态
        EventModel event = param.getPaymentResult()
            ? EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.payment_success)
            : EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.payment_fail);
        event.setValueObject(JSONUtil.parseObj(param));
        orderEntity.onEvent(event);   // ← 整个业务就这一行
        ...
    });
}

应用层干的事:查单、幂等判断、加锁、选事件、触发。它不写任何"状态从 X 变 Y"的规则。

角色四:基础设施适配器------技术细节隔离在墙外

分布式锁不在业务代码里手写 Redisson,而是走 IDomainLockAdapter 接口,实现里统一封装"重试 6 次抢锁、持有 5 秒、释放前校验 isHeldByCurrentThread"。你排查过锁超时问题,应该一眼看出这个封装的价值:所有加锁业务共享同一套经过验证的锁策略,不用每个 Service 各写各的 tryLock,也不会再出现"忘了校验持锁线程就 unlock 抛 IllegalMonitorStateException"的老坑。

串起来:onEvent 的六步流水线

orderEntity.onEvent(event) 这一行背后,框架(GenericDomain.java)替你跑了一条标准流水线。六步里最值钱的是第 ① 步------非法的状态+事件组合,根本走不到业务代码:

第三站:直观对比------加一个新事件要动几处?

场景:产品说"医生可以驳回订单,驳回后进'已驳回'状态"。两边的动作清单差别就在这:

三层版靠"程序员的记性"保证一致性,DDD 版靠"结构"保证一致性------非法状态下的驳回根本进不来,verify 这关就过不去,测试只需要覆盖合法路径。

DDD 版加"驳回"的实际动作就两步:

java 复制代码
domainDescriptor.registerStateMachine(running, doctorReject, rejected);
domainDescriptor.registerEventTrigger(running, doctorReject, "salesOrderDoctorRejectHandler");

再写一个 salesOrderDoctorRejectHandler,只写"驳回后做什么"。完事。

迁移策略:千万别推倒重来

mpc 的做法值得抄作业:

  1. 不动老代码。patient 的三层服务照常跑,没有为了 DDD 而重写;

  2. 新建领域模块 。领域骨架放在独立子模块(chinaunicom-medical-ihm-domain),和业务模块物理隔离;

  3. 从最痛的业务开始。挑状态流转最复杂、规则最常变的(订单/支付)先行试点,验证收益后再推广;

  4. 新旧共存。两套架构在同一工程里各司其职,按业务复杂度选型,不搞一刀切。

这种"在新地盘上建新房子,老房子住到自然退役"的策略,业内叫绞杀者模式(Strangler Pattern)------名字吓人,思路很朴实。

DDD(领域驱动设计)篇四:退费 + 支付,三层 vs DDD 各撸一遍

相关推荐
wangchunyu11411 小时前
JAVA CompletableFuture异步编程实战
java·开发语言·python
paopaokaka_luck12 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
Thomas.Sir12 小时前
第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收
java·spring boot
程序 代码狂人12 小时前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
2501_9445215913 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
天河归来13 小时前
从原始消息到业务数据:三层数据架构的设计与实践
数据库·数据仓库·架构
天远Date Lab14 小时前
零信任架构实战:基于天远企业年报信息核验构建自动化商业保理财务与社保穿透评估网关
人工智能·架构·自动化
EnCi Zheng14 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
救救孩子把14 小时前
LangGraph 工程化架构:类型、状态、流式通信与可观测性
架构·agent·trace·opentelemetry·langgraph·graphrag