没有最好的架构,只有合适的架构:模块化单体的选型与演进

SailTrack
2026-05-09
点 赞
1
热 度
9
评 论
0
  1. 首页
  2. 后端开发
  3. 没有最好的架构,只有合适的架构:模块化单体的选型与演进

一、从考勤到Agent平台——模块是怎么长出来的

Workforce Hub 最早只有一个模块:员工考勤。

就是上学期期末做的那个打卡平台,一个 Spring Boot 工程,几张表,CRUD 搞定。说实话,那时候根本不需要考虑什么架构——总共没几个类,全塞一个包都跑得好好的。

变化是从这学期开始的。

老师说可以优化成毕设,我就开始往上加东西。先是加了假期管理(请假总得有个地儿查余额吧),然后又加了审批流程(请假不能自己批自己吧),再然后加了即时消息(审批结果总得通知一下别人吧)。到这里其实还好,都是围绕考勤一条业务线在扩展。

真正让架构"绷不住"的是 Agent 和文件系统这两个模块。

Agent 模块要调用工作流引擎、要查知识库、要发消息——它天然就和好几个模块都有关系。文件系统更麻烦,我一开始心里想的是"个人文件和企业文件分开,用不同的存储策略",代码写出来却是强耦合——改企业文件的上传逻辑,个人文件那边也会受影响。

做文件系统那段时间说实话挺沮丧的。明明设计阶段画图的时候分得清清楚楚,一写代码就不自觉地把它们揉在一起了。后来专门花了一周重构,才把文件系统的个人域和企业域真正拆开。

也就是从那个时候开始,我才真正理解了什么叫"模块边界"——不是你画了线就算分开了,是改 A 模块的时候 B 模块真的不受影响,才算分开了。

二、为什么不是微服务?——一个实用主义者的选择

聊架构选型的时候,我被问过最多的问题就是:为什么不做微服务?

说实话,我也纠结过。现在网上到处都在讲微服务,Spring Cloud、Docker、Kubernetes 这些名词天天在技术社区刷屏。如果你说你用单体架构,总有一种"是不是我不会微服务"的心虚感。

但我最后还是选了模块化单体。不是因为我不会微服务,是因为以项目目前的功能规模,做微服务没有明显的性能收益,反而会带来几个实实在在的问题:

开发周期会明显变长。 微服务不是把代码拆开就完了——你要搞定服务注册发现、远程调用、分布式事务、统一配置中心、链路追踪……这些东西每一个都是时间。一个学期的时间本来就紧,再把这些基础设施全搭一遍,业务代码都不用写了。

运维复杂度。 微服务意味着每个服务独立部署、独立扩容。对于我这个一个人开发的项目来说,光是本地调试就要同时起好几个服务,内存先吃没了。更别说 Docker Compose 编排、日志聚合这些配套设施。

当前规模不需要。 Workforce Hub 目前是单企业部署的,用户量级和并发量都没到单体扛不住的程度。在性能瓶颈没有出现之前,拆微服务属于过早优化——花了巨大的成本,解决了一个你暂时没有的问题。

模块化单体和微服务不是"高级"和"低级"的区别,是在不同约束条件下做的不同选择。你手里有一把锤子,你要钉的是一颗钉子——这时候给你一个液压打桩机,不是更好,是更麻烦。

三、模块化单体怎么玩——SPI 解耦与清晰的模块边界

选了单体不代表不用设计架构。恰恰相反,正因为所有代码都在一个进程里跑,模块之间更容易"不小心黏在一起",架构约束反而要更严格。

3.1 目录级别的物理隔离

每个业务模块有自己独立的包和依赖:

server/src/main/java/com/workforcehub/
├── module-auth/          # 认证模块
├── module-org/           # 组织人员模块
├── module-attendance/    # 考勤模块
├── module-leave/         # 假期模块
├── module-workflow/      # 审批流程模块
├── module-im/            # 即时消息模块
├── module-doc/           # 文档管理模块
├── module-kb/            # 知识库模块
├── module-agent/         # Agent 平台模块
├── module-file/          # 文件中心模块
├── module-notify/        # 通知模块
├── module-hr/            # 员工生命周期模块
├── module-admin/         # 后台管理模块
├── module-connector/     # 外部连接器模块
├── module-common/        # 公共模块
└── module-spi/           # SPI 接口定义

关键设计:业务模块之间不互相 import。比如 module-leave 不会直接依赖 module-attendance 的代码——如果请假需要联动考勤,通过 SPI 接口来通信。

3.2 SPI:让模块通信但不依赖

