模块化单体架构:业务边界的单体与演进

深入模块化单体(Modular Monolith)架构:为何它是微服务之前的理性中间态、模块边界识别与防腐层、模块内的分层、模块间通信与依赖治理、DB 层的模块化、从单体演进到微服务的路径,以及何时该转向微服务。

单体在坊间被误读为"又一个巨石",微服务则被当成万能解药。但现实是:绝大多数团队真正需要的,是模块化单体(Modular Monolith)——一个可独立演进、边界清晰、部署却仍是一个进程的架构。本文讲清楚它是什么、不是微服务的原因,以及如何把模块边界砌漂亮。

1. 引言:为什么微服务之前应该先考虑模块化单体

微服务不是起点

微服务能带来独立部署与弹性,也带来分布式系统的一切麻烦:网络故障、数据一致、链路追踪、版本兼容。复杂度不会消失,只会从代码转移到底层设施。

对大多数业务,单体交付最快、最稳。而单体的最大问题不是"技术",而是模块边界崩塌——互相 import、共享大对象、牵一发动全身。模块化单体正好补上这一课:保留单体的部署简单性,引入微服务的模块边界纪律。

模块化单体定义

一个进程/一个大仓库,内部按业务领域切成清晰模块,
模块间通过显式接口通信,禁止任意跨模块访问内部细节。

一句话:微服务是"把模块分到进程",模块化单体是"把模块纪律放进一个进程"。先学会边界,再谈拆分进程。


2. 模块边界:限界上下文的落地

2.1 以 DDD 限界上下文为边界

模块最自然的边界是 DDD 的限界上下文(Bounded Context):订单、库存、支付、用户……每个上下文内部高内聚,上下文之间通过显式接口往来。

订单模块 ──(下单事件/接口)──→ 库存模块
   │                           │
   └──(支付命令)──→ 支付模块

2.2 明确禁止的依赖

  • 禁止跨模块直接访问另一模块的内部类/数据表;
  • 禁止跨模块 new 对方的实现类,只能调公开接口;
  • 禁止模块间共享"大杂烩"工具类导致隐性耦合。

2.3 防倾倒依赖:防腐

跨模块协作时,用接口隔离内部表征。例如订单模块要通知库存扣减,只暴露 StockService.markReserved(orderId, sku, qty),不暴露库存内部模型。

一句话:模块边界 = 限界上下文 + 显式接口 + 禁止内部访问;边界画得好,模块才是"可插拔的零件"而不是"互相缠绕的线团"。


3. 模块内的结构与依赖治理

3.1 模块内部分层

模块(如 Order)
  ├── api/         对外接口(DTO、门面)
  ├── domain/      领域逻辑(实体、服务、策略)
  ├── application/ 用例编排(应用服务)
  └── infra/       实现(仓储、外部调用)

对外可见的只有 api/,其余对模块外部不可见(包私有/模块私有)。

3.2 依赖方向约束

  • 单向依赖:模块 A 依赖模块 B,则 B 不得反向依赖 A;
  • 禁止环:模块间成环会让"谁能改谁"说不清,用架构测试(ArchUnit 等)在 CI 强制;
  • 依赖倒置:跨模块调用依赖接口,具体实现留在各自模块。

3.3 结构可视化

用依赖图检查工具(如 JDepend/ArchUnit/ndepend)把模块依赖画出来——环与多层依赖会立刻现形,并作为 CI 质量门禁。

一句话:模块内部分层、模块之间单向依赖、用架构测试守门——这是模块化单体不腐化的三条支腿。


4. 数据层的单边界:模块各自建表

4.1 一份 Schema,多模块共库不共表

模块化单体通常共用一个数据库实例,但各模块拥有自己的表集合:

单独库:  用户库 / 订单库 / 库存库(强隔离,重)
共享库分表:db 同一库,order 模块只管 order_*表

推荐共享库 + 各模块私有表组 + 明确命名前缀:

  • 订单模块只碰 order_ 前缀的库表;
  • 存根:库存模块从"读自己的表",不读订单模块的表;
  • 跨模块数据需求一律走接口,不许直连对方表。

4.2 事务的边界与妥协

单体共享库带来一个好处:跨模块一个本地事务能做到。但也容易被滥用成"巨事务"。

