模块化单体:先把边界分清,再决定是否拆服务

一个应用可以有很多模块,却只部署一个产物。用户、订单、库存各有自己的职责,通过明确接口合作,这就是模块化单体的一种常见组织方式。它不要求代码杂乱,也不意味着只能运行一个实例。

1. 单体说的是部署,不是代码质量

把全部核心业务作为一个主要单元构建和发布,通常称为单体。它内部可以分层、采用领域模型,也可以横向运行多份实例。反过来,把混乱代码拆成几个 HTTP 服务,原来的依赖问题可能变成更难定位的网络依赖。

模块化的关键是所有权:谁定义数据含义,谁允许改变它,外部通过什么接口访问。初期订单与库存可以使用同一个数据库事务,但订单模块不应到处依赖库存表的每一列。

2. 按业务而不是技术目录分组

Text
src/ orders/ api/ 对其他模块开放的用例和事件 internal/ 状态变化与实现细节 persistence/ 本模块数据访问 inventory/ api/ internal/ persistence/ identity/ api/ internal/

这里的目录约定不是编译器自动提供的隔离。要配合包可见性、导出限制、依赖规则检查或架构测试,禁止外部导入 internal。共享工具可以提取,但共享目录若塞进所有业务实体,会重新把模块连成一团。

3. 怎样找业务边界

从业务语言开始。对库存来说,“商品”关心可售数量和仓库;对订单来说,“商品”往往是下单时的名称、单价快照。它们未必要共享同一个可变对象。让已经付款的订单金额随商品当前价格变化,就是混淆了两个语境。

这与 DDD 的限界上下文有关:同一个词在某个边界内有明确模型和含义,不要求在整个公司只有一份万能实体。上下文可对应模块或服务,但并非每个上下文都必须独立部署。

拿一张白纸写下:订单拥有订单状态;库存拥有可售数量;通知拥有投递记录。再列出交互,如 reserveStock、OrderPlaced。如果模块之间需要不停互查内部表才能工作,应重新检查边界,而不是马上增加消息队列。

4. 什么时候开始拆

出现独立扩容需求只是一个信号,还要看独立发布收益、团队职责和故障隔离。通知模块可能适合先作为独立 Worker,因为它耗时、允许延后,且对订单核心事务依赖较少。库存和订单若共享复杂强一致约束,拆开后需要新的协议和恢复成本。

拆分前先统计模块间调用与数据依赖,建立契约测试,明确旧入口到新入口的迁移路径。将某个模块复制到新仓库但继续直接读写旧表,往往仍是耦合部署,只是增加了故障点。

5. 学习练习

为订单模块只开放三个能力:创建、取消、查询。让通知模块仅消费订单事件,而不修改订单表。测试取消已发货订单时,领域规则如何阻止状态变化。再画出如果通知模块停机一小时,哪些功能仍然正常。

这是练习建议,不是“所有项目必须从单体起步”的定律。领域已经成熟、团队已有独立服务经验时,初始设计可以不同;选择应写出当前证据与代价。

参考资料