SPI(Service Provider Interface)是整个架构里我最满意的一个设计决策。它的作用说白了就是:模块之间可以互相调用,但编译时互不依赖。

具体怎么做的:

// 文件位置:module-spi/src/main/java/.../spi/LeaveQueryService.java
// 这是一个接口,定义在 SPI 模块里,不归属任何业务模块

public interface LeaveQueryService {
    /**
     * 查询某员工在某段时间内的假期使用情况
     */
    List<LeaveRecord> queryLeaveRecords(String employeeId, 
                                        LocalDate start, 
                                        LocalDate end);
}

module-attendance 需要知道请假信息来重算考勤结果,但它不需要知道 module-leave 里的任何实现细节——它只依赖 SPI 接口。module-leave 提供这个接口的实现:

// 文件位置:module-leave/src/main/java/.../spi/LeaveQueryServiceImpl.java

@Service
public class LeaveQueryServiceImpl implements LeaveQueryService {
    @Override
    public List<LeaveRecord> queryLeaveRecords(...) {
        // module-leave 内部的数据库查询逻辑
    }
}

然后 Spring Boot 自动发现实现类,注入到考勤模块里。考勤模块的代码里只出现 LeaveQueryService 这个接口,不会出现任何 module-leave 的类名。

这样做的好处:如果将来真的要把假期模块拆成一个独立微服务,我只需要把 LeaveQueryServiceImpl 从本地实现换成远程调用实现,考勤模块的代码一行都不用改。

3.3 目前完成的模块

所有模块都遵循同一个包结构规范:

module-xxx/
├── dto/           # 数据传输对象
├── repository/    # 数据访问接口
├── mapper/        # MyBatis 映射文件
├── controller/    # REST 接口
├── service/       # 业务逻辑
├── spi/           # SPI 接口实现(如果需要)
└── domain/        # 领域实体

到目前为止,认证、组织、文件中心、审批流程、假期、考勤这六个模块已经完成了可运行的闭环。Agent、知识库、即时消息等模块还在开发中。

四、踩过的坑——文件中心的强依赖教训

前面提了一嘴,这里展开讲。

文件中心(module-file)的设计初衷是把个人文件和企业文件分开管理:

  • 个人文件:员工上传的头像、个人资料附件,存在个人空间,用完即删

  • 企业文件:审批附件、知识库文档、公司公告,存在企业空间,长期保留,有版本管理

画图的时候,我画了两个服务:PersonalFileServiceEnterpriseFileService,各自负责各自的存储策略。看起来很干净。

写代码的时候就出问题了——为了"方便",我在 EnterpriseFileService 里直接调了 PersonalFileService 的方法来处理企业用户的头像上传。“就一行代码的事儿”,我当时这么想。

结果后面改个人文件的上传流程(加了文件类型校验),企业那边的头像上传直接挂了。调试了半天才发现,两个八竿子打不着的功能被一行 import 绑在了一起。

教训:模块解耦不能靠"心里记住",得靠编译器帮你把关。如果两个模块不应该有依赖关系,那就真的不能让它们互相 import。后来我把企业头像上传的逻辑完全重写,不再复用个人文件的服务,两个模块彻底独立。

这也让我更坚定了一开始就设计 SPI 层这个决策——如果文件中心也用 SPI 来隔离个人和企业两个域,就不会犯这个错误。

五、总结

核心收获

  • 架构选型的第一原则不是"选最先进的",是"选最合适的"——以当前项目的规模,模块化单体是性价比最高的方案

  • 选了单体不代表不做设计——目录隔离和 SPI 解耦是两个关键的架构约束,能防止模块暗中耦合

  • 模块边界不是你画了就算——只有"改 A 模块不影响 B 模块"才算真正解耦。编译器级别的隔离比人脑记住靠谱

  • 如果将来业务体量真的超出了单体的承载能力,拆微服务的成本是可控的——因为 SPI 已经把接口定义好了

下一步计划

  • 完成 Agent 模块与知识库模块的 SPI 对接

  • 完善文件中心的企业空间版本管理与去重

  • 补 Docker Compose 多服务编排,为可能的拆分做准备

我个人对架构的理解:没有最好的架构,只有合适的架构。当前最适合这个项目的就是模块化单体。如果后续迭代中功能持续膨胀、出现了单体架构扛不住的场景,那我也做好了拆成微服务的准备。架构不是信仰,是工具——哪个好用用哪个。


让我们忠于理想,让我们面对显示

SailTrack

entp 辩论家

站长

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

目录

欢迎来到SailTrack的站点,为您导航全站动态

30 文章数
11 分类数
3 评论数
23标签数