取舍:
  强一致的跨模块写 → 一体事务(简单,但耦合)
  可最终一致的写   → 模块内事务 + 消息/事件(边界更清晰)

4.3 分库边界先行

如果确定未来会拆分,现在就按模块开设独立 Schema/独立库,模块化单体到微服务时数据层基本不动。

一句话:数据层也要守模块边界——共库不共表、命名前缀隔离、跨模块靠接口而不是 SQL join。


5. 模块间通信:接口与事件的取舍

5.1 同步接口

模块间同步调用(RPC/本地方法)适合请求-响应、强实时的场景;代价是调用方持有被调模块的引用,耦合加深,但胜在简单直接。

订单服务 → 同步调用 → 支付模块.submit()

5.2 异步事件

模块间发布领域事件(订单已创建 → 库存模块监听扣库存)。解耦最好,但引入了最终一致性:

// 事件发布(伪)
orderModule.publish('order.placed', { orderId, items });

// 库存模块订阅
inventoryModule.on('order.placed', async (e) => { await reserve(e.items); });
维度同步接口异步事件
耦合度高低
实时性实时有延迟
一致性强最终一致
排障复杂度低高

设计原则:业务边界紧密、需强一致的用同步;对实时性不敏感、想解耦的用事件。演进到微服务时,模块间的接口与会话能直接映射为服务间远程调用。


6. 部署形态:一体进程,模块可独立演进

6.1 一个大部署,多模块独立发布

模块化单体仍是一个进程、一条部署流水线,但模块内部 3 日能独立演进(加字段、改接口)不影响全局。

好处:

  • 部署一个 jar/镜像即完成,运维简单;
  • 不需要分布式的服务发现、网关、链路追踪(尚不需);
  • 一个请求穿越模块,本地调试、断点、单元测试都顺手。

6.2 演进到微服务的准备

当年级增长使"独立部署的独立团队协作"成为瓶颈时,再考虑分进程:

模块化单体 → 把"最热/最独立"的模块拆成独立服务
            → 拆的边界 = 模块边界(已验证)
            → 数据随模块迁移

模块化单体的最大价值,就是让未来拆分"沿模块边界、干净利落",而不是推倒重来。

一句话:模块化单体先把"边界与依赖"做对,后面要分按模块边界切就是了——它是某种意义上的"微服务的地基预习"。


7. 何时不该用 / 何时必须转向微服务

不构成用模块单体的信号应该转向微服务的信号
项目 < 3 个领域的完整体组织 > 10 个团队并自用发布节奏
团队 < 2 个模块要独立扩容/弹性伸缩
尚无明确领域边界模块间异构技术栈(多语言)
单体已 BE需要独立部署不同团队的发布窗口

一句话:别因为"听说单体坏"就不敢用模块化单体。简单场景它是更负责任的默认;等团队、规模真正触发需求,微服务才入场。


8. 踩坑清单

坑现象对策
无边界模块互相 import、缠成团用限界上下文切模块 + 架构测试
共享大工具类隐性耦合工具类按模块拆分/就近放置
跨模块直连他人表数据耦合只走公开接口 + 命名前缀隔离
慕求进程式微服务没分工先扛分布式成本先模块化单体打磨模块边界
模块间成环无法独立演进依赖方向治理 + ArchUnit 拦截
过度拆数据库部署至少两个共享库 + 模块表组即可

9. 总结

环节要点
是什么一个部署,内部按限界上下文切模块
边界限界上下文 + 显式接口 + 防腐
依赖单向、无环、依赖倒置、架构测试
数据共库不共表、命名前缀、接口通信
通信同步接口(强) / 异步事件(解耦)
演进沿模块边界平滑拆微服务

一句话记住:模块化单体是"进可微服务、退可单体"的中间态——它把模块边界、依赖、数据隔离这些核心能力在一个进程里练扎实,是企业级架构最被低估、性价比最高的第一步。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「架构」更多文章

  1. 发布策略与灰度架构:蓝绿、金丝雀、滚动与回滚
  2. 混沌工程:主动制造故障,验证系统弹性
  3. API 设计与契约治理:从 REST 到 OpenAPI 的工程化