凌晨三点,会议室灯火通明。产品经理指着屏幕上的流程图,唾沫横飞:“为什么用户取消订单后,优惠券没自动退回?”
你低头看了看自己写的代码——订单模块调用了支付模块,支付模块又调用了优惠券模块,优惠券模块还调用了用户模块。这四个模块像俄罗斯套娃一样互相嵌套,改一行代码要评估八个地方的影响。
“我……我查查。”你声音发虚,心里清楚:这代码连自己都看不懂,更别说解释了。
如果你也曾在这种“代码迷宫”里迷失方向,明明只是改个简单业务,却要翻遍十几个Service类;如果你也曾对着需求文档发呆,不知道那些“会员等级规则”、“积分兑换策略”该加到哪个类里……
那么,今天我们就一起聊聊DDD(领域驱动设计),看看如何从业务逻辑的混乱中,重构出一套清晰的代码。
一、为何你的代码总是一团乱麻?
先别急着学“限界上下文”或“聚合根”,咱们先解决一个更根本的问题:为什么传统写法总会把代码越写越乱?
1.1 场景1:业务逻辑的“捉迷藏”游戏
需求:“用户下单后,如果24小时未支付,自动取消订单。”
传统写法:
// OrderService.java - 第53行
public void createOrder(OrderDTO dto) {
// 创建订单逻辑...
orderDao.save(order);
// 顺便启动个定时任务?
timer.schedule(new CancelTask(order.getId()), 24, TimeUnit.HOURS);
}
// 三个月后,另一个需求来了...
// PaymentService.java - 第128行
public void processPayment(PaymentDTO dto) {
// 支付成功逻辑...
// 等等,是不是要取消那个定时任务?
timer.cancel(order.getId()); // 如果还能找到的话
}
// 六个月后,第三个程序员接需求
// 他要在“订单超时”时发短信提醒
// 现在他开始全项目搜索:到底在哪设置的超时??
发现问题了吗?一个完整的业务规则(订单超时处理),被拆得七零八落,散落在项目的各个角落。 新来的同事想改逻辑?能找到所有相关代码算你赢。
1.2 场景2:数据库表的“暴政”
更常见的是这种“数据库驱动开发”:
// 看着数据库表设计代码
// user表:id, name, email, phone, vip_level, points...
// 于是写出这样的“领域模型”
public class User {
private Long id;
private String name;
private String email;
// ... 20个getter/setter
// 等等,业务逻辑呢?
// 用户升级VIP的逻辑在哪?用户消费积分的逻辑在哪?
// 哦,在UserService里,800行的一个类里
}
数据库表结构成了代码的“紧箍咒”,业务逻辑被迫适应表结构,而不是表结构服务于业务逻辑。
1.3 场景3:团队协作的“鸡同鸭讲”
更可怕的是沟通成本。看看这段对话:
- 产品经理:“用户下单后,库存要立即锁定。”
- 后端A(订单模块):“好的,我下单时调用你的库存接口。”
- 后端B(库存模块):“等等,什么叫‘锁定’?是减库存还是预留?”
- 后端A:“就是不能让别人买了。”
- 后端B:“那超时了要释放吗?”
- 产品经理:“当然要啊,30分钟未支付就释放。”
- 后端C(支付模块):“那我支付成功要确认扣减哦。”
- ...(一小时后)
- 测试:“为什么用户支付后库存又超卖了?!”
不同的模块对同一个业务概念有不同的理解,这种认知偏差在代码里埋下了无数定时炸弹。
二、DDD术语:别怕,其实很简单
好了,吐槽完毕。现在来看看DDD怎么解决这些问题。别怕那些术语,我给你准备了一份“通俗解释版”。
2.1 领域(Domain)
通俗解释:就是你负责的那摊子事儿。
比如你在做电商系统:
- 商品团队负责 “商品领域”:管上架、下架、库存、价格
- 订单团队负责 “订单领域”:管下单、支付、发货、退款
- 用户团队负责 “用户领域”:管注册、登录、资料、会员等级
每个领域都有自己的“行话”和“规矩”,DDD的第一步就是承认这些差异,而不是强行统一。
2.2 限界上下文(Bounded Context)
通俗解释:给每个领域画个圈,圈内自己说了算。
这是DDD最核心、也最被神话的概念。其实很简单:
// 【商品上下文】里的“商品”
public class Product {
private Long id;
private String name;
private BigDecimal price;
private Integer stock; // 实时库存
// 商品上下文关心:上下架、改价格、扣库存
public void deductStock(Integer quantity) {
if (this.stock < quantity) {
throw new ProductException("库存不足");
}
this.stock -= quantity;
}
}
// 【订单上下文】里的“商品快照”
public class OrderItem {
private Long productId;
private String productName;
private BigDecimal snapshotPrice; // 下单时的价格快照
private Integer quantity;
// 订单上下文关心:买了什么、多少钱、买了几件
// 不关心实时库存!那是商品上下文的事儿
}
看到区别了吗?同一个词(“商品”)在不同上下文里有不同含义。 限界上下文就是承认这种差异,并规定:“在我的地盘,我说了算。”
2.3 领域模型(Domain Model)
通俗解释:把业务规则写进对象里,而不是散落在Service里。
对比一下两种写法:
传统写法(贫血模型):
// 数据容器(贫血)
public class Order {
private Long id;
private BigDecimal amount;
private String status; // "UNPAID", "PAID", "CANCELLED"
// 只有getter/setter
}
// 业务逻辑全在这里(肿了)
public class OrderService {
public void payOrder(Long orderId, BigDecimal payAmount) {
Order order = orderDao.findById(orderId);
// 业务规则判断
if (!"UNPAID".equals(order.getStatus())) {
throw new RuntimeException("订单不是未支付状态");
}
if (!order.getAmount().equals(payAmount)) {
throw new RuntimeException("金额不匹配");
}
// 更新状态
order.setStatus("PAID");
orderDao.update(order);
// 触发其他操作
inventoryService.deductStock(order.getItems());
pointService.addPoints(order.getUserId(), order.getAmount());
// ... 还有五六个Service要调
}
}
DDD写法(充血模型):
// 领域模型(充血,有行为)
public class Order {
private Long id;
private BigDecimal amount;
private OrderStatus status; // 枚举,更安全
private List<OrderItem> items;
// 核心:业务行为封装在模型内部
public void pay(BigDecimal payAmount, Payment payment) {
// 业务规则判断
if (!this.status.canPay()) {
throw new OrderDomainException(
String.format("订单[%d]当前状态[%s]不允许支付", id, status)
);
}
if (!this.amount.equals(payAmount)) {
throw new OrderDomainException("支付金额与订单金额不匹配");
}
// 状态转换
this.status = OrderStatus.PAID;
// 发布领域事件(告诉外界:我支付成功了)
DomainEvents.publish(new OrderPaidEvent(
this.id,
this.amount,
payment.getId(),
this.items
));
}
// 更多业务行为
public void cancel() { /* 取消逻辑 */ }
public void applyRefund() { /* 申请退款逻辑 */ }
}
// Service变薄了,只做协调
public class OrderApplicationService {
public void payOrder(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)
通俗解释:在一堆相关的对象里,选个“家长”,外人只能跟家长打交道。
举个例子:订单和订单项。
// 聚合根:Order
public class Order {
private Long id;
private List<OrderItem> items; // 内部对象
// 添加商品:只能通过聚合根的方法
public void addItem(ProductSnapshot product, Integer quantity) {
// 业务规则:不能重复添加相同商品
if (items.stream().anyMatch(item -> item.getProductId().equals(product.getId()))) {
throw new OrderDomainException("商品已存在于订单中");
}
items.add(new OrderItem(product, quantity));
this.calculateTotalAmount(); // 重新计算总金额
}
// 移除商品
public void removeItem(Long productId) {
// 业务规则:至少保留一个商品
if (items.size() <= 1) {
throw new OrderDomainException("订单至少需要包含一个商品");
}
items.removeIf(item -> item.getProductId().equals(productId));
this.calculateTotalAmount();
}
// 外部只能通过聚合根访问内部对象
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items); // 返回不可变列表
}
}
// 内部对象:OrderItem
public class OrderItem {
private Long productId;
private String productName;
private BigDecimal price;
private Integer quantity;
// 没有public的setter!只能通过Order聚合根来修改
void updateQuantity(Integer quantity) {
this.quantity = quantity;
}
}
为什么需要聚合根?
- 保证一致性:修改订单项时,总金额会自动重新计算
- 简化访问:外部只需操作
Order,不用管 OrderItem 的死活
- 明确边界:清晰的告诉你:“从这里开始,是订单的地盘”
2.5 领域事件(Domain Event)
通俗解释:有事发个朋友圈,谁感兴趣谁自己看。
传统写法的问题:
public void payOrder() {
// 支付逻辑...
// 通知库存扣减
inventoryService.deduct(order.getItems());
// 通知积分增加
pointService.add(order.getUserId(), order.getAmount());
// 通知物流系统
logisticsService.create(order);
// 通知客服系统
customerService.notify(order);
// 三个月后要加个“发送短信”
// 你得回来改这里,加一行
smsService.send(order.getUserPhone(), "支付成功");
}
每加一个新功能,就要回来改旧代码,这违反了“开闭原则”。
DDD的解决方案:领域事件
public class Order {
public void pay(Payment payment) {
// 支付逻辑...
this.status = OrderStatus.PAID;
// 发个“朋友圈”
DomainEvents.publish(new OrderPaidEvent(
this.id,
this.userId,
this.amount,
this.items
));
}
}
// 谁感兴趣谁监听
@Component
public class OrderPaidListener {
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
// 库存模块:扣库存
inventoryService.deduct(event.getItems());
}
}
@Component
public class PointListener {
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
// 积分模块:加积分
pointService.add(event.getUserId(), event.getAmount());
}
}
// 三个月后要加短信通知?
// 简单,再写个监听器就行,不用改Order的代码!
@Component
public class SmsListener {
@EventListener
public void handleOrderPaid(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)) {
throw new IllegalArgumentException("币种不同");
}
return new Money(this.amount.add(other.amount), currency);
}
public Money multiply(Integer times) {
return new Money(amount.multiply(new BigDecimal(times)), currency);
}
}
// 枚举:订单状态(封装状态流转规则)
public enum OrderStatus {
CREATED("已创建") {
@Override
public boolean canPay() { return true; }
@Override
public boolean canCancel() { return true; }
},
PAID("已支付") {
@Override
public boolean canPay() { return false; }
@Override
public boolean canCancel() { return false; }
@Override
public boolean canShip() { return true; }
},
// ... 其他状态
private final String desc;
// 抽象方法:强制每个状态定义自己能做什么
public abstract boolean canPay();
public abstract boolean canCancel();
public boolean canShip() default { return false; };
}
// 实体:订单项
public class OrderItem {
private final Long productId;
private final String productName;
private final Money price; // 下单时的价格快照
private final Integer quantity;
public Money getSubtotal() {
return price.multiply(quantity);
}
}
// 聚合根:订单(核心!)
public class Order {
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;
// 工厂方法:创建订单
public static 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;
}
// 核心业务行为:支付
public void pay(Payment payment) {
// 守卫条件
if (!status.canPay()) {
throw new OrderDomainException(
String.format("订单[%d]当前状态[%s]不允许支付", id, status)
);
}
// 金额校验
if (!totalAmount.equals(payment.getAmount())) {
throw new OrderDomainException("支付金额与订单金额不匹配");
}
// 状态转换
this.status = OrderStatus.PAID;
this.paidAt = LocalDateTime.now();
// 发布事件
DomainEvents.publish(new OrderPaidEvent(
this.id,
this.userId,
this.totalAmount,
this.items
));
}
// 核心业务行为:取消
public void cancel(String reason) {
if (!status.canCancel()) {
throw new OrderDomainException(
String.format("订单[%d]当前状态[%s]不允许取消", id, status)
);
}
this.status = OrderStatus.CANCELLED;
// 发布事件
DomainEvents.publish(new OrderCancelledEvent(
this.id,
this.userId,
this.items,
reason
));
}
// 查询方法
public boolean isPaid() {
return status == OrderStatus.PAID;
}
public boolean containsProduct(Long productId) {
return items.stream().anyMatch(item -> item.getProductId().equals(productId));
}
// 注意:没有public的setter!
// 所有状态变更都通过业务方法完成
}
3.3 实现仓储(Repository)
核心代码如下:
// 仓储接口:面向领域模型,不是面向数据库表
public interface OrderRepository {
// 查询方法
Optional<Order> findById(Long id);
Optional<Order> findByOrderNo(String orderNo);
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
// 保存:新增或更新
void save(Order order);
// 删除
void delete(Long id);
// 复杂查询:直接返回领域模型,而不是DTO
List<Order> findUnpaidOrdersOlderThan(LocalDateTime time);
}
// 实现:负责领域模型与数据库的转换
@Repository
@RequiredArgsConstructor
public class OrderRepositoryImpl implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final 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
@Transactional
public void save(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
@RequiredArgsConstructor
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient; // 外部服务客户端
private final ProductClient productClient;
// 命令:创建订单
@Transactional
public 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()));
return new 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. 返回DTO
return OrderResult.from(order);
}
// 命令:支付订单
@Transactional
public 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);
return new PaymentResult(payment.getId(), payment.getStatus());
}
}
3.5 处理领域事件
// 事件:订单已支付
public record OrderPaidEvent(
Long orderId,
Long userId,
Money amount,
List<OrderItem> items
) {}
// 监听器1:扣减库存
@Component
@RequiredArgsConstructor
public class InventoryHandler {
private final InventoryService inventoryService;
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
// 批量扣减库存
inventoryService.batchDeduct(
event.items().stream()
.map(item -> new InventoryDeductCommand(
item.productId(),
item.quantity()
))
.toList()
);
}
}
// 监听器2:增加积分
@Component
@RequiredArgsConstructor
public class PointHandler {
private final PointService pointService;
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
// 计算积分(100元=1积分)
int points = event.amount().amount()
.divide(new BigDecimal(100), RoundingMode.DOWN)
.intValue();
pointService.addPoints(event.userId(), points, "订单支付");
}
}
// 监听器3:通知物流
@Component
@RequiredArgsConstructor
public class LogisticsHandler {
private final LogisticsService logisticsService;
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
logisticsService.createShipment(
new CreateShipmentCommand(
event.orderId(),
event.userId(),
event.items()
)
);
}
}
四、DDD四大填坑避雷
4.1 过度设计
错误示范:一个用户管理系统,硬拆成“用户上下文”、“资料上下文”、“权限上下文”,三个微服务之间用事件总线通信,就为了改个用户头像。
正确姿势:DDD不是银弹!判断标准:
- 团队小于10人,业务简单 → CRUD够用
- 团队10-30人,业务中等复杂度 → 单体应用内用DDD思想组织代码
- 团队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); // 发布OrderItemAddedEvent
order.updateAddress(addr); // 发布OrderAddressUpdatedEvent
order.calculateAmount(); // 发布OrderAmountCalculatedEvent
// ... 一个简单操作发10个事件
正确姿势:事件只用于跨上下文、异步处理的重要业务变更。一个经验法则:如果监听器需要修改本上下文的数据,那可能不该用事件。
五、总结:让代码说业务的话
其实DDD理解起来一点都不难,它的核心思想就两点:
(1)让代码反映业务,而不是数据库
- 传统开发:数据库表 → Entity → Service → Controller(技术驱动)
- DDD开发:业务概念 → 领域模型 → 仓储/服务 → 接口(业务驱动)
(2)统一语言,让技术和业务说同一种话
当产品经理说“订单支付成功后要扣库存”,你应该能直接在代码里找到:
// Order.pay() 方法里
DomainEvents.publish(new OrderPaidEvent(...));
// 库存监听器里
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
inventoryService.deduct(event.getItems());
}
业务人员能看懂,技术人员能实现,这就是DDD最大的价值。
最后的小建议:
不要试图一次性把整个系统改成DDD。从你最痛的那个模块开始:
- 选一个业务复杂的模块(比如订单)
- 画出它的业务流程图
- 识别核心业务规则
- 设计领域模型(先别管数据库)
- 重构,把业务逻辑从Service挪到模型里
- 看看效果,再决定要不要继续
现在,回头看看你那团乱麻的代码,是不是觉得有点思路了?从今天开始,试着用业务的眼光看待代码,而不仅仅是技术的角度。
欢迎在云栈社区与其他开发者交流你在实践中遇到的DDD问题,你会发现,写代码也可以是一件很有逻辑、很优雅的事情。