单体架构的三种形态

单体架构的三种形态

单体架构的三种模块组织形态

单体架构 ≠ 一个 Main 函数 + 一堆 if-else。从模块组织方式看,单体只有三种形态。选错形态,项目变大后的维护成本指数级上升。


先界定讨论范围

"架构分类"可以从很多维度看,为避免概念混淆,先把每个维度的关系说清楚:

复制代码
一个 Spring Boot 项目的架构可以从多个维度同时描述:

  维度 1:部署拓扑
    ├── 单机部署   → 一台服务器一个进程(本文不展开)
    └── 分布式     → 多服务 / 多节点

  维度 2:模块组织  ← 本文只讨论这一个
    ├── 单模块单体
    ├── 分层模块单体
    └── 模块化单体

  维度 3:内部架构风格
    ├── 分层架构 (Layered)
    ├── 六边形架构 (Hexagonal)
    ├── 清洁架构 (Clean Architecture)
    └── CQRS / 事件驱动 等(本文不展开)

  维度 4:数据库拓扑
    ├── 共享数据库
    └── 每模块独立数据源(本文不展开)

本文只讨论维度 2------模块组织方式。 同一个项目可以同时是"分层模块单体 + 分层架构 + 共享数据库 + 单机部署",这不矛盾。

在这个维度下,单体只有三种形态。区分它们的关键问题只有一个:

Maven / Gradle 的模块边界,是按什么切的?


一、总览

复制代码
                 单体架构的模块组织方式
                          │
          ┌───────────────┼───────────────┐
          │               │               │
     单模块单体      分层模块单体      模块化单体
   (Single-Module)  (Layered)    (Modular Monolith)
          │               │               │
     不拆模块        按技术层拆        按业务领域拆
          │               │               │
  pom.xml ×1       pom.xml ×N        pom.xml ×N
  包名约定分层       admin/system/     user/order/
                   framework/common   product/payment

三种形态都打成 1 个 JAR、1 个进程、1 个数据库。 区别全在源码组织上。


二、形态一:单模块单体

是什么

整个项目只有一个 Maven/Gradle 模块,所有的类在同一个源码目录下。分层靠包名约定,编译器不干预。

目录结构

复制代码
project/
├── pom.xml                        ← 全项目唯一的 pom
└── src/main/java/com/example/
    ├── config/                    ← Security、CORS、MyBatis 配置
    ├── controller/                ← REST 接口
    ├── service/                   ← 业务逻辑
    │   └── impl/
    ├── mapper/                    ← 数据访问
    ├── entity/                    ← 数据库实体
    ├── dto/                       ← 入参 / 出参对象
    ├── interceptor/               ← 拦截器
    ├── exception/                 ← 全局异常处理
    └── util/                      ← 工具类

典型代表

  • Spring PetClinic(Spring 官方示例项目) --- 单模块,包名分层
  • 单体博客、个人项目、外包小后台 --- 绝大多数小型 Spring Boot 项目
  • 早期 Spring Boot 教程项目 --- controller/service/mapper 三件套

核心特征

复制代码
┌──────────────────────────────────────┐
│           单一 Maven/Gradle 模块       │
│                                      │
│   controller/  ←→  service/          │
│        ↕              ↕              │
│      dto/    ←→   mapper/            │
│        ↕              ↕              │
│    entity/   ←→    util/             │
│                                      │
│   所有类在同一个 classpath 下          │
│   任何类可以 import 任何类             │
│   编译器:零隔离                      │
│   分层约束:全靠团队自觉               │
└──────────────────────────────────────┘

编译器不管的后果

以下代码在单模块中完全可以编过,但破坏了分层原则:

java 复制代码
// 文件:com/example/service/UserService.java

@Autowired
private UserController userController;  // ❌ Service 引用了 Controller!
// 编译器:✅ 没报错(因为 UserController 也在 classpath 上)
// 后继者:这人为什么在 Service 里调 Controller?
java 复制代码
// 文件:com/example/mapper/UserMapper.java

import com.example.controller.dto.LoginRequest;  // ❌ Mapper 依赖了 Controller 层的 DTO
// 编译器:✅ 没报错
// 结果:DTO 一改,Mapper 跟着编译不过------但只要改完 DTO 别忘了 Mapper 就行

