大家好,我是冰河~~
凌晨三点,会议室灯火通明,产品经理指着屏幕上的流程图,唾沫横飞:“为什么用户取消订单后,优惠券没自动退回?”
你低头看了看自己写的代码——订单模块调用了支付模块,支付模块又调用了优惠券模块,优惠券模块还调用了用户模块。这四个模块像俄罗斯套娃一样互相嵌套,改一行代码要评估八个地方的影响。
“我……我查查。”你声音发虚,心里清楚:这代码连自己都看不懂,更别说解释了。
如果你也曾在这种“代码迷宫”里迷失方向,明明只是改个简单业务,却要翻遍十几个Service类;如果你也曾对着需求文档发呆,不知道那些“会员等级规则”、“积分兑换策略”该加到哪个类里……
那么恭喜你,今天这篇文章就是为你准备的。今天我们就一起聊聊的DDD(领域驱动设计)
一、为何你的代码总是一团乱麻?
先别急着学什么“限界上下文”、“聚合根”,咱们先解决一个更根本的问题:为什么传统写法总会把代码越写越乱?
1.1 场景1:业务逻辑的“捉迷藏”游戏
需求:“用户下单后,如果24小时未支付,自动取消订单。”
传统写法:
// OrderService.java - 第53行publicvoidcreateOrder(OrderDTO dto){// 创建订单逻辑... orderDao.save(order);// 顺便启动个定时任务? timer.schedule(new CancelTask(order.getId()), 24, TimeUnit.HOURS);}// 三个月后,另一个需求来了...// PaymentService.java - 第128行 publicvoidprocessPayment(PaymentDTO dto){// 支付成功逻辑...// 等等,是不是要取消那个定时任务? timer.cancel(order.getId()); // 如果还能找到的话}// 六个月后,第三个程序员接需求// 他要在“订单超时”时发短信提醒// 现在他开始全项目搜索:到底在哪设置的超时??
发现问题了吗?一个完整的业务规则(订单超时处理),被拆得七零八落,散落在项目的各个角落。 新来的同事想改逻辑?这能顺利改吗?能找到所有相关代码算你赢。
1.2 场景2:数据库表的“暴政”
更常见的是这种“数据库驱动开发”:
// 看着数据库表设计代码// user表:id, name, email, phone, vip_level, points...// 于是写出这样的“领域模型”publicclassUser{private Long id;private String name;private String email;// ... 20个getter/setter// 等等,业务逻辑呢?// 用户升级VIP的逻辑在哪?用户消费积分的逻辑在哪?// 哦,在UserService里,800行的一个类里}
数据库表结构成了代码的“紧箍咒”,业务逻辑被迫适应表结构,而不是表结构服务于业务逻辑。
1.3 场景3:团队协作的“鸡同鸭讲”
更可怕的是沟通成本。看看这段对话:
后端A(订单模块):“好的,我下单时调用你的库存接口。”
后端B(库存模块):“等等,什么叫‘锁定’?是减库存还是预留?”
后端C(支付模块):“那我支付成功要确认扣减哦。”
不同的模块对同一个业务概念有不同的理解,这种认知偏差在代码里埋下了无数定时炸弹。
二、DDD术语
好了,吐槽完毕。现在来看看DDD怎么解决这些问题。别怕那些术语,我给你准备了一份“通俗解释版”。
2.1 领域(Domain)
通俗解释:就是你负责的那摊子事儿。
比如你在做电商系统:
- 商品团队负责 “商品领域”:管上架、下架、库存、价格
- 订单团队负责 “订单领域”:管下单、支付、发货、退款
- 用户团队负责 “用户领域”:管注册、登录、资料、会员等级
每个领域都有自己的“行话”和“规矩”,DDD的第一步就是承认这些差异,而不是强行统一。
2.2 限界上下文(Bounded Context)
通俗解释:给每个领域画个圈,圈内自己说了算。
这是DDD最核心、也最被神话的概念。其实很简单:
// 【商品上下文】里的“商品”publicclassProduct{private Long id;private String name;private BigDecimal price;private Integer stock; // 实时库存// 商品上下文关心:上下架、改价格、扣库存publicvoiddeductStock(Integer quantity){if (this.stock < quantity) {thrownew ProductException("库存不足"); }this.stock -= quantity; }}// 【订单上下文】里的“商品快照”publicclassOrderItem{private Long productId;private String productName;private BigDecimal snapshotPrice; // 下单时的价格快照private Integer quantity;// 订单上下文关心:买了什么、多少钱、买了几件// 不关心实时库存!那是商品上下文的事儿}
看到区别了吗?同一个词(“商品”)在不同上下文里有不同含义。限界上下文就是承认这种差异,并规定:“在我的地盘,我说了算。”
2.3 领域模型(Domain Model)
通俗解释:把业务规则写进对象里,而不是散落在Service里。
对比一下两种写法:
传统写法(贫血模型):
// 数据容器(贫血)publicclassOrder{private Long id;private BigDecimal amount;private String status; // "UNPAID", "PAID", "CANCELLED"// 只有getter/setter}// 业务逻辑全在这里(肿了)publicclassOrderService{publicvoidpayOrder(Long orderId, BigDecimal payAmount){ Order order = orderDao.findById(orderId);// 业务规则判断if (!"UNPAID".equals(order.getStatus())) {thrownew RuntimeException("订单不是未支付状态"); }if (!order.getAmount().equals(payAmount)) {thrownew RuntimeException("金额不匹配"); }// 更新状态 order.setStatus("PAID"); orderDao.update(order);// 触发其他操作 inventoryService.deductStock(order.getItems()); pointService.addPoints(order.getUserId(), order.getAmount());// ... 还有五六个Service要调 }}
DDD写法(充血模型):
// 领域模型(充血,有行为)publicclassOrder{private Long id;private BigDecimal amount;private OrderStatus status; // 枚举,更安全private List<OrderItem> items;// 核心:业务行为封装在模型内部publicvoidpay(BigDecimal payAmount, Payment payment){// 业务规则判断if (!this.status.canPay()) {thrownew OrderDomainException( String.format("订单[%d]当前状态[%s]不允许支付", id, status) ); }if (!this.amount.equals(payAmount)) {thrownew OrderDomainException("支付金额与订单金额不匹配"); }// 状态转换this.status = OrderStatus.PAID;// 发布领域事件(告诉外界:我支付成功了) DomainEvents.publish(new OrderPaidEvent(this.id,this.amount, payment.getId(),this.items )); }// 更多业务行为publicvoidcancel(){ /* 取消逻辑 */ }publicvoidapplyRefund(){ /* 申请退款逻辑 */ }}// Service变薄了,只做协调publicclassOrderApplicationService{publicvoidpayOrder(PayOrderCommand command){ Order order = orderRepository.findById(command.getOrderId()); Payment payment = paymentService.createPayment(order.getAmount());// 调用领域模型的行为 order.pay(command.getPayAmount(), payment);// 保存结果 orderRepository.save(order); }}
关键区别:在DDD里,Order不是一个哑巴数据容器,而是一个有智能的业务对象。它知道自己什么时候能支付、什么时候能取消,业务规则被封装在对象内部。
2.4 聚合根(Aggregate Root)
通俗解释:在一堆相关的对象里,选个“家长”,外人只能跟家长打交道。
举个例子:订单和订单项。
// 聚合根:OrderpublicclassOrder{private Long id;private List<OrderItem> items; // 内部对象// 添加商品:只能通过聚合根的方法publicvoidaddItem(ProductSnapshot product, Integer quantity){// 业务规则:不能重复添加相同商品if (items.stream().anyMatch(item -> item.getProductId().equals(product.getId()))) {thrownew OrderDomainException("商品已存在于订单中"); } items.add(new OrderItem(product, quantity));this.calculateTotalAmount(); // 重新计算总金额 }// 移除商品publicvoidremoveItem(Long productId){// 业务规则:至少保留一个商品if (items.size() <= 1) {thrownew OrderDomainException("订单至少需要包含一个商品"); } items.removeIf(item -> item.getProductId().equals(productId));this.calculateTotalAmount(); }// 外部只能通过聚合根访问内部对象public List<OrderItem> getItems(){return Collections.unmodifiableList(items); // 返回不可变列表 }}// 内部对象:OrderItempublicclassOrderItem{private Long productId;private String productName;private BigDecimal price;private Integer quantity;// 没有public的setter!只能通过Order聚合根来修改voidupdateQuantity(Integer quantity){this.quantity = quantity; }}
为什么需要聚合根?
- 简化访问:外部只需操作
Order,不用管OrderItem的死活 - 明确边界:清晰的告诉你:“从这里开始,是订单的地盘”
2.5 领域事件(Domain Event)
通俗解释:有事发个朋友圈,谁感兴趣谁自己看。
传统写法的问题:
publicvoidpayOrder(){// 支付逻辑...// 通知库存扣减 inventoryService.deduct(order.getItems());// 通知积分增加 pointService.add(order.getUserId(), order.getAmount());// 通知物流系统 logisticsService.create(order);// 通知客服系统 customerService.notify(order);// 三个月后要加个“发送短信”// 你得回来改这里,加一行 smsService.send(order.getUserPhone(), "支付成功");}
每加一个新功能,就要回来改旧代码,这违反了“开闭原则”。
DDD的解决方案:领域事件
publicclassOrder{publicvoidpay(Payment payment){// 支付逻辑...this.status = OrderStatus.PAID;// 发个“朋友圈” DomainEvents.publish(new OrderPaidEvent(this.id,this.userId,this.amount,this.items )); }}// 谁感兴趣谁监听@ComponentpublicclassOrderPaidListener{@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){// 库存模块:扣库存 inventoryService.deduct(event.getItems()); }}@ComponentpublicclassPointListener{@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){// 积分模块:加积分 pointService.add(event.getUserId(), event.getAmount()); }}// 三个月后要加短信通知?// 简单,再写个监听器就行,不用改Order的代码!@ComponentpublicclassSmsListener{@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){// 发短信 smsService.send(event.getUserPhone(), "支付成功"); }}
就像微信朋友圈:你发一条状态,感兴趣的朋友自己会看、会点赞、会评论。你不需要一个个私聊通知。
三、实战:用DDD重写“订单支付”
光说不练假把式,我们用一个完整例子展示DDD如何落地。
3.1 划分限界上下文
根据业务职责,我们划分出:
3.2 设计领域模型(订单上下文为例)
// 值对象:地址(不可变,没有唯一标识)public record Address( String province, String city, String district, String detail){public String getFullAddress(){return String.format("%s%s%s%s", province, city, district, detail); }}// 值对象:金额(封装计算逻辑)public record Money(BigDecimal amount, String currency){public Money add(Money other){if (!this.currency.equals(other.currency)) {thrownew IllegalArgumentException("币种不同"); }returnnew Money(this.amount.add(other.amount), currency); }public Money multiply(Integer times){returnnew Money(amount.multiply(new BigDecimal(times)), currency); }}// 枚举:订单状态(封装状态流转规则)publicenum OrderStatus { CREATED("已创建") {@OverridepublicbooleancanPay(){ returntrue; }@OverridepublicbooleancanCancel(){ returntrue; } }, PAID("已支付") {@OverridepublicbooleancanPay(){ returnfalse; }@OverridepublicbooleancanCancel(){ returnfalse; }@OverridepublicbooleancanShip(){ returntrue; } },// ... 其他状态privatefinal String desc;// 抽象方法:强制每个状态定义自己能做什么publicabstractbooleancanPay();publicabstractbooleancanCancel();publicabstractbooleancanShip()default{ returnfalse; };}// 实体:订单项publicclassOrderItem{privatefinal Long productId;privatefinal String productName;privatefinal Money price; // 下单时的价格快照privatefinal Integer quantity;public Money getSubtotal(){return price.multiply(quantity); }}// 聚合根:订单(核心!)publicclassOrder{private Long id;private Long userId;private OrderStatus status;private Address shippingAddress;private Money totalAmount;private List<OrderItem> items = new ArrayList<>();private LocalDateTime createdAt;private LocalDateTime paidAt;// 工厂方法:创建订单publicstatic Order create(Long userId, List<OrderItem> items, Address address){ Order order = new Order(); order.id = IdGenerator.nextId(); order.userId = userId; order.items = new ArrayList<>(items); order.shippingAddress = address; order.status = OrderStatus.CREATED; order.createdAt = LocalDateTime.now();// 计算总金额 order.totalAmount = items.stream() .map(OrderItem::getSubtotal) .reduce(Money.ofZero(), Money::add);// 发布领域事件 DomainEvents.publish(new OrderCreatedEvent(order.id, order.userId, order.items));return order; }// 核心业务行为:支付publicvoidpay(Payment payment){// 守卫条件if (!status.canPay()) {thrownew OrderDomainException( String.format("订单[%d]当前状态[%s]不允许支付", id, status) ); }// 金额校验if (!totalAmount.equals(payment.getAmount())) {thrownew OrderDomainException("支付金额与订单金额不匹配"); }// 状态转换this.status = OrderStatus.PAID;this.paidAt = LocalDateTime.now();// 发布事件 DomainEvents.publish(new OrderPaidEvent(this.id,this.userId,this.totalAmount,this.items )); }// 核心业务行为:取消publicvoidcancel(String reason){if (!status.canCancel()) {thrownew OrderDomainException( String.format("订单[%d]当前状态[%s]不允许取消", id, status) ); }this.status = OrderStatus.CANCELLED;// 发布事件 DomainEvents.publish(new OrderCancelledEvent(this.id,this.userId,this.items, reason )); }// 查询方法publicbooleanisPaid(){return status == OrderStatus.PAID; }publicbooleancontainsProduct(Long productId){return items.stream().anyMatch(item -> item.getProductId().equals(productId)); }// 注意:没有public的setter!// 所有状态变更都通过业务方法完成}
3.3 实现仓储(Repository)
核心代码如下:
// 仓储接口:面向领域模型,不是面向数据库表publicinterfaceOrderRepository{// 查询方法Optional<Order> findById(Long id);Optional<Order> findByOrderNo(String orderNo);List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);// 保存:新增或更新voidsave(Order order);// 删除voiddelete(Long id);// 复杂查询:直接返回领域模型,而不是DTOList<Order> findUnpaidOrdersOlderThan(LocalDateTime time);}// 实现:负责领域模型与数据库的转换@Repository@RequiredArgsConstructorpublicclassOrderRepositoryImplimplementsOrderRepository{privatefinal OrderJpaRepository jpaRepository;privatefinal OrderItemJpaRepository itemJpaRepository;@Override@Transactional(readOnly = true)public Optional<Order> findById(Long id){return jpaRepository.findById(id) .map(this::toDomain); }private Order toDomain(OrderEntity entity){// 转换数据库实体为领域模型 List<OrderItem> items = itemJpaRepository.findByOrderId(entity.getId()) .stream() .map(itemEntity -> new OrderItem( itemEntity.getProductId(), itemEntity.getProductName(),new Money(itemEntity.getPrice(), "CNY"), itemEntity.getQuantity() )) .toList();return Order.builder() .id(entity.getId()) .userId(entity.getUserId()) .status(OrderStatus.valueOf(entity.getStatus())) .totalAmount(new Money(entity.getTotalAmount(), "CNY")) .items(items) .createdAt(entity.getCreatedAt()) .paidAt(entity.getPaidAt()) .build(); }@Override@Transactionalpublicvoidsave(Order order){// 保存聚合根和所有子对象 OrderEntity entity = toEntity(order); jpaRepository.save(entity);// 保存订单项 itemJpaRepository.deleteByOrderId(order.getId()); List<OrderItemEntity> itemEntities = order.getItems().stream() .map(item -> toItemEntity(item, order.getId())) .toList(); itemJpaRepository.saveAll(itemEntities); }}
3.4 编写应用服务
// 应用服务:薄薄的一层,只做协调@Service@RequiredArgsConstructorpublicclassOrderApplicationService{privatefinal OrderRepository orderRepository;privatefinal PaymentClient paymentClient; // 外部服务客户端privatefinal ProductClient productClient;// 命令:创建订单@Transactionalpublic OrderResult createOrder(CreateOrderCommand command){// 1. 校验商品信息(调用商品上下文) List<ProductInfo> products = productClient.getProductsByIds( command.getItemIds() );// 2. 构建订单项 List<OrderItem> items = command.getItems().stream() .map(item -> { ProductInfo product = products.stream() .filter(p -> p.getId().equals(item.getProductId())) .findFirst() .orElseThrow(() -> new ProductNotFoundException(item.getProductId()));returnnew OrderItem( product.getId(), product.getName(),new Money(product.getPrice(), "CNY"), item.getQuantity() ); }) .toList();// 3. 创建领域模型 Order order = Order.create( command.getUserId(), items, command.getAddress() );// 4. 保存 orderRepository.save(order);// 5. 返回DTOreturn OrderResult.from(order); }// 命令:支付订单@Transactionalpublic PaymentResult payOrder(PayOrderCommand command){// 1. 获取订单 Order order = orderRepository.findById(command.getOrderId()) .orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));// 2. 调用支付服务(支付上下文) Payment payment = paymentClient.createPayment(new CreatePaymentRequest( order.getId(), order.getTotalAmount(), command.getPaymentMethod() ) );// 3. 调用领域模型的行为 order.pay(payment);// 4. 保存状态变更 orderRepository.save(order);returnnew PaymentResult(payment.getId(), payment.getStatus()); }}
3.5 处理领域事件
// 事件:订单已支付public record OrderPaidEvent( Long orderId, Long userId, Money amount, List<OrderItem> items){}// 监听器1:扣减库存@Component@RequiredArgsConstructorpublicclassInventoryHandler{privatefinal InventoryService inventoryService;@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){// 批量扣减库存 inventoryService.batchDeduct( event.items().stream() .map(item -> new InventoryDeductCommand( item.productId(), item.quantity() )) .toList() ); }}// 监听器2:增加积分@Component@RequiredArgsConstructorpublicclassPointHandler{privatefinal PointService pointService;@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){// 计算积分(100元=1积分)int points = event.amount().amount() .divide(new BigDecimal(100), RoundingMode.DOWN) .intValue(); pointService.addPoints(event.userId(), points, "订单支付"); }}// 监听器3:通知物流@Component@RequiredArgsConstructorpublicclassLogisticsHandler{privatefinal LogisticsService logisticsService;@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){ logisticsService.createShipment(new CreateShipmentCommand( event.orderId(), event.userId(), event.items() ) ); }}
四、DDD四大填坑避雷
4.1 过度设计
错误示范:一个用户管理系统,硬拆成“用户上下文”、“资料上下文”、“权限上下文”,三个微服务之间用事件总线通信,就为了改个用户头像。
正确姿势:DDD不是银弹!判断标准:
- 团队10-30人,业务中等复杂度 → 单体应用内用DDD思想组织代码
4.2 未完全落实DDD
错误示范:
// 换汤不换药public class Order { private Long id; private String status; // ... getter/setter 一大堆 // 业务逻辑呢?哦,还是在Service里}
正确姿势:真正的DDD,业务逻辑在领域模型里!Service应该很薄,只做协调。
4.3 和业务人员对话不同频
错误示范:程序员自己对着需求文档脑补,设计出一套“完美”但业务人员看不懂的模型。
正确姿势:组织“领域研讨会”,拉着产品、运营、业务专家一起,用他们的语言讨论:
把这些业务规则直接翻译成代码。
4.4 事件乱飞,大量消息堆积
错误示范:
// 什么都发事件order.addItem(item); // 发布OrderItemAddedEventorder.updateAddress(addr); // 发布OrderAddressUpdatedEvent order.calculateAmount(); // 发布OrderAmountCalculatedEvent// ... 一个简单操作发10个事件
正确姿势:事件只用于跨上下文、异步处理的重要业务变更。一个经验法则:如果监听器需要修改本上下文的数据,那可能不该用事件。
五、总结
其实DDD理解起来一点都不难,它的核心思想就两点:
(1)让代码反映业务,而不是数据库
传统开发:数据库表 → Entity → Service → Controller(技术驱动) DDD开发:业务概念 → 领域模型 → 仓储/服务 → 接口(业务驱动)
(2) 统一语言,让技术和业务说同一种话
当产品经理说“订单支付成功后要扣库存”,你应该能直接在代码里找到:
// Order.pay() 方法里DomainEvents.publish(new OrderPaidEvent(...));// 库存监听器里@EventListenerpublicvoidhandleOrderPaid(OrderPaidEvent event){ inventoryService.deduct(event.getItems());}
业务人员能看懂,技术人员能实现,这就是DDD最大的价值。
最后的小建议
不要试图一次性把整个系统改成DDD。从你最痛的那个模块开始:
现在,回头看看你那团乱麻的代码,是不是觉得有点思路了?从今天开始,试着用业务的眼光看待代码,而不仅仅是技术的角度。你会发现,写代码也可以是一件很有逻辑、很优雅的事情。
六、写在最后
在冰河技术知识星球,《AI智能客服系统》已完结,一站式AI智能平台项目已开启,还有其他二十几个项目,像AI智能问答系统、实战AI大模型、手写高性能敏组件、手写线程池、手写高性能SQL引擎、手写高性能Polaris网关、手写高性能熔断组件、手写通用指标上报组件、手写高性能数据库路由组件、手写分布式IM即时通讯系统、手写Seckill分布式秒杀系统、手写高性能RPC、实战高并发设计模式、简易商城系统等等。
这些项目的需求、方案、架构、落地等均来自互联网真实业务场景,让你真正学到互联网大厂的业务与技术落地方案,并将其有效转化为自己的知识储备。
值得一提的是:冰河自研的Polaris高性能网关比某些开源网关项目性能更高,目前正在热更AI智能客服项目,也正在实现MCP,全程带你分析原理和手撸代码。
你还在等啥?不少小伙伴经过星球硬核技术和项目的历练,早已成功跳槽加薪,实现薪资翻倍,而你,还在原地踏步,抱怨大环境不好。抛弃焦虑和抱怨,我们一起塌下心来沉淀硬核技术和项目,让自己的薪资更上一层楼。
🚀PS:星球原价299,目前已开通最大优惠:长按或扫码加入星球立减200

🚀PS:另外星球开通了续期优惠券,可与续期5折优惠叠加使用,长按或扫码续费星球,可在5折基础上再享立减优惠。 🚀

目前,领券加入星球就可以跟冰河一起学习《一站式AI智能平台》、《AI智能客服系统》、《AI智能问答系统》、《实战AI大模型》、《手写高性能Redis组件》、《手写高性能脱敏组件》、《手写线程池》、《手写高性能SQL引擎》、《手写高性能Polaris网关》、《手写高性能RPC项目》、《分布式Seckill秒杀系统》、《分布式IM即时通讯系统》《手写高性能通用熔断组件项目》、《手写高性能通用监控指标上报组件》、《手写高性能数据库路由组件》、《手写简易商城脚手架项目》、《Spring6核心技术与源码解析》和《实战高并发设计模式》,从零开始介绍原理、设计架构、手撸代码。
花很少的钱就能学这么多硬核技术、中间件项目和大厂秒杀系统、分布式IM即时通讯系统,AI大模型项目,比其他培训机构不知便宜多少倍,硬核多少倍,如果是我,我会买他个十年!
加入要趁早,后续还会随着项目和加入的人数涨价,而且只会涨,不会降,先加入的小伙伴就是赚到。
另外,还有一个限时福利,邀请一个小伙伴加入,冰河就会给一笔 分享有奖 ,有些小伙伴都邀请了50+人,早就回本了!
七、其他方式加入星球
- 链接 :打开链接 http://m6z.cn/6aeFbs 加入星球。
- 回复 :在公众号 冰河技术 回复 星球 领取优惠券加入星球。
特别提醒: 苹果用户进圈或续费,请加微信 hacker_binghe 扫二维码,或者去公众号 冰河技术 回复 星球 扫二维码加入星球。
好了,今天就到这儿吧,我是冰河,我们下期见~~