基于DDD的Modular Monolith架构模块通信设计方案咨询
DDD 模块化单体跨模块通信机制设计实践
你提到的跨模块直接调用时不能直接依赖其他模块领域服务、避免暴露内部领域模型的判断完全符合DDD与模块化单体的核心设计原则,模块间交互必须通过明确定义的公开边界完成,下面针对两个具体问题给出落地实践方案:
问题1:跨模块共享的数据与服务定义位置
你考虑的「设置独立全局项目统一暴露所有契约接口」的方案不是最优实践,长期维护很容易演变成反模式:全局公共契约会让所有模块产生强依赖,最终大概率会变成所有团队都随意往里面塞代码的“大泥球”,直接打破模块的自治边界,和传统分层架构里无人管控的Common包腐化路径完全一致。
正确的定义方式是契约归属权属于提供服务的模块自身,每个模块的代码结构拆分为两个访问权限完全隔离的部分:
- 内部实现区:包含模块的领域层、基础设施层、内部应用服务,这部分代码全部设置为模块内部可见(比如Java的包级私有、C#的internal访问修饰符),其他模块完全无法直接引用
- 公开契约区(Contracts):放在模块自身目录下的独立子包,仅对外暴露三类内容:
- 跨模块调用的服务接口(仅定义方法签名,不带任何实现逻辑)
- 跨模块传输的纯数据结构DTO,这类DTO完全不包含业务逻辑,也绝不引用模块内部的领域对象
- 模块对外发布的集成事件定义(如果搭配事件驱动通信使用)
依赖规则严格单向:只有调用方可以依赖被调用方的Contracts子包,被调用方绝对不能依赖任何其他模块的代码(包括其他模块的Contracts)。比如订单模块需要调用用户模块的信息查询能力,仅订单模块可以引用用户模块的Contracts包,用户模块本身不需要感知订单模块的存在。
问题2:转换层的部署位置与实现方式
你提到的ACL转换层必须部署在提供服务的模块内部,不能放在调用方,也不能放在全局公共位置。
具体实现逻辑如下:
- 公开契约区定义的服务接口,由模块内部的适配层实现,这个实现类标记为模块内部可见,不对外暴露
- 转换逻辑直接写在该实现类中:接收契约层定义的入参DTO,转换为内部领域层可识别的参数,调用内部领域服务、仓储完成逻辑处理;拿到内部领域对象返回结果后,再裁剪、转换为契约层定义的对外DTO,返回给调用方
- 举个简单的实现示例:
// 位于UserModule.Contracts 子包,对外可见 public interface IUserQueryService { UserBriefDto GetUserById(long userId); } // 位于UserModule 内部实现区,标记为internal,外部模块不可访问 internal class InternalUserQueryService : IUserQueryService { private readonly IUserRepository _userRepository; // 仅注入模块内部的基础设施依赖 public InternalUserQueryService(IUserRepository userRepository) { _userRepository = userRepository; } public UserBriefDto GetUserById(long userId) { // 调用内部领域逻辑 var userEntity = _userRepository.FindById(userId); if (userEntity == null) return null; // 转换逻辑:领域实体 -> 对外DTO,同时做字段裁剪、敏感信息过滤 return new UserBriefDto { UserId = userEntity.Id, DisplayName = userEntity.Profile.NickName, AvatarUrl = userEntity.Profile.AvatarPath // 内部字段比如用户密码哈希、实名认证信息等绝对不会出现在DTO中 }; } }
- 注册依赖时,通过模块自身的DI初始化逻辑,将契约接口和内部实现类做绑定,调用方只需要通过构造函数注入契约接口即可完成调用,完全感知不到模块内部的实现逻辑和转换过程。
注意:转换层的唯一职责是做内外数据结构的转换、字段裁剪、敏感信息过滤,绝对不要在转换层中编写业务规则,所有业务逻辑必须收敛在模块内部的领域层。
内容的提问来源于stack exchange,提问作者Fede
相关产品推荐
相关产品推荐