单模块不意味着代码一定烂,但烂了编译器不会告诉你。

优点

优点 说明
心智负担最低 一个项目窗口看全部代码,新人 15 分钟就能开始改 bug
构建快 无模块间依赖解析,mvn compile 一条命令
重构灵活 IDE 拖拽就能把类从一个包移到另一个包
CI/CD 极简 一条流水线,一个产出物

缺点

缺点 说明
边界腐化 新人不知道(或不遵守)分层约定 → 半年后变成大泥球
循环依赖 AService → BService → AService,编译器不报,运行时 Spring 循环注入才炸
拆不开 想拆出独立服务时,先要手工梳理所有 import 关系
测试成本 单元测试经常不自觉地加载了整个 Spring Context

什么时候选它

  • 团队 ≤ 3 人,每个人都清楚分层约定
  • 代码 < 5 万行,预期生命周期 < 2 年
  • 快速验证、一次性项目、个人工具

三、形态二:分层模块单体(Layered Monolith)

是什么

把单模块按技术层 纵向切开,每个技术层变成一个独立的 Maven 模块。模块间的依赖方向通过 pom.xml 声明,编译器强制执行。

目录结构

复制代码
project/
├── pom.xml                        ← 父 POM(聚合所有模块)
├── project-web/                   ← Web 层(入口)
│   ├── pom.xml                    ← 依赖 project-service, project-common
│   └── src/main/java/.../
│       └── controller/            ← 只放 Controller
├── project-service/               ← 业务层
│   ├── pom.xml                    ← 依赖 project-mapper, project-common
│   └── src/main/java/.../
│       ├── service/               ← 业务逻辑
│       └── dto/                   ← 业务 DTO
├── project-mapper/                ← 数据访问层
│   ├── pom.xml                    ← 依赖 project-entity, project-common
│   └── src/main/java/.../
│       ├── mapper/                ← MyBatis Mapper 接口
│       └── mapper/xml/            ← SQL XML
├── project-entity/                ← 实体层
│   └── src/main/java/.../
│       └── entity/                ← 数据库实体类
└── project-common/                ← 公共层(最底层)
    └── src/main/java/.../
        ├── util/                  ← 通用工具
        ├── annotation/            ← 自定义注解
        └── constant/              ← 常量

典型代表

  • 若依 (RuoYi-Vue) --- 国内最知名的开源后台管理系统,ruoyi-admin / ruoyi-system / ruoyi-framework / ruoyi-common 四个核心模块
  • JeecgBoot --- 另一个国内流行的低代码平台,同样按 web/system/common 拆模块
  • 大量企业自研后台系统 --- 3-10 人团队,需要编译器约束防犯错

核心特征

模块边界 = 技术层的物理化。 之前靠包名分的层,现在变成了 Maven 模块,编译器开始管事了。

复制代码
┌──────────────────────────────────────────┐
│              project-web                  │
│           (Web 层 / 入口模块)              │
│                                            │
│   依赖 ↓ (pom.xml 里写 dependency)          │
├──────────────────────────────────────────┤
│            project-service                │
│            (业务逻辑层)                     │
│                                            │
│   依赖 ↓                                    │
├──────────────────────────────────────────┤
│            project-mapper                  │
│            (数据访问层)                     │
│                                            │
│   依赖 ↓                                    │
├──────────────┬───────────────────────────┤
│ project-entity│     project-common         │
│  (数据库实体)  │   (工具 / 注解 / 常量)       │
│              │     不依赖任何模块            │
└──────────────┴───────────────────────────┘

依赖方向:自上而下 ↓ (单向 DAG)
违规检测:编译期(Maven 找不到类 → 直接报错)

Maven 如何强制约束

xml 复制代码
<!-- project-web/pom.xml -->
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>project-service</artifactId>  <!-- ✅ Web 可以依赖 Service -->
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>project-common</artifactId>   <!-- ✅ Common 谁都能用 -->
    </dependency>
    <!-- 注意:这里绝对不能写 project-web 依赖自己 -->
</dependencies>
xml 复制代码
<!-- project-service/pom.xml -->
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>project-mapper</artifactId>   <!-- ✅ Service 依赖 Mapper -->
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>project-common</artifactId>
    </dependency>
    <!-- 这里不能写 project-web!Service 不能反向依赖 Web 层 -->
    <!-- 如果写了 → Maven 检测到循环依赖 → 编译失败 -->
