模块化单体的依赖约束
模块化单体在一个部署单元内划分业务模块,并通过可验证的依赖规则保留各模块的独立性。
1. 模块按业务能力组织
传统分层目录常把所有 Controller、Service 和 Repository 分别放在一起。一个订单需求会横跨多个公共包,模块责任很难从代码结构中看出来。
模块化单体先按业务能力分组,再在模块内部安排应用、领域和基础设施代码:
com.example.shop
├── order
│ ├── OrderManagement.java
│ └── internal
├── inventory
│ ├── InventoryManagement.java
│ └── internal
└── payment
├── PaymentManagement.java
└── internal
每个模块拥有一组业务规则、公开接口和内部实现。所有模块仍在同一个进程中运行,可以共享发布流水线和运行基础设施。
2. 模块只公开必要接口
模块的公开 API 应表达业务能力,例如 reserveInventory,而不是暴露内部 Repository 或数据库实体。其余类型保持 package-private,或放在明确的 internal 包中。
调用方只依赖公开接口后,模块可以在不影响外部的情况下调整表结构、缓存和内部对象。公开 API 也要克制:每增加一个类型,就增加一项需要兼容和维护的契约。
Spring Modulith 默认把应用根包的直接子包识别为模块,并把模块根包中的公开类型视为 API。需要额外暴露接口时,可以声明 named interface,同时限制允许的模块依赖。
3. 依赖图保持单向
模块 A 调用模块 B,同时 B 又调用 A,会产生依赖环。依赖环会带来三个直接问题:
- 两个模块无法独立测试和理解;
- 初始化、事务和事件顺序容易相互影响;
- 未来拆分时必须同时移动双方。
解决依赖环要回到业务责任:把共同规则归给真正的所有者,抽出稳定契约,或把后续动作改为事件通知。不要建立一个名为 common 的大包,把双方内部类型全部搬进去;那只是隐藏了依赖。
理想的模块依赖形成有向无环图。底层共享库只包含稳定、无业务所有权争议的能力,例如时钟、标识类型或审计接口。
4. 同步调用与事件各有位置
调用方需要立即得到结果,并且后续动作必须由当前用例决定时,可以使用模块 API 同步调用。例如订单确认前查询库存预占结果。
一个模块已经完成自己的事务,只需要通知其他模块继续处理时,可以发布应用事件。例如订单取消后通知库存释放和退款。进程内事件没有网络成本,但仍要明确:
- 监听器是在同一事务内还是提交后运行;
- 失败是否阻止主事务;
- 将来拆成异步消息时,消费者是否幂等;
- 事件结构由哪个模块维护。
不要用事件隐藏本应直接返回的失败,也不要让同步调用链无限延长。
5. 数据可以共库但要分所有权
模块化单体通常共用一个数据库实例。每张业务表仍应有唯一写入模块,其他模块通过 API、事件或专用读取模型获取数据。
数据库层面可以使用 schema、表名前缀和独立迁移脚本标记所有权。跨模块查询如果是报表或搜索场景,可以建立只读视图或投影;跨模块直接更新表,会绕过对方的不变量。
共库事务是一项可用能力,但不应成为任意耦合的理由。只有确实需要原子提交的业务规则才使用跨模块事务,并把这项依赖记录下来。
6. 用自动化检查结构
依靠代码评审记住所有依赖规则,项目变大后很容易失效。可以把规则写成测试:
class ModuleStructureTests {
@Test
void verifiesModules() {
ApplicationModules.of(ShopApplication.class).verify();
}
}
Spring Modulith 的验证会检查模块依赖环、对内部包的越界访问,以及可选的允许依赖列表。ArchUnit 还可以验证分层、包依赖、命名和自定义架构规则。
结构测试应进入 CI。一次违规若确有合理原因,应调整边界或显式更新规则,避免用全局排除项让检查失去意义。
7. 模块需要独立测试与观测
一个模块至少需要覆盖:
- 公开 API 的用例测试;
- 模块内部规则的单元测试;
- 与直接依赖模块的契约测试;
- 关键模块调用和事件的指标。
Spring Modulith 可以为单个应用模块建立集成测试范围。即使所有代码在同一进程,日志和指标也应带上模块维度,这样才能判断边界是否产生过长调用链或热点。
8. 适用边界
模块化单体适合业务边界仍在形成、团队规模有限、需要事务简洁和低运维成本的系统。它也适合作为遗留单体治理和未来服务拆分的中间状态。
当某个模块已经有明确的独立发布节奏、单独扩缩容需求、故障隔离价值或独立团队责任时,可以评估拆为服务。只有代码目录分开,却没有接口、数据所有权和依赖检查时,仍然属于普通单体。
9. 常见问题
9.1 Maven 多模块是否就是模块化单体
不一定。Maven 模块提供构建边界,但业务模块仍可能互相引用内部类型和共享数据。模块化单体需要业务责任、公开契约、依赖方向和数据所有权同时成立。
9.2 一个模块是否只能被一个团队维护
不强制,但必须有清楚的决策责任。多个团队都能随意修改模块接口和表结构时,边界会迅速失效。团队结构可以变化,代码所有权和评审人要随之更新。
10. 面试题
10.1 模块化单体与微服务怎样选择
出现公司:字节跳动、美团
考察重点
- 逻辑模块与部署单元的区别。
- 契约、依赖、数据所有权和结构检查。
- 独立部署收益与分布式成本。
相关内容:第 1 节“模块按业务能力组织”至第 8 节“适用边界”。
参考回答
两者都可以按业务能力划分边界。模块化单体把模块放在同一进程和发布单元中,调用与事务更简单,基础设施成本较低;微服务增加独立部署、扩缩容和故障隔离,同时引入网络故障、分布式数据、版本兼容、观测和运维成本。
如果边界还在变化,团队规模有限,也没有明确的独立发布或扩容需求,我会先用模块化单体,并用公开 API、内部包、单向依赖、数据所有权和 CI 结构测试保证边界。等某个模块的变化、负载和责任已经稳定,再根据量化收益渐进拆分。