DDD领域驱动设计落地实践与六边形架构
引言
先讲一个我亲身经历过的项目事故。
三年前我接手一个"电商中台"的重构项目。这个系统最初只有商品管理和订单创建两个功能,两年后膨胀到 47 个 Service 类、12 万行代码。最要命的是,某次大促前,运营提了一个需求:"VIP 用户在特定品类下单时,可以叠加使用定向优惠券,但优惠券不能和满减活动同时生效。"
听起来是个简单规则,但我们的开发同学改了整整两周,上线后出现了三处资损漏洞。原因很简单——这段业务规则被拆散在 OrderService、CouponService、PromotionService、UserService 四个类里,没有任何一个地方能完整表达"VIP 用户 + 特定品类 + 定向券 + 满减"这个业务事实。
这就是典型的贫血模型(Anemic Domain Model)灾难:对象只有 getter/setter,所有业务逻辑散落在 Service 层,业务规则无处安放,改一处漏三处。
DDD(Domain-Driven Design)和六边形架构(Hexagonal Architecture)正是为了解决这类问题而生的。但市面上大量文章只讲概念不讲落地,看完还是不知道怎么下手。这篇文章,我会用源码级分析 + 可运行代码,把 DDD 落地这件事讲透。
一、核心概念:用餐厅类比理解 DDD 与六边形架构
1.1 餐厅类比
把软件系统想象成一家餐厅:
| 软件概念 | 餐厅类比 | 说明 |
|---|---|---|
| 领域(Domain) | 餐厅的核心业务 | 做菜、点单、结账 |
| 实体(Entity) | 一道具体的菜 | 有唯一标识(订单号),状态会变(生的→熟的) |
| 值对象(Value Object) | 一份"微辣"口味要求 | 不可变,只描述特征,没有身份 |
| 聚合(Aggregate) | 一张订单 | 订单 + 菜品 + 备注作为一个整体,有事务边界 |
| 聚合根(Aggregate Root) | 订单本身 | 外界只能通过订单访问菜品,不能绕过订单直接改菜品 |
| 领域服务(Domain Service) | 大厨 | 跨多个食材的复杂烹饪逻辑 |
| 仓储(Repository) | 仓库管理员 | 负责取出/存放食材,隐藏存储细节 |
| 六边形架构 | 餐厅的就餐区 + 后厨 + 采购口 | 就餐区(API)、后厨(领域)、采购口(DB)通过标准接口对接 |
关键点:餐厅的"菜谱"(业务规则)必须写在厨房里,而不是写在服务员的对讲机里(Service 层)。这就是 DDD 的核心主张——业务逻辑回归领域对象。
1.2 六边形架构的核心价值
六边形架构(也叫端口-适配器架构)的本质是依赖倒置:
- 端口(Port):领域层定义的接口,表达"我需要什么能力"
- 适配器(Adapter):基础设施层实现端口,比如 MySQL、Redis、RPC
注意看箭头方向:领域层是绝对核心,它不依赖任何外层;所有依赖都通过端口(接口)倒置。这就是六边形架构的灵魂——领域层零依赖。
二、源码级深度分析:为什么贫血模型会失控?
2.1 贫血模型的典型代码
来看一段真实的订单创建代码(简化版):
// ❌ 贫血模型:Order 只是个数据袋子
public class Order {
private Long id;
private Long userId;
private BigDecimal amount;
private Integer status;
// 只有 getter/setter...
}
// ❌ 业务逻辑全在 Service,规则散落
@Service
public class OrderService {
public void createOrder(Long userId, BigDecimal amount) {
// 校验用户
User user = userService.get(userId);
if (user == null) throw new BizException("用户不存在");
// 校验金额
if (amount.compareTo(BigDecimal.ZERO) <= 0)
throw new BizException("金额非法");
// 计算折扣(散落在 Service 里的规则)
if (user.isVip() && amount.compareTo(new BigDecimal("1000")) > 0) {
amount = amount.multiply(new BigDecimal("0.9"));
}
Order order = new Order();
order.setUserId(userId);
order.setAmount(amount);
order.setStatus(0);
orderMapper.insert(order);
}
}这段代码的问题:
- 规则无法复用:另一个入口(比如 MQ 消费)想创建订单,必须再写一遍校验
- 并发不安全:
order.setStatus(0)后如果有人改了 status,没有任何保护 - 无法表达不变量:Order 对象可以处于任意非法状态(amount 为负、status 为 999)
- 测试困难:想测试"金额非法"的场景,必须启动 Spring 容器
2.2 用 DDD 重构后的代码
// ✅ 充血模型:Order 自己守护业务不变量
public class Order {
private final OrderId id; // 值对象,不可变
private final UserId userId;
private Money amount; // 值对象,自带校验
private OrderStatus status; // 枚举,状态机
private final List<OrderItem> items;
// 私有构造 + 工厂方法,禁止非法创建
private Order(OrderId id, UserId userId, List<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new DomainException("订单必须包含至少一个商品");
}
this.id = id;
this.userId = userId;
this.items = List.copyOf(items); // 防御性拷贝
this.amount = calculateTotal(items);
this.status = OrderStatus.CREATED;
}
public static Order create(UserId userId, List<OrderItem> items) {
return new Order(OrderId.generate(), userId, items);
}
// 业务行为内聚在领域对象里
public void applyDiscount(DiscountPolicy policy, User user) {
if (this.status != OrderStatus.CREATED) {
throw new DomainException("只有待支付订单可以打折");
}
this.amount = policy.calculate(this.amount, user, this.items);
}
public void pay(PaymentId paymentId) {
if (this.status != OrderStatus.CREATED) {
throw new DomainException("订单状态不允许支付");
}
this.status = OrderStatus.PAID;
// 发布领域事件...
}
private Money calculateTotal(List<OrderItem> items) {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
// 只暴露必要的行为,不暴露 setter
public Money amount() { return amount; }
public OrderStatus status() { return status; }
}核心差异:
- 贫血模型:对象是名词,逻辑是动词,散落各处
- 充血模型:对象既是名词也是动词,逻辑内聚
2.3 聚合边界的源码级判断
如何判断两个实体是否应该在同一聚合?看这个经验法则:
一个事务只修改一个聚合。
比如"下单扣库存"这个场景,很多同学会把 Order 和 Inventory 放在一个聚合里,导致:
// ❌ 错误:跨聚合事务,锁竞争严重
@Transactional
public void createOrder(...) {
Order order = Order.create(...);
orderRepository.save(order);
inventory.decrease(order.items()); // 同一个事务里改库存
}正确做法是通过领域事件最终一致:
// ✅ 正确:Order 聚合只负责自己
@Transactional
public void createOrder(...) {
Order order = Order.create(...);
orderRepository.save(order);
// 领域事件在事务提交后发布
eventPublisher.publish(new OrderCreatedEvent(order.id(), order.items()));
}
// 库存服务订阅事件,独立事务处理
@EventHandler
public void on(OrderCreatedEvent event) {
inventoryService.decrease(event.items());
}三、实战代码:三个完整可运行示例
示例 1:完整的订单聚合 + 值对象(Java)
// ============ 值对象:Money ============
package com.example.domain.shared;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;
/**
* 值对象:金额
* 特性:不可变、无身份、可替换
*/
public final class Money {
public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("CNY"));
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
this.amount = amount.setScale(2, java.math.RoundingMode.HALF_UP);
this.currency = Objects.requireNonNull(currency);
}
public static Money of(BigDecimal amount) {
return new Money(amount, Currency.getInstance("CNY"));
}
public static Money of(double amount) {
return of(BigDecimal.valueOf(amount));
}
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(BigDecimal rate) {
return new Money(this.amount.multiply(rate), this.currency);
}
private void assertSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("币种不一致");
}
}
public BigDecimal value() { return amount; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 && currency.equals(money.currency);
}
@Override
public int hashCode() { return Objects.hash(amount, currency); }
}// ============ 聚合根:Order ============
package com.example.domain.order;
import com.example.domain.shared.Money;
import com.example.domain.shared.DomainEvent;
import com.example.domain.shared.DomainException;
import java.time.Instant;
import java.util.*;
/**
* 订单聚合根
* - 唯一入口:所有对订单的修改必须通过 Order 的方法
* - 不变量:金额非负、状态机合法、至少一个商品
*/
public class Order {
private final OrderId id;
private final UserId userId;
private final List<OrderItem> items;
private Money amount;
private OrderStatus status;
private final Instant createdAt;
private final List<DomainEvent> events = new ArrayList<>();
private Order(OrderId id, UserId userId, List<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new DomainException("订单必须包含至少一个商品");
}
this.id = Objects.requireNonNull(id);
this.userId = Objects.requireNonNull(userId);
this.items = List.copyOf(items);
this.amount = items.stream().map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
this.status = OrderStatus.CREATED;
this.createdAt = Instant.now();
this.events.add(new OrderCreatedEvent(this.id, this.userId, this.amount));
}
public static Order create(UserId userId, List<OrderItem> items) {
return new Order(OrderId.generate(), userId, items);
}
/** 应用折扣策略 —— 策略通过参数传入,聚合不依赖具体实现 */
public void applyDiscount(DiscountPolicy policy, UserContext ctx) {
requireStatus(OrderStatus.CREATED, "应用折扣");
Money discounted = policy.calculate(this.amount, ctx, this.items);
if (discounted.value().compareTo(Money.ZERO.value()) < 0) {
throw new DomainException("折扣后金额不能为负");
}
this.amount = discounted;
this.events.add(new OrderDiscountedEvent(this.id, this.amount));
}
public void pay(PaymentId paymentId) {
requireStatus(OrderStatus.CREATED, "支付");
this.status = OrderStatus.PAID;
this.events.add(new OrderPaidEvent(this.id, paymentId, this.amount));
}
public void cancel(String reason) {
if (status == OrderStatus.PAID) {
throw new DomainException("已支付订单需走退款流程");
}
this.status = OrderStatus.CANCELLED;
this.events.add(new OrderCancelledEvent(this.id, reason));
}
private void requireStatus(OrderStatus expected, String action) {
if (this.status != expected) {
throw new DomainException(
String.format("订单当前状态[%s]不允许[%s]", status, action));
}
}
/** 拉取领域事件(事务提交后清空) */
public List<DomainEvent> pullEvents() {
List<DomainEvent> copy = List.copyOf(events);
events.clear();
return copy;
}
public OrderId id() { return id; }
public Money amount() { return amount; }
public OrderStatus status() { return status; }
public List<OrderItem> items() { return items; }
}// ============ 端口:Repository ============
package com.example.domain.order;
/**
* 端口(Port):领域层定义,基础设施层实现
* 注意:接口在领域层,实现在基础设施层 —— 依赖倒置
*/
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByUserId(UserId userId);
}这段代码的关键设计点:
Order没有 setter,外部只能通过业务方法修改applyDiscount接收DiscountPolicy接口而非具体实现,领域层不依赖任何框架pullEvents()实现领域事件的延迟发布,保证事务一致
示例 2:六边形架构的应用服务 + 适配器(Java + Spring)
// ============ 应用层:编排用例 ============
package com.example.application;
import com.example.domain.order.*;
import com.example.domain.shared.DomainEvent;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 应用服务:只做编排,不写业务规则
* 职责:加载聚合 → 调用领域方法 → 保存 → 发布事件
*/
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final UserQueryService userQueryService;
private final DiscountPolicyFactory discountPolicyFactory;
private final DomainEventPublisher eventPublisher;
public OrderApplicationService(OrderRepository orderRepository,
UserQueryService userQueryService,
DiscountPolicyFactory discountPolicyFactory,
DomainEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.userQueryService = userQueryService;
this.discountPolicyFactory = discountPolicyFactory;
this.eventPublisher = eventPublisher;
}
@Transactional
public OrderId createOrder(CreateOrderCommand cmd) {
// 1. 调用领域工厂创建聚合
List<OrderItem> items = cmd.items().stream()
.map(i -> new OrderItem(i.skuId(), i.price(), i.quantity()))
.toList();
Order order = Order.create(new UserId(cmd.userId()), items);
// 2. 查询上下文(跨聚合查询走读模型,不加载整个 User 聚合)
UserContext ctx = userQueryService.loadContext(cmd.userId());
// 3. 调用领域行为
DiscountPolicy policy = discountPolicyFactory.resolve(ctx);
order.applyDiscount(policy, ctx);
// 4. 持久化
orderRepository.save(order);
// 5. 事务提交后发布事件(通过 TransactionSynchronization 保证)
order.pullEvents().forEach(eventPublisher::publishAfterCommit);
return order.id();
}
}// ============ 适配器层:REST Controller ============
package com.example.adapter.web;
import com.example.application.OrderApplicationService;
import com.example.application.CreateOrderCommand;
import org.springframework.web.bind.annotation.*;
/**
* 入站适配器(Driving Adapter)
* 职责:协议转换(HTTP → Command),不包含业务逻辑
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderApplicationService orderService;
public OrderController(OrderApplicationService orderService) {
this.orderService = orderService;
}
@PostMapping
public ApiResponse<String> create(@RequestBody CreateOrderRequest req) {
// 只做 DTO → Command 转换
CreateOrderCommand cmd = new CreateOrderCommand(
req.userId(),
req.items().stream()
.map(i -> new CreateOrderCommand.Item(
i.skuId(), i.price(), i.quantity()))
.toList()
);
String orderId = orderService.createOrder(cmd).value();
return ApiResponse.ok(orderId);
}
}// ============ 适配器层:Repository 实现 ============
package com.example.adapter.persistence;
import com.example.domain.order.*;
import org.springframework.stereotype.Repository;
/**
* 出站适配器(Driven Adapter)
* 职责:领域对象 ↔ 数据库表 的映射
*/
@Repository
public class OrderRepositoryImpl implements OrderRepository {
private final OrderJpaMapper jpaMapper;
public OrderRepositoryImpl(OrderJpaMapper jpaMapper) {
this.jpaMapper = jpaMapper;
}
@Override
public void save(Order order) {
OrderPO po = OrderConverter.toPO(order);
jpaMapper.save(po);
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaMapper.findById(id.value())
.map(OrderConverter::toDomain);
}
@Override
public List<Order> findByUserId(UserId userId) {
return jpaMapper.findByUserId(userId.value())
.stream()
.map(OrderConverter::toDomain)
.toList();
}
}分层职责一览:
| 层 | 职责 | 依赖方向 |
|---|---|---|
| Adapter(适配器) | 协议转换、数据映射 | 依赖 Application |
| Application(应用) | 用例编排、事务边界 | 依赖 Domain |
| Domain(领域) | 业务规则、不变量 | 零依赖 |
| Infrastructure | 技术实现(DB、MQ、RPC) | 实现 Domain 端口 |
示例 3:领域事件 + 最终一致性(Go 版本)
换个语言,展示 DDD 不是 Java 专属:
package order
import (
"errors"
"time"
)
// Order 聚合根
type Order struct {
id OrderID
userID UserID
items []OrderItem
amount Money
status Status
events []DomainEvent
createdAt time.Time
}
// 工厂方法,禁止直接构造
func NewOrder(userID UserID, items []OrderItem) (*Order, error) {
if len(items) == 0 {
return nil, errors.New("订单必须包含至少一个商品")
}
total := ZeroMoney()
for _, it := range items {
total = total.Add(it.Subtotal())
}
o := &Order{
id: NewOrderID(),
userID: userID,
items: items,
amount: total,
status: StatusCreated,
createdAt: time.Now(),
}
o.record(OrderCreatedEvent{OrderID: o.id, Amount: total})
return o, nil
}
// 业务行为
func (o *Order) ApplyDiscount(policy DiscountPolicy, ctx UserContext) error {
if o.status != StatusCreated {
return errors.New("只有待支付订单可以打折")
}
newAmount, err := policy.Calculate(o.amount, ctx, o.items)
if err != nil {
return err
}
o.amount = newAmount
o.record(OrderDiscountedEvent{OrderID: o.id, NewAmount: newAmount})
return nil
}
func (o *Order) Pay(paymentID PaymentID) error {
if o.status != StatusCreated {
return errors.New("订单状态不允许支付")
}
o.status = StatusPaid
o.record(OrderPaidEvent{OrderID: o.id, PaymentID: paymentID})
return nil
}
func (o *Order) record(e DomainEvent) {
o.events = append(o.events, e)
}
// PullEvents 拉取并清空事件
func (o *Order) PullEvents() []DomainEvent {
evts := o.events
o.events = nil
return evts
}
// 端口定义
type Repository interface {
Save(o *Order) error
FindByID(id OrderID) (*Order, error)
}四、方案对比:DDD 与传统分层架构
| 维度 | 传统三层架构 | DDD + 六边形架构 |
|---|---|---|
| 业务逻辑位置 | Service 层(贫血) | 领域对象(充血) |
| 对象职责 | 数据载体 | 数据 + 行为 |
| 依赖方向 | Controller → Service → DAO | Adapter → Application → Domain ← Infrastructure |
| 可测试性 | 需启动容器 | 领域层纯单元测试 |
| 业务规则复用 | 差(散落) | 好(内聚) |
| 学习成本 | 低 | 高 |
| 适用场景 | CRUD、简单业务 | 复杂业务、长期演进系统 |
与其他架构的对比
1. DDD vs 事务脚本(Transaction Script)
事务脚本适合简单场景,比如后台管理系统的增删改查。强行上 DDD 是过度设计。判断标准:如果一个业务规则能用 3 行 if-else 说清楚,就不要用聚合。
2. DDD vs 洋葱架构(Onion Architecture)
洋葱架构和六边形架构是同一思想的不同表达。区别在于:
- 六边形强调端口/适配器(技术视角)
- 洋葱强调依赖同心圆(分层视角)
实战中两者常常混用,本质都是"领域核心,依赖倒置"。
3. DDD vs CQRS
CQRS 不是 DDD 的替代,而是补充。DDD 解决"写模型"的复杂业务,CQRS 解决"读模型"的高并发查询。常见组合:写侧用 DDD 聚合,读侧用扁平化 View。
充血模型] D --> E[写库] E -.事件同步.-> F[读库
宽表/ES] C --> F style D fill:#ffcccc style F fill:#ccffcc
五、最佳实践与避坑指南
✅ 最佳实践
- 聚合尽量小:一个聚合只保护真正需要强一致的不变量。Order + OrderItem 是合理的,Order + User 就是灾难。
- 通过 ID 引用其他聚合:
Order.userId而不是Order.user。跨聚合加载会破坏事务边界。
- 领域层零框架依赖:Domain 模块的
pom.xml里不应该有 Spring、MyBatis。这是检验架构是否干净的硬指标。
- 领域事件延迟发布:用
TransactionSynchronizationManager保证事务提交后才发 MQ,避免"消息发了但事务回滚"。
- DTO 与领域对象分离:Controller 的 Request 和 Domain 的 Entity 必须分开,防止外部直接操作领域对象。
❌ 常见坑
坑 1:把聚合根当成万能容器
// ❌ 聚合过大:Order 里塞了 User、Coupon、Promotion
public class Order {
private User user; // 跨聚合!
private List<Coupon> coupons; // 跨聚合!
private Promotion promotion; // 跨聚合!
}后果:加载一个订单要 join 十张表,锁竞争剧烈。
坑 2:领域服务滥用
领域服务只应该处理"跨多个实体的业务规则",比如"转账"涉及两个 Account。如果只有一个实体参与,写在实体内部。
坑 3:Repository 里写业务逻辑
// ❌ 错误
public interface OrderRepository {
List<Order> findExpiredOrders(); // 什么算"过期"?这是业务规则!
}
// ✅ 正确
public interface OrderRepository {
List<Order> findByStatusAndCreatedBefore(OrderStatus status, Instant time);
}坑 4:贫血模型 + DDD 命名 = 伪 DDD
很多团队号称用了 DDD,代码里却全是 OrderDTO、OrderService.updateStatus()。判断标准:领域对象有没有行为方法?没有就是贫血。
坑 5:忽略防腐层(ACL)
对接外部系统时,不要直接把对方的 DTO 引入领域层。用适配器 + 转换器隔离:
// ✅ 防腐层
public class ExternalPaymentAdapter implements PaymentGateway {
private final ThirdPartyPaymentClient client; // 三方 SDK
@Override
public PaymentResult pay(Order order, Money amount) {
// 转换:领域模型 → 三方请求
ThirdPartyRequest req = new ThirdPartyRequest(
order.id().value(), amount.value().toString());
ThirdPartyResponse resp = client.invoke(req);
// 转换:三方响应 → 领域模型
return resp.success()
? PaymentResult.success(resp.transactionId())
: PaymentResult.fail(resp.errorMsg());
}
}六、总结与延伸思考
核心要点回顾
- DDD 的本质是"把业务规则放回它该在的地方"——领域对象。贫血模型是绝大多数业务失控的根源。
- 聚合是事务边界,不是对象容器。一个事务只改一个聚合,跨聚合用领域事件最终一致。
- 六边形架构的核心是依赖倒置。领域层零依赖,所有外部能力通过端口(接口)注入。
- DDD 不是银弹。CRUD 系统上 DDD 是自寻烦恼,复杂业务系统不用 DDD 是慢性自杀。
延伸思考
- DDD 与微服务的边界如何对齐? 一个限界上下文(Bounded Context)通常对应一个微服务,但并非绝对。如果两个上下文共享同一团队、同一发布节奏,合并为模块化单体反而更好。
- 事件溯源(Event Sourcing)值得上吗? 只有在需要完整审计、时间旅行查询时才考虑。大多数场景,传统持久化 + 领域事件足够。
- 如何度量 DDD 落地效果? 看两个指标:领域层单元测试覆盖率(应 > 80%)、领域模块的依赖数(应为 0)。
架构的演进从来不是一蹴而就的。我建议的落地路径是:先在核心域试点一个聚合 → 团队达成共识 → 逐步推广到支撑域 → 通用域保持简单 CRUD。切忌一步到位,那是架构师最大的傲慢。
愿你的领域模型,永远清晰如初。