</dependencies>

编译器阻断实例

java 复制代码
// 文件:project-service/.../service/UserService.java

// ✅ 同级或下层模块,随便引用
import com.example.common.utils.StringUtils;       // common 在所有模块的依赖链上
import com.example.entity.User;                    // entity 在 service 的依赖链上
import com.example.mapper.UserMapper;              // mapper 在 service 的依赖链上

// ❌ 上层模块 → 编译报错
import com.example.web.controller.UserController;  // 编译器:找不到这个类!
// 原因:project-service/pom.xml 里没写 project-web 的 dependency
// Maven 在编译 service 模块时,web 模块根本不在 classpath 上

这是分层模块单体和单模块单体最本质的区别------约束从"文档规范"变成了"编译器报错"。

优点

优点 说明
编译期强制分层 Service 引用 Controller → 直接编译失败
依赖方向可审计 mvn dependency:tree 打印完整的模块依赖图
增量编译 只改了 common → 只重编 common,不用全量
拆分成本降低 模块边界已划定,拆微服务时只需把 dependency 换成 RPC
按模块分人 A 负责 web 层,B 负责 service 层,C 负责 common

缺点

缺点 说明
横向改动面大 加一个"用户列表"功能,要在 web/service/mapper/entity 四个模块都加文件
业务内聚性差 用户相关的代码散落在 web/service/mapper/entity 四个模块
common 膨胀 什么工具都塞 common,最终变成垃圾桶模块
学习成本 新人要先看懂模块依赖图,才知道代码该放哪

什么时候选它

  • 团队 3-10 人,需要编译期防呆
  • 代码量 5-20 万行
  • 企业后台管理系统(RuoYi 的典型场景)
  • 确认未来可能拆微服务,但现阶段不想承担微服务运维成本

四、形态三:模块化单体(Modular Monolith)

是什么

把项目按业务领域竖向切开。每个模块内部有自己的 Controller、Service、Mapper、Entity------每个业务模块是一个"小单体"。模块间通过接口通信。

目录结构

复制代码
shop/
├── pom.xml                              ← 父 POM
├── shop-common/                         ← 共享内核(仅接口 + 值对象 + 事件)
│   └── src/main/java/com/shop/common/
│       ├── UserLookup.java              ← 接口定义(不是实现!)
│       ├── OrderConfirmedEvent.java      ← 领域事件类
│       └── Money.java                   ← 值对象
├── shop-user/                           ← 用户领域(完整的微型单体)
│   ├── pom.xml                          ← 依赖 shop-common
│   └── src/main/java/com/shop/user/
│       ├── UserController.java
│       ├── UserService.java
│       ├── UserMapper.java
│       ├── User.java
│       └── UserLookupImpl.java          ← 实现 common 定义的接口
├── shop-order/                          ← 订单领域
│   ├── pom.xml                          ← 依赖 shop-common
│   └── src/main/java/com/shop/order/
│       ├── OrderController.java
│       ├── OrderService.java
│       ├── OrderMapper.java
│       └── Order.java
├── shop-product/                        ← 商品领域
│   └── src/main/java/com/shop/product/
│       ├── ProductController.java
│       ├── ProductService.java
│       ├── ProductMapper.java
│       └── Product.java
└── shop-payment/                        ← 支付领域
    └── src/main/java/com/shop/payment/
        ├── PaymentController.java
        ├── PaymentService.java
        └── PaymentMapper.java

典型代表

  • DDD 经典示例项目(如 IDDD 书中的协作上下文例子)
  • Kamil Grzybek 的 Modular Monolith 示例
  • Spring Modulith 官方示例
  • 大型电商系统的初期形态 --- Shopify 在拆微服务前就是这个形态

核心特征

模块边界 = 业务领域。 和分层模块单体不同------这里"垂直切",而不是"水平切"。

