工程结构图
说明:
- 从应用层中拆分出一个 contract (契约)层,用于解耦 infra (基础设施)层与 Application(应用) 层
- Application(应用)层只依赖 Domain(领域)层+contract(契约)层
- infra(基础设施)层只依赖 Domain(领域)层+contract(契约)层
- contract(契约)层是一组纯接口+数据结构+基础设施层对外暴露接口(从动)
各层级职责划分
| 层级 | 职责 |
|---|---|
| 接口层(Interface) | 负责接收和处理外部请求 |
| 应用层(Application) | 负责业务流程的编排,事务控制、消费领域业务事件,外部服务的调用(如 MQ、RPC等) |
| 契约层(contract) | 解耦应用层与基础设置层之间的依赖;从应用层拆解出来的,暴露外部接口和定义查询接口(纯查询) |
| 领域层(domain) | 业务建模(聚合根/实体、值对象)、封装业务规则、领域服务、领域工厂、所需的仓储接口、不依赖外部技术(如数据库、MQ、RPC 等) |
| 基础设施层(infrastructure) | 技术实现,如数据库访问、缓存、MQ、远程调用等 |
| 公共包(common) | 基础工具包,与业务逻辑毫无关系的通用类(如工具类、异常定义、常量) |
| api 包(api) | 接口定义、参数类、接口文档等;无实现 |
数据传递图
数据结构模型定义及及说明
| 层级 | 名称 | 用途 | 说明 | 命名示例 | ||
|---|---|---|---|---|---|---|
| 接口层 | Request | 接口入参 | 用于接收前端参数 | LoginRequest | ||
| Response | 接口返回 | 返回给前端的结构体 | LoginResponse | |||
| 应用层 | DTO | 数据传输对象 | 结构灵活,适合跨层;不包含业务逻辑;主要作为返回对象使用 | UserDTO | ||
| Command | 动作入参 | 表示一种意图或动作 | CreateUserCommand | |||
| Query | 查询入参 | 用于封装查询条件 | UserListQuery | |||
| Message | 消息体 | MQ 消息体(主要是生产,消费的可以在接口那层定义) | OrderPayMessage | |||
| 领域层 | Entity | 聚合根/领域实体 | 有唯一标识,有生命周期(有状态、流程和规则限制的);无生命周期可简单建模(如字典)或不建模,通过事件在基础设施层监听消费(如日志) | User,Order | ||
| Param | 领域层参数 | 用于领域工厂构造复杂实体和业务规则校验 | ||||
| ID | 标识值对象 | 用于标识实体,强类型替代 Long | UserId、Email | |||
| Event | 领域事件 | 说明“发生了某件事” | UserLockedEvent | |||
| 基础设施层 | PO | 数据库映射对象 | 一般与表结构 1:1映射 | UserPO | ||
| Condition | DB 查询条件 | MyBatis 查询参数容器 | UserQueryCondition | |||
| BO | 多表联合查询的结果 | 应用层的中间业务数据,为组装 DTO 对象提供数据支撑 | OrderDetailBO | |||
| API 包/基础设施层 rpc包 | Args | RPC 请求参数对象 | API 包是为服务端的接口,基础设施层是客服端,无论服务端还是客户端都使用一样的后缀 | RoleArgs | ||
| Result | RPC 响应参数对象 | RoleResult |
各层级编码规范
接口层
-
定义接口时,每个接口的 Request 对象应独立存在;除通用返回的 Response 外,每个接口的 Response 也应独立定义。
-
Request 和 Response 不宜直接向下层传递,应做相应转换。
-
Controller 仅负责简单的参数校验(通过注解实现)和请求转发,不处理业务逻辑。
-
返回数据应使用自定义的包装类封装,包含状态码(code)等信息,而非直接返回 Response、Object或者 ResponseEntity<Response/Object>。
-
当 Response 返回多层级数据时,采用内部类结构承载子数据,保证层次清晰。
-
统一接口日志拦截规范,确保请求和响应日志打印且脱敏:
- HandlerInterceptor:打印 URI、请求参数、traceId、耗时等。
- ResponseBodyAdvice:打印 JSON 响应体,支持脱敏和统一封装。
- ControllerAdvice:统一捕获并处理异常。
应用层
- 不处理核心业务逻辑(如状态机、复杂业务规则,例如订单超时自动取消等)。
- 对于读操作,调用查询和数据组装逻辑,不直接调用领域层,相关接口定义在契约层;应用层负责组装返回结果。
- 对于写操作,负责流程编排、事务管理及调用第三方服务,核心业务规则由领域服务实现,大部分业务代码写在领域层。
- 对于包含读写的行为操作,负责编排多步骤流程:调用外部服务、执行领域行为、保存数据。
- 对领域服务抛出的业务事件,在应用层进行监听和消费。
契约层
- 仅定义接口协议和数据结构,不包含任何业务逻辑,业务编排由应用层完成。
- 三方服务暴露出来的接口
领域层
- 领域层不负责所有查询,仅支持命令执行所需的必要查询。
- 实体按需加载,仅包含当前业务行为所必需的数据。
- 聚合间引用实体时,应仅引用对方实体的 ID,而非整个实体对象。
- 具有业务语义、不可拆分且需行为或校验的字段,应封装为值对象以提升复用性。
- 实体应定义行为,贯彻行为驱动设计。
- 领域层抛出的事件中,带有业务含义的事件由应用层监听消费;无业务含义的事件(如日志)由基础设施层消费。
- 简单实体可通过 Assembler 进行(Command → Entity)的映射转换;复杂建模或涉及业务规则时,应通过领域工厂(Factory)完成(Command → Param)的处理。
- 不直接调用第三方服务,所有外部访问通过端口(Port)实现。
- 限界上下文建设:当数据库单表承载多个业务领域状态与行为时,单一模型容易臃肿且职责模糊。为避免此类问题,应拆分为多个实体,分别归属不同聚合,实现职责清晰划分和领域隔离。(可选)
- 判断对象是否属于某个聚合根管理,关键在于对象生命周期是否依赖聚合根,且存在以聚合根为前提。
基础设施层
-
实现 Repository 和 QueryService 接口,负责数据查询,确保仅做简单数据判断,不承担数据组装任务。
-
提供技术服务实现,如数据库连接、消息队列(MQ)、RPC 调用、缓存等。
-
监听和消费领域层抛出的非业务性事件(如日志)
-
SQL 日志管理:
- 打印 SQL 执行时间、traceId、接口路径、方法名、数据库名、表名等元数据。
- 生产环境不打印完整 SQL,开发和测试环境可打印完整 SQL 用于调试。
其他
- 锁的范围和生命周期应覆盖整个事务过程,可适当提前加锁或延后释放,避免并发冲突及数据不一致。
- 锁 Key 应基于业务唯一键设计,同一业务不同操作共享同一把锁,保障并发安全。
- 大多数情况下,锁加在接口层,事务管理放在应用层;批量处理场景可能需多把锁保证粒度和效率。
- 发送业务消息(如 MQ)应采用事件注册机制,确保消息仅在事务成功提交后发送,保障数据一致性。
- 各层保持清晰解耦,避免跨层调用,保证高内聚低耦合。
- 关键业务操作推荐设计幂等机制,防止重复执行导致数据异常。
从传统架构转整洁架构中疑问答疑
| 问题 | 解决方案 | 说明 |
|---|---|---|
| 依赖倒置后, PO 类不能暴露出去,应用层查询接口要查一些数据,但是没有接收类怎么办 | 全部定义 DTO 来接收 | DDD/整洁架构中,PO 只属于基础设施层。不能跨层暴露给应用层或接口层。使用专门定义的 DTO 显式承载要展示的数据内容,是职责边界清晰、可维护、可扩展的设计方式。 |
| 我能不能直接调用领域层的查询接口,返回 Entity? | 不推荐;即使满足需求也不该直接用 Entity | Entity 是领域内部的行为载体,不该作为传输载体暴露出去。否则违反“面向接口编程”和“依赖倒置原则”,会让外层代码对内部结构产生强耦合。 |
| DTO 太多太杂,看着爆炸怎么办? | 组织结构、命名规范、复用结构、统一转换 | DTO 爆炸是“清晰职责划分”的自然产物。不是问题,只是组织问题。可以通过:1、场景命名:ListDTO / DetailDTO / ExportDTO 2、抽取通用字段:BaseXxxDTO 3、Assembler 封装统一转换实现结构清晰、职责分明。 |
| DTO 应该复用吗? | 尽量不共用(保持语义清晰);可结构复用(字段) | 各个使用场景的 DTO 应该独立定义(即使字段一样),避免语义冲突和耦合问题。但可以通过字段提取成公共结构类 BaseDTO 来避免冗余。 |
| DTO 怎么转换? | 封装进 Assembler 类 | 比如 OrderDetailAssembler.toDTO(order)。Assembler 是解耦转换逻辑的重要手段,减少重复代码和脏耦合。 |
| 如果领域服务中需要调用外部服务(如发短信、 MQ 、 RPC )怎么办? | 使用事件机制解耦 | 领域层不允许直接依赖外部服务。业务事件:发布后在应用层消费(如“用户注册成功后发券”)技术事件:基础设施层消费(如日志、MQ) |
| 如果领域服务内部要直接依赖远程 RPC 并处理返回逻辑呢? | 定义接口 CreditRpc 放在领域层,基础设施层实现它 | 这种情况确实存在,尤其在微服务中。当RPC 的结果是领域判断所必需的信息时,应将其作为“领域所依赖的能力”建模成接口放在领域层(Port),由 infra 层提供具体实现(Adapter)。 |
| 事件机制会不会破坏事务的一致性? | Spring 默认事件是同步执行、共享主事务 | 使用 @EventListener,事件会立即同步执行、和主流程共享事务。能读到主事务待提交的数据,并统一回滚。本质就像“把流程拆成模块”,而不是异步。 |
| 如果我用 @TransactionalEventListener(phase = AFTER_COMMIT) 呢? | 会延迟执行,不能回滚主事务 | AFTER_COMMIT 意味着事务已经提交,监听器才触发。适合做“后置副作用”(如发 MQ、写日志、发邮件等)。但监听器内部失败,不会影响主流程,需手动监控、补偿、幂等处理。 |
| 如果我要让事件异步执行(如发 MQ ),怎么办? | 必须自己建立一致性机制 | 一旦事件与主事务解耦,就必须处理一致性:① 落库保证可靠投递(Outbox 模式)② 消费幂等③ 消息失败补偿 / 监控告警④ 避免副作用影响主流程这会让你的事件机制复杂度上升很多。 |
| 事件发布和调用服务代码有啥区别? | 默认事件机制就像“正常流程代码拆分” | 使用 Spring 同步事件的效果就像是“把流程代码拆成多个模块”,职责更清晰,但仍然在一个事务中同步执行。只是逻辑分布更清楚,不影响执行顺序和原子性。 |
总结
本文系统地阐述了基于整洁架构的多层设计理念,重点围绕 CQRS、事件驱动和依赖倒置原则展开,明确了各层职责和边界,实现了业务逻辑与技术实现的高内聚低耦合。
通过拆分契约层,有效解耦了应用层与基础设施层的依赖,提升了系统的可维护性和扩展性。CQRS 模式的应用,使得读写职责清晰分离,进一步优化了系统的性能和复杂度管理。
事件驱动机制不仅保证了业务流程的松耦合,还增强了事务一致性与异步处理能力,为复杂业务场景提供了可靠的支持。依赖倒置原则的贯彻,确保了系统模块间的灵活替换和解耦,促进了高质量代码的持续演进。
综合来看,该架构设计既兼顾了理论先进性,又具备良好的实用性和工程落地能力,是打造健壮、可扩展企业级应用的有效范式。未来可在此基础上,结合具体业务特点,进一步深化异步一致性方案和领域事件设计,推动系统架构不断优化升级。