MateCloud DDD四层架构实战:跟着一个请求看清每一层的边界
【免费下载链接】matecloud🔥MateCloud是一款基于Spring Cloud Alibaba的微服务架构。目前已经整合Spring Boot 4.0.7、 SpringCloud 2025、Spring Cloud Alibaba 2025、Spring Security Oauth2、Feign、Dubbo、JetCache、RocketMQ等,支持多租户的低代码平台,Saas平台开发套件项目地址: https://gitcode.com/GitHub_Trending/ma/matecloud
在用户模块加一个字段要动6个文件、800行的UserService、单测要先mock半个类——这些痛点的解药,就是DDD四层架构。MateCloud是一个基于Spring Cloud Alibaba的微服务项目(整合Spring Boot、Dubbo、RocketMQ等,面向多租户SaaS开发),把这套分层落成了真实源码:Trigger、Application、Domain、Infrastructure四层,各管各的,严禁越界。本文以它真实的用户模块为例,一步步拆解这次微服务分层架构实践。
🎯 一分钟看懂核心思想:每层只管自己的事
思路不复杂:把"业务规则"和"技术实现"分开,把"用例编排"和"入口协议"分开。每个服务模块下就四个目录:
- Trigger层(trigger/):管外部入口——REST控制器、Dubbo RPC、事件监听,只做认证、参数校验、结果包装,严禁写业务规则;
- Application层(application/):管用例编排——CQRS把读写拆成两条路,写命令服务管事务边界,严禁自己揣业务规则;
- Domain层(domain/):业务核心——聚合根、实体、值对象、领域服务,所有业务不变量在这,严禁引用Spring、MyBatis注解;
- Infrastructure层(infrastructure/):管技术实现——DAO、PO、仓储实现、事件发布实现,严禁让业务概念往上泄漏,只给领域层定义的接口做适配器。
关键在依赖方向:领域层不依赖任何人。仓储接口由领域层定义、基础设施层实现,这是依赖倒置,也是"可替换"的前提。admin(角色/菜单/字典)和tenant(租户)子模块用的是同一套结构,说明这是全项目约定,不是某个模块的偏好。
📍 跟着一个请求走:创建用户的完整链路
以"创建一个用户"(POST /api/v1/users)为例,从入口追到数据库。
① Trigger层:认证、校验、然后交给下一层
请求经网关转发到mate-system,落在 UserController。类级@SaCheckLogin查登录,方法级@SaCheckPermission(Perms.USER_ADD)查权限,@Valid完成参数校验,然后直接转交命令服务。如果在这里写"用户名查重""密码编码",后面新增RPC或导入入口时,这些规则就得再实现一遍——这正是它不碰业务的原因。
② Application层:事务边界与编排
写路径的核心是 UserCommandService 的 createUser,它演示了应用层的分工:
@Transactional(rollbackFor = Exception.class) public String createUser(RegisterUserCommand command) { tenantQuotaService.assertCanAddUser(TenantContext.getTenantId()); UserAggregate aggregate = userDomainService.createUser( command.getUsername(), command.getPassword(), command.getMobile(), command.getEmail(), command.getRealName()); userRepository.save(aggregate); eventPublisher.publish(new UserCreatedEvent( aggregate.getId(), command.getUsername(), command.getMobile())); return aggregate.getId(); }先看多租户的套餐配额,再让领域服务创建聚合,经仓储接口持久化,最后发一个领域事件——监听器必须用@TransactionalEventListener(AFTER_COMMIT),回滚时不会有幽灵事件。关键:整个方法没有一条if/else业务规则,只有"先A后B、事务我来管"。
③ Domain层:业务规则与状态变更
UserDomainServiceImpl 是一个没有任何Spring注解的类,它做校验:用户名查重、手机号查重、密码经PasswordEncoderPort端口哈希(领域层不关心用哪种加密),然后UserAggregate.createNew生成聚合——分配UUID、状态置为ACTIVE,并记录一条CREATE操作流水。如果这些校验写在SQL或DAO里,规则就只存在于那一处,下一个人走导入或RPC"创建用户"时就直接绕过去了。
④ Infrastructure层:把聚合落进数据库
UserRepositoryImpl 接手领域层定义的仓储接口:转换器把实体转成UserPO,MyBatis-Plus的UserDao插入,再把聚合上累积的操作流水批量入库。表结构、mapper、逻辑删除(@TableLogic)这些技术细节全封在这层,哪天换持久化框架,动的只有这个目录。
读路径更短:UserQueryServiceImpl 经仓储接口取聚合,再用转换器 UserConvertor 返回UserInfoResponse——这个DTO定义在mate-api模块,其他服务的RPC调用共用。同一套领域代码还被 RpcUserServiceImpl 和Excel导入复用,importBatch 就是逐行走同一个 createUser 管线,保证两个入口规则一致。
🔎 分层要点速览
Trigger层:协议适配器,业务逻辑出门口即停
职责边界:HTTP/RPC/事件的最后一段路,只做认证、校验和Result包装。
@PostMapping @SaCheckPermission(Perms.USER_ADD) public Result<String> createUser(@Valid @RequestBody RegisterUserCommand command) { return Result.ok(userCommandService.createUser(command)); }关键在"转手就走",控制器代码量几乎是恒定的,它变长就是逻辑泄漏的信号。常见误区:把查重这类规则放控制器里,后果是每个新入口都要复制一份。
Application层:CQRS拆读写,事务在这层
职责边界:写走CommandService(挂@Transactional),读走QueryService返回DTO。
@Override public UserInfoResponse findById(String userId) { UserAggregate aggregate = userRepository.findById(userId); return aggregate == null ? null : userConvertor.toResponse(aggregate); }关键:读路径绝不碰"改状态"的方法,返回的是DTO而不是聚合根。常见误区:把状态机if/else塞进应用层,规则就和"这条编排"绑死了,别的用例没法复用。
Domain层:纯Java,一个框架注解都没有
职责边界:聚合、实体、值对象、领域服务,所有业务不变量在这层。
public void freeze() { checkActive(); this.status = UserStatus.FROZEN; } private void checkActive() { if (isFrozen()) throw UserException.of(UserErrorCode.USER_IS_FROZEN); if (isDomainDeleted()) throw UserException.of(UserErrorCode.USER_IS_DELETED); }关键:"已删除的用户不能再被冻结"写在实体自己的方法里,谁来调都绕不过。常见误区:给领域类加@Service或MyBatis注解,领域层从此没法用纯JUnit测。
Infrastructure层:只为领域定义的接口做实现
职责边界:DAO、PO、仓储实现、事件发布实现,只有技术细节。
@Override @Transactional(rollbackFor = Exception.class) public void save(UserAggregate aggregate) { UserPO po = UserInfraConvertor.INSTANCE.toPO(aggregate.getUser()); userDao.insert(po); aggregate.getUser().setId(po.getId()); batchSaveStreams(aggregate.getAndClearOperateStreams()); }关键:转换、表结构、逻辑删除都封在里面,领域层从头到尾没看见UserPO。常见误区:把仓储接口放在infra、让domain依赖它,依赖方向一反过来,"可替换"就是空话。
📊 传统写法 vs DDD四层:一笔账怎么算
| 维度 | 传统写法(大Service) | MateCloud的四层 |
|---|---|---|
| 改一个字段/规则 | Controller/Service/DAO/mapper多处散改 | 领域模型+转换类,入口和其他层不动 |
| 单测业务规则 | 要Spring容器+测试库,慢 | 纯JUnit不起容器,见 UserTest、UserAggregateTest |
| 换持久化技术 | 业务代码与DAO缠绕 | 只动infra层的Dao与仓储实现 |
| 新增入口(RPC/导入/事件) | 逻辑复制或跨层乱调 | 复用同一个Command服务 |
| 多租户隔离 | 每条SQL手工加租户条件 | 命令层套餐配额断言+租户拦截器自动圈定查询范围 |
⚠️ 分层架构最常见的坑
- 业务逻辑写进Controller。后果:规则只存在于那一处,单测只能走HTTP集成。正确姿势:控制器只做认证、校验、转交,规则下沉领域层。
- 领域层引入框架注解。
@Service、@Transactional、MyBatis注解,出现一个,领域层就不"可移植"了。正确姿势:看 UserDomainServiceImpl——零注解,Bean由基础设施层的 DomainServiceConfiguration 注册。 - 应用层直调DAO绕过领域。后果:"冻结用户不能改资料"这类不变量只在部分入口生效,同一份数据不同入口行为不一致。正确姿势:写操作一律 领域服务 → 聚合 → 仓储。
- 聚合根直接返回给前端。后果:密码这类内部字段可能泄漏,表结构一变前端跟着遭殃。正确姿势:读路径统一经转换器出DTO。
- 什么都上全套四层。单表字典CRUD也切四层,样板代码比逻辑还多。我的建议:按领域复杂度决定建模深度,值得拆的才拆,简单域可以先用两层过渡。
🧩 落地清单:在你自己项目复刻的最小步骤
- 先选一个限界上下文:挑你最熟的模块(用户、订单、角色都行),划出trigger/application/domain/infrastructure四个目录,先不动代码,只搭骨架;
- 再抽领域模型:把聚合根、实体、值对象从Service里挪出来,"什么状态下能做什么"变成实体方法,只留纯POJO和接口;
- 然后做依赖倒置:仓储接口放domain、实现放infrastructure,领域服务的Bean用config类注册;
- 再拆读写:Command/Query分开,读走转换器出DTO,统一Result返回;
- 最后补入口:RPC、事件监听、导入都进trigger层,复用同一套命令服务。
设计动机和约定细节,项目里都有现成文档可以对照。
🏁 收尾:它解决了什么,没解决什么
回到开头的问题:字段变更不再散落六处文件,规则变更集中在领域模型,业务规则的单测不用容器就能写。但四层架构不解决所有事——限界上下文的边界怎么划、建模做到多深,仍然靠你的判断;前期建模成本比一个大Service更高,很小的CRUD项目硬上全套反而负担。这套架构是前期付费、后续每次需求变更都在回本。想深入设计动机,直接看 DDD架构RFC、编码规范 和 用户模块源码。
【免费下载链接】matecloud🔥MateCloud是一款基于Spring Cloud Alibaba的微服务架构。目前已经整合Spring Boot 4.0.7、 SpringCloud 2025、Spring Cloud Alibaba 2025、Spring Security Oauth2、Feign、Dubbo、JetCache、RocketMQ等,支持多租户的低代码平台,Saas平台开发套件项目地址: https://gitcode.com/GitHub_Trending/ma/matecloud
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考