复制代码
分层模块单体(水平切):              模块化单体(垂直切):

   web 层                              user 领域
   ┌────────────┐                      ┌──────────────────┐
   │UserController│                    │ UserController    │
   │OrderController│                   │ UserService       │
   │ProductController│                 │ UserMapper        │
   └────────────┘                      │ User.java         │
                                       └──────────────────┘
   service 层                          order 领域
   ┌────────────┐                      ┌──────────────────┐
   │UserService  │                     │ OrderController   │
   │OrderService │                     │ OrderService      │
   │ProductService│                    │ OrderMapper       │
   └────────────┘                      │ Order.java        │
                                       └──────────────────┘
   mapper 层                           product 领域
   ┌────────────┐                      ┌──────────────────┐
   │UserMapper   │                     │ ProductController │
   │OrderMapper  │                     │ ProductService    │
   │ProductMapper│                     │ ProductMapper     │
   └────────────┘                      │ Product.java      │
                                       └──────────────────┘

改"用户登录"→ 3 个模块都要动        改"用户登录"→ 只动 user 一个模块

模块间通信机制

关键原则:模块之间不能直接 import 对方的实现类,只能通过接口或事件。

方式一:接口 + 依赖注入
java 复制代码
// ======== shop-common:定义接口 ========
public interface UserLookup {
    UserInfo findById(Long userId);
}

// ======== shop-user:提供实现 ========
@Component
class UserLookupImpl implements UserLookup {
    private final UserMapper userMapper;

    public UserInfo findById(Long userId) {
        return userMapper.findById(userId).toInfo();
    }
}

// ======== shop-order:只依赖接口 ========
@Service
public class OrderService {
    private final UserLookup userLookup;  // 注入接口,不是具体实现

    public OrderDTO createOrder(CreateOrderRequest req) {
        UserInfo user = userLookup.findById(req.getUserId());
        // ...
    }
}

shop-orderpom.xml 里只依赖 shop-common(接口所在),不依赖 shop-user(实现在哪)。Spring 在运行时自动注入 UserLookupImpl

方式二:领域事件
java 复制代码
// ======== shop-order:发布事件 ========
@Service
@Transactional
public class OrderService {
    private final ApplicationEventPublisher events;

    public void confirmOrder(Long orderId) {
        Order order = orderMapper.findById(orderId);
        order.confirm();
        events.publishEvent(new OrderConfirmedEvent(order)); // 发出事件
    }
}

// ======== shop-user:订阅事件 ========
@Component
public class UserEventHandlers {
    @EventListener
    public void onOrderConfirmed(OrderConfirmedEvent event) {
        userService.addLoyaltyPoints(event.getUserId(), 100);
        // 订单模块完全不知道"用户模块在监听"
    }
}

事件机制让 shop-ordershop-user 之间零编译期依赖 。两者都只依赖 shop-common(事件类定义在那)。

优点

优点 说明
业务内聚性极高 改"订单"功能只需动 shop-order 一个模块
团队自治 A 团队全部在 shop-user 里干活,B 团队全部在 shop-order
天然可拆分 某个模块需要独立部署时,把接口调用换成 RPC/消息队列即可
模块边界清晰 一眼看出哪些代码属于哪个业务领域

缺点

缺点 说明
前期设计成本高 必须先识别业务边界------DDD 的限界上下文、事件风暴
接口维护成本 跨模块接口一改,实现方和调用方都要联动
过度工程化 3 个业务模块的小系统强行 DDD → 接口比业务代码还多
common 膨胀 什么接口和事件都往 common 塞,common 变成隐形的"大模块"
跨模块查询困难 不能写 JOIN,只能在代码层拼数据

什么时候选它

  • 业务领域边界明确且复杂(如电商、金融、物流)
  • 团队 > 10 人,按业务线分组开发
  • 未来确定会拆微服务,但想在单体阶段先验证领域模型
  • 用 Spring Modulith 或 ArchUnit 做编译期模块边界验证

五、三种形态一张表

单模块单体 分层模块单体 模块化单体
切分依据 不切(包名约定) 按技术层 按业务领域
Maven 模块数 1 4-10 5-20+
各模块内部 包 = 技术层 包 = 业务类 包 = 技术层
典型代表 Spring PetClinic、个人博客 若依 RuoYi-Vue DDD 电商参考实现
编译期隔离 ❌ 无 ✅ 技术层之间 ✅ 业务领域之间
循环依赖 ❌ 靠人发现 ✅ 编译报错 ✅ 编译报错
改一个功能 动 1 个模块(多个包) 动 3-4 个模块 动 1 个模块
新人上手 15 分钟 2 小时 1 天
拆微服务难度 极难 中等 容易
过度工程化风险
适合团队 1-3 人 3-10 人 10+ 人
适合代码量 < 5 万行 5-20 万行 > 10 万行
约束力来源 团队自律 Maven 依赖树 接口契约 + Maven

六、从单模块到模块化的演进路线

复制代码
项目从小到大的自然演变路径:

单模块单体 ────→ 分层模块单体 ────→ 模块化单体 ────→ 微服务
   │                 │                 │              │
   │                 │                 │              │
 阶段 1            阶段 2            阶段 3         阶段 4
 "一个人"          "一个组"          "多个组"       "多个团队"
 快速出活          需要防呆          需要自治       独立交付

什么时候升级?

  阶段 1 → 2 的信号:
    - 团队超过 3 人
    - Code Review 反复抓出"Service 引用了 Controller"
    - 新人入职后两个月还在放错包
    → 拆成分层模块单体,让编译器替你管

  阶段 2 → 3 的信号:
    - "加一个订单导出"要改 web/service/mapper/common 四个模块
    - "加一个用户标签"也是这四个模块
    - 每次上线都在改同一批模块,上线冲突频繁
    → 拆成模块化单体,按业务领域收拢代码

  阶段 3 → 4 的信号:
    - 某个业务模块(如支付)需要独立扩容
    - 某个业务模块需要独立技术栈(如 Go 重写)
    - 各业务线的发布节奏无法同步
    → 拆成微服务,独立部署

七、常见误区

误区 1:多模块 = 微服务

错。 若依有 6 个 Maven 模块,但最终打成 1 个 JAR,部署 1 个进程,连接 1 个数据库。它的运维模型是纯粹的单体。模块组织影响的是源码结构和编译约束,不影响部署拓扑。

误区 2:单模块 = 质量差

错。 单模块只说明没有编译期隔离,不说明代码耦合。一个包名清晰、分层严格的单模块项目,比一个 common 模块 500 个类的多模块项目更容易维护。

误区 3:模块化单体就是按业务建目录

没那么简单。 真正的模块化单体要求模块间不能直接 import 实现类 ,必须通过接口或事件通信。如果只是建了 user/order/product 三个目录但互相随便 import,那不叫模块化单体,叫"假装拆了的单模块单体"。

误区 4:项目大了必须上微服务

错。 Shopify 是 Rails 单体,撑到几十亿美元市值才拆。关键在于模块边界是否清晰------如果模块化单体做得好,一个 JAR 可以撑十年。


八、推荐阅读

  • Simon Brown --- Modular Monoliths (2020)
  • Kamil Grzybek --- Modular Monolith: A Primer (2019)
  • Sam Newman --- Monolith to Microservices (O'Reilly, 2019)
  • Vaughn Vernon --- Implementing Domain-Driven Design (Addison-Wesley, 2013)
  • Spring 官方 --- Spring Modulith 文档
相关推荐
生成论实验室3 小时前
《事件关系阴阳博弈动力学:识势应势之道》第八篇:认知与反思关系——探索、定位与延续
人工智能·算法·架构·知识图谱·创业创新
冷雨夜中漫步6 小时前
Claude Code源码分析——Claude Code 核心架构与关键模块实现设计
ai·架构·claude·claudecode
landuochong2006 小时前
给 Claude 订阅装一只电表 —— Claude API 多项目计量代理 `token-proxy` 实现详解
架构·claudecode
一个处女座的程序猿O(∩_∩)O6 小时前
大模型决战2026:从百模大战到空间智能,AI Agent与推理架构的深度实战
人工智能·架构
skilllite作者6 小时前
SkillLite 原生系统级沙箱功能代码导览
人工智能·chrome·后端·架构·rust
空中海8 小时前
03 性能、动画与 React Native 新架构
react native·react.js·架构
萑澈9 小时前
Ripple新前端框架的发展与AI原生全栈开发前景:架构重塑与生产力范式转移研究报告
架构·前端框架·ai-native
weixin_446260859 小时前
DeepDive:深度解析 DeepSeek V4 架构革新与长文本时代的算力重塑
架构
狂奔solar10 小时前
从“钢筋安装质量验收标准“谈起:知识库问答“多跳检索”架构演进与实践
架构·知识图谱·知识库溯源