三层架构把所有"业务相关"的代码塞进一个叫Service的黑盒里。DDD把这个黑盒打开了。
你的Service层,是不是已经超过1000行了?
我问过几十个后端开发,回答"是"的超过80%。
更可怕的是,改一个业务规则,你发现要改三四个Service,改完心里还没底------不知道漏了哪里。上线之后果然出了Bug,积分规则在某一个角落没有被同步。
问题出在哪?
出在三层架构的那个"业务逻辑层"是一个黑盒。
它用三个字定义了一切,却从未告诉你:里面到底该怎么组织?四种不同性质的逻辑混在一起,凭什么不爆炸?
今天这篇文章,用一个真实的 "下单支付、扣余额、加积分" 的例子,带你把三层架构的Service解剖开,然后用DDD把它重写一遍。
看完你会明白:为什么有人能把Service写进坟墓,有人能写出清明上河图。
读完这篇文章,你将收获什么?
✅ 看清三层架构的Service里,到底混了哪四种 不同性质的逻辑
✅ 理解DDD为什么要拆出 "应用层"和"领域层" ,它们跟原来的Service有什么区别
✅ 用"下单支付扣余额加积分"的完整代码,搞清楚 "贫血模型"和"充血模型" 的本质区别
✅ 从包结构 一眼判断一个项目是真DDD还是挂羊头卖狗肉
✅ 一张表讲透 "原来的Service → 应用层 → 领域层" ,读完这张就够了
一、三层架构:一个被我们默认为"标准答案"的起点
先定标。经典三层架构长这样:
| 层级 | 职责 | 典型组件 |
|---|---|---|
| 表示层 | 接收请求、返回响应 | Controller |
| 业务逻辑层 | 处理业务规则、流程编排 | Service |
| 数据访问层 | 操作数据库 | DAO / Mapper |
依赖方向自上而下:Controller 依赖 Service,Service 依赖 DAO。
新人三天上手------一个请求进来,Controller 调 Service,Service 调 DAO,返回数据。这套模式统治了绝大多数企业级开发。
但三层架构在"业务逻辑层"这个方框里画了个问号。 它把所有说不清道不明的代码全部扔进Service,然后用一句话堵住你的嘴:"这是业务逻辑。"
事实是,Service里什么都有------有业务规则,有数据库操作,有缓存读写,有消息发送。它们交织在一起,像一团乱麻。谁也不敢动,谁也不敢改。
这就是三层架构的真相:它不是架构,它只是一个起跑线。
二、解剖一个真实的Service:它到底装着什么?
来看一段典型的"下单支付成功,扣钱包余额,加积分"的三层架构代码:
java
// 文件: service/OrderService.java(三层架构)
@Service
public class OrderService {
@Autowired private UserDao userDao;
@Autowired private OrderDao orderDao;
@Autowired private CreditLogDao creditLogDao;
@Autowired private RedisTemplate redis;
@Autowired private MqProducer mq;
@Transactional
public void payOrder(Long orderId, Long userId) {
// 1. 技术动作:查数据
Order order = orderDao.findById(orderId);
User user = userDao.findById(userId);
// 2. 业务规则:校验订单状态
if (order.getStatus() != OrderStatus.UNPAID) {
throw new BizException("订单已支付或已取消");
}
// 3. 业务规则:校验余额是否充足
if (user.getBalance() < order.getTotalAmount()) {
throw new BizException("余额不足");
}
// 4. 业务规则:扣余额
user.setBalance(user.getBalance() - order.getTotalAmount());
// 5. 技术动作:存数据库
userDao.save(user);
// 6. 业务规则:加积分(消费1元积1分)
int points = order.getTotalAmount().intValue();
user.setPoints(user.getPoints() + points);
// 7. 技术动作:存数据库
userDao.save(user);
// 8. 业务规则:更新订单状态
order.setStatus(OrderStatus.PAID);
order.setPayTime(LocalDateTime.now());
// 9. 技术动作:存数据库
orderDao.save(order);
// 10. 技术动作:记录积分流水
creditLogDao.save(new CreditLog(userId, points, "订单支付"));
// 11. 技术动作:更新缓存
redis.opsForValue().set("user:" + userId, JSON.toJSONString(user));
// 12. 技术动作:发送消息
mq.send("order.paid", orderId);
}
}
现在我问你:这个方法里,到底有几种不同性质的逻辑?
仔细数:
业务规则 (说"是什么"):第2行校验状态、第3行校验余额、第4行扣余额、第6行加积分、第8行更新状态
持久化逻辑 (说"存哪里"):第1行查数据、第5/7/9行存数据库、第10行记流水
技术辅助 (说"怎么更快/更联动"):第11行更新缓存、第12行发消息
流程编排(说"谁先谁后"):整个方法的执行顺序
四种不同性质的逻辑,在同一个方法里交错出现。
原来的Service不是因为"太大"而坏掉的,而是因为它同时承担了四种不同性质的职责,却没有在结构上把它们分开。
当业务规则变化时(积分配置改为黄金会员积2分),你去Service里找。
当技术方案变化时(Redis换Caffeine),你还是去Service里找。
Service变成了所有变化的交汇点------业务人员和技术人员的变更,都瞄准同一个文件。任何一个维度的变动,都会冲击其他维度。
如果你再看User这个类:
java
// 文件: entity/User.java(三层架构)
@Entity
public class User {
private Long id;
private BigDecimal balance;
private Integer points;
// 只有 getter 和 setter,没有任何行为
}
User是个 "空心人" ------知道自己有余额和积分,但不知道自己能做什么。扣余额的逻辑在Service里,加积分的逻辑在Service里。一切行为都在Service里。
Martin Fowler管这叫 "贫血模型" 。
后果:如果AdminService也要给用户加积分(比如活动赠送),你只能把第6行那段复制粘贴过去。业务规则散布在各处,改漏一个就是Bug。
贫血模型 = 业务逻辑散落四处 → 低内聚
Service直接依赖DAO/Redis/MQ → 业务与技术绑定 → 高耦合
根本原因:业务逻辑层是一个未定义内部结构的黑盒
三、DDD的回应:不是"加一层",而是"拆一类"
DDD的第一刀就落在这里:把不同性质的逻辑拆开,各自归位。
| 三层架构 | → | DDD四层架构 | 拆分的逻辑 |
|---|---|---|---|
| 表示层 | → | 用户接口层 | 对应,但更宽(HTTP/RPC/MQ) |
| 业务逻辑层(Service) | → | 应用层 | 接手"流程编排"+"技术协调"------查、存、事务、发消息 |
| → | 领域层 | 接手 全部业务规则------余额够不够、积分怎么算、状态对不对 | |
| 数据访问层 | → | 基础设施层 | 接手"持久化实现"+"技术辅助"------实现仓储接口、Redis、MQ |
关键规则:
- 应用层 只能做流程编排和技术协调,不允许出现任何业务规则 (不能有
if (balance < amount)) - 领域层 只能做业务规则,不允许出现任何技术代码 (不能有
@Autowired、@Transactional、@Table)
下面用"下单支付,扣余额加积分"逐层展示拆分后的样子。
四、拆分后的四层代码长什么样?
4.1 领域层:定义"业务规则是什么"(核心!)
所有业务规则的唯一居所。纯Java,没有Spring注解,不依赖任何技术组件。
java
// 文件: domain/user/User.java
public class User {
private Long id;
private String name;
private Money balance; // 值对象:金额
private Points points; // 值对象:积分
private UserLevel level; // 枚举:普通/黄金/钻石
public User(String name) {
this.name = name;
this.balance = Money.zero();
this.points = Points.zero();
this.level = UserLevel.NORMAL;
}
// 业务规则:扣余额(只有这一个入口)
public void deductBalance(Money amount) {
if (amount == null || amount.isNegative()) {
throw new DomainException("扣减金额不能为空或负数");
}
if (this.balance.lessThan(amount)) {
throw new DomainException("余额不足");
}
this.balance = this.balance.minus(amount);
}
// 业务规则:加积分(根据会员等级计算不同倍数)
public void addPointsForConsumption(Money spentAmount) {
if (spentAmount == null || spentAmount.isNegative()) {
throw new DomainException("消费金额不能为空或负数");
}
int basePoints = spentAmount.getAmount().intValue(); // 1元=1积分
int multiplier = this.level.getPointMultiplier(); // 普通:1, 黄金:2, 钻石:3
this.points = this.points.add(basePoints * multiplier);
}
}
java
// 文件: domain/user/Money.java
public class Money {
private BigDecimal value;
public Money(BigDecimal value) {
if (value == null || value.compareTo(BigDecimal.ZERO) < 0) {
throw new DomainException("金额不能为负");
}
this.value = value;
}
public static Money zero() { return new Money(BigDecimal.ZERO); }
public boolean isNegative() { return this.value.compareTo(BigDecimal.ZERO) < 0; }
public boolean lessThan(Money other) { return this.value.compareTo(other.value) < 0; }
public Money minus(Money other) { return new Money(this.value.subtract(other.value)); }
public BigDecimal getAmount() { return this.value; }
}
java
// 文件: domain/user/Points.java
public class Points {
private int value;
public Points(int value) {
if (value < 0) throw new DomainException("积分不能为负");
this.value = value;
}
public static Points zero() { return new Points(0); }
public Points add(int amount) {
if (amount < 0) throw new DomainException("增加积分必须为正");
return new Points(this.value + amount);
}
public int getValue() { return value; }
}
java
// 文件: domain/user/UserLevel.java
public enum UserLevel {
NORMAL(1), GOLD(2), DIAMOND(3);
private int pointMultiplier;
UserLevel(int multiplier) { this.pointMultiplier = multiplier; }
public int getPointMultiplier() { return pointMultiplier; }
}
java
// 文件: domain/order/Order.java
public class Order {
private Long id;
private Long userId;
private Money totalAmount;
private OrderStatus status;
private LocalDateTime payTime;
public Order(Long userId, Money totalAmount) {
this.userId = userId;
this.totalAmount = totalAmount;
this.status = OrderStatus.UNPAID;
}
// 业务规则:支付(状态校验 + 状态变更)
public void pay() {
if (this.status != OrderStatus.UNPAID) {
throw new DomainException("订单已支付或已取消");
}
this.status = OrderStatus.PAID;
this.payTime = LocalDateTime.now();
}
public Money getTotalAmount() { return totalAmount; }
}
java
// 文件: domain/order/OrderStatus.java
public enum OrderStatus { UNPAID, PAID, CANCELLED }
java
// 文件: domain/repositories/UserRepository.java(接口,不实现)
public interface UserRepository {
User findById(Long id);
void save(User user);
}
java
// 文件: domain/repositories/OrderRepository.java
public interface OrderRepository {
Order findById(Long id);
void save(Order order);
}
领域层的三个特征:
- 没有任何
@Autowired、@Transactional、@Table注解 - 不知道数据库、Redis、MQ的存在
- 只关心一件事------业务规则
4.2 应用层:负责"流程怎么跑"
接手流程编排和技术协调。不做任何业务判断。
java
// 文件: application/service/PayOrderAppService.java
@Service
public class PayOrderAppService {
// 依赖的是领域层定义的接口,不是具体的DAO!
private final UserRepository userRepo;
private final OrderRepository orderRepo;
private final EventPublisher eventPublisher;
public PayOrderAppService(UserRepository userRepo,
OrderRepository orderRepo,
EventPublisher eventPublisher) {
this.userRepo = userRepo;
this.orderRepo = orderRepo;
this.eventPublisher = eventPublisher;
}
@Transactional
public void handle(Long orderId, Long userId) {
// 1. 技术协调:获取领域对象
Order order = orderRepo.findById(orderId);
User user = userRepo.findById(userId);
// 2. 【核心】调用领域方法------没有一行if/else业务判断!
// 所有规则都在领域层内部执行
order.pay(); // 校验状态+改状态
user.deductBalance(order.getTotalAmount());// 校验余额+扣钱
user.addPointsForConsumption(order.getTotalAmount());// 计算积分+加分
// 3. 技术协调:持久化
orderRepo.save(order);
userRepo.save(user);
// 4. 技术协调:发布事件
eventPublisher.publish(new OrderPaidEvent(orderId, userId));
}
}
应用层的特征:你找不到if (balance < amount),找不到points = amount * level。 它只做四件事:获取、调用、保存、发事件。
4.3 基础设施层:实现"技术怎么落地"
java
// 文件: infrastructure/persistence/UserRepositoryImpl.java
@Repository
public class UserRepositoryImpl implements UserRepository {
@Autowired private UserJpaMapper userMapper;
@Override
public User findById(Long id) {
UserPo po = userMapper.selectById(id);
return UserConverter.toDomain(po);
}
@Override
public void save(User user) {
UserPo po = UserConverter.toPo(user);
userMapper.updateById(po);
}
}
java
// 文件: infrastructure/persistence/converter/UserConverter.java
public class UserConverter {
public static User toDomain(UserPo po) {
User user = new User(po.getName());
user.setId(po.getId());
user.setBalance(new Money(po.getBalance()));
user.setPoints(new Points(po.getPoints()));
user.setLevel(UserLevel.valueOf(po.getLevel()));
return user;
}
public static UserPo toPo(User user) {
UserPo po = new UserPo();
po.setId(user.getId());
po.setName(user.getName());
po.setBalance(user.getBalance().getAmount());
po.setPoints(user.getPoints().getValue());
po.setLevel(user.getLevel().name());
return po;
}
}
java
// 文件: infrastructure/message/EventPublisherImpl.java
@Component
public class EventPublisherImpl implements EventPublisher {
@Autowired private MqProducer mqProducer;
@Override
public void publish(DomainEvent event) {
if (event instanceof OrderPaidEvent) {
mqProducer.send("order.paid", ((OrderPaidEvent) event).getOrderId());
}
}
}
4.4 用户接口层:接收请求
java
// 文件: interfaces/web/OrderController.java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired private PayOrderAppService appService;
@PostMapping("/pay")
public Result pay(@RequestBody PayRequest req) {
// 只做参数校验和路由
appService.handle(req.getOrderId(), req.getUserId());
return Result.success();
}
}
五、一张表讲透:原来的Service vs 应用层 vs 领域层
如果你没时间读完全文,读完这张表就够了。
| 对比维度 | 原来的三层Service | DDD的Application Service | DDD的Domain Layer |
|---|---|---|---|
| 文件位置 | service/OrderService.java |
application/service/PayOrderAppService.java |
domain/user/User.java domain/order/Order.java |
| 代码长啥样 | if (balance < amount) user.setBalance(...) userDao.save(user) redis.set(...) mq.send(...) |
order.pay() user.deductBalance() user.addPoints() userRepo.save() eventPublisher.publish() |
if (balance < amount) balance = balance - amount points = amount * level.getMultiplier() |
| 包含业务规则吗? | ✅ 混在技术代码中 | ❌ 不包含 | ✅ 全部包含 |
| 包含技术协调吗? | ✅ 混在业务代码中 | ✅ 查、存、发事件 | ❌ 不包含 |
| 包含流程编排吗? | ✅ 混在代码里 | ✅ 显式按顺序调用 | ❌ 不包含 |
| 依赖什么? | DAO、Redis、MQ | Repository接口 | 谁都不依赖 |
| 单元测试 | 需启动Spring | 需Mock接口 | 直接new对象 |
六、高内聚、低耦合:两把尺子量到底
三层架构 = 业务低内聚 + 技术高耦合
- 低内聚 :扣余额、加积分的规则散布在
OrderService、AdminService、ActivityService里。改一处规则,多处修改,漏一个就Bug。 - 高耦合:Service直接依赖DAO、Redis、MQ。换缓存方案,Service代码跟着改。业务被绑在技术上。
DDD = 业务高内聚 + 技术极低耦合
- 高内聚 :User的所有行为在
User.java里,Order的支付在Order.java里。改积分规则,只改这两个文件。 - 极低耦合:领域层不知道数据库、缓存的存在。换技术方案,只换基础设施层,领域层毫不知情。
七、包结构:看一眼就知道是真DDD还是假DDD
三层架构(按技术角色分)
text
com.company.project
├── controller/ # 表示层
├── service/ # 所有逻辑混在一起
├── dao/ # 数据访问
└── entity/ # 贫血实体
DDD(先按业务领域分,再按层分)
text
com.company.project
├── interfaces/
│ └── web/OrderController.java
├── application/
│ └── service/PayOrderAppService.java
├── domain/ # ← 核心!最大最活跃
│ ├── user/
│ │ ├── User.java # 充血模型!
│ │ ├── Money.java
│ │ ├── Points.java
│ │ └── UserLevel.java
│ ├── order/
│ │ ├── Order.java # 充血模型!
│ │ └── OrderStatus.java
│ └── repositories/
│ ├── UserRepository.java
│ └── OrderRepository.java
└── infrastructure/
├── persistence/
│ ├── UserRepositoryImpl.java
│ └── converter/
└── message/
一眼判断:
- 顶级包是
controller/service/dao/entity→ 三层 - 顶级包是
interfaces/application/domain/infrastructure,且domain包最大 → DDD domain里的类带@Table→ 假DDD;纯Java带行为方法 → 真DDD
八、何时选择?三层不是被淘汰,而是被超越
| 场景 | 三层架构 | DDD |
|---|---|---|
| CRUD、管理后台 | ✅ 够用 | ❌ 过度设计 |
| 金融、供应链、电商核心 | ❌ 很快撑不住 | ✅ 长期受益 |
| 业务稳定 | ✅ 快速交付 | ❌ 没必要 |
| 规则频繁变化 | ❌ 改不动 | ✅ 高内聚 |
渐进路径:
- 先把复杂逻辑从Service移到Entity(先充血)
- 引入Repository接口,实现依赖倒置
- 拆分Service为Application Service + Domain Service
- 按业务边界划分限界上下文
最后说一个可能得罪人的判断
三层架构没有死,它只是被用错了地方。
90%的CRUD系统、管理后台、内部工具,三层架构足够用。 硬上DDD是过度设计,是炫技。
但另外10%的系统------那些业务复杂、规则多变、长期演进的系统------如果不从三层架构向DDD演进,就是技术债的滚雪球。
区别在于:三层架构是你为"当前功能"付出的成本。DDD是你为"未来变化"付出的投资。
问题是:你的系统,还能活多久?
总结
三层架构有一个叫Service的黑盒 → 打开发现混了四种逻辑 → DDD拆成应用层(流程)和领域层(规则)→ 用充血模型把规则内聚到对象里 → 依赖倒置,领域层成为纯核心 → 业务高内聚,技术低耦合 → 这一切长在包结构里。
三层架构解决的是"怎么分层"的问题。
DDD回答的是更深一层的问题:业务逻辑层里面到底是什么,以及怎么组织它。
当你在代码里写下第一个充血模型时------比如把if (balance < amount)从Service挪到User.java里------你其实是在用代码说:
我知道这个业务规则属于谁,以及它应该待在哪里。
这才是架构从"能用"走向"好用"的那一步。
如果这篇文章对你有帮助,可以把它分享给那个正在为Service臃肿而头疼的同事。
欢迎在评论区留下你的观点:你的Service超过1000行了吗?你是选择重构还是继续堆代码?