全栈必知:从小单体到大系统,模块怎么长出来的?

2 阅读8分钟

软件架构的进化史,就是和「复杂度」相爱相杀的过程!

  • 萌新期:就一个人单干,业务超简单,能跑起来就算成功!
  • 成长期:业务越来越复杂,团队也人挤人,主打一个拒绝重复和混乱
  • 大佬期:高并发、强安全一个不少,必须把模块拆得明明白白,还要有超强的扩展能力

业务刚开始,“能用” 才是王道;等业务体量上来了,“有序” 才是 YYDS!模块设计的精细度和职责划分,也得跟着一路升级。

本文将沿着 "小型单体→中型应用→大型微服务" 的路径,结合不同规模项目的必备模块与可选模块,解析模块设计如何逐步适应不同阶段的需求。

划重点:本文模块名称参考 NestJS、Spring Boot、Express 这些常见后端框架,按职责分层,方便不同技术栈的小伙伴都能看懂~

先统一「语言」:模块分层到底是啥?

不同框架(NestJS/Spring Boot/Express)的模块命名可能不一样,但职责层次相通。先明确这几个核心模块,避免后面聊懵:

层次核心模块通俗理解
请求入口层Controller + Middleware/Interceptor「前台接待」:接请求、做通用处理(日志 / 鉴权)
业务逻辑层Service + Guard + Pipe/DTO「后厨加工」:核心逻辑、权限判断、数据校验
数据访问层Model/DAO + ORM「仓库管理」:定义数据格式、操作数据库
横切支持层Logger/Config/Cache/Queue 等「后勤保障」:日志、配置、缓存、异步任务等

有了这个「分层字典」,后面聊不同阶段的模块设计就清晰多了。

一、小型单体:用最少的模块「跑起来」再说

典型场景:个人博客(文章 CRUD + 登录)、小商城(商品 + 下单)、数据看板(读库展示)

特点:2 张表撑全场,1-2 人开发,CRUD 为主,能跑就行

必备模块:只留「核心链路」

  1. Controller:请求「接收站」

比如用户点「登录」,它接住username和password,直接扔给 Service,自己不做复杂处理。

// 伪代码:极简Controller
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const result = userService.login(username, password); // 直接甩给Service
  res.send(result);
});

2. Service:业务「加工车间」

只关心「按规则处理数据」,不管数据从哪来、往哪去。例如,在一个电商订单处理系统中,订单计算折扣的 Service 伪代码如下:

function calculateDiscount(order) {
    // 获取订单总金额
    totalAmount = order.totalAmount;
    // 假设满100减20的规则
    if (totalAmount >= 100) {
        discount = 20;
    } else {
        discount = 0;
    }
    return discount;
}

3. Model/DAO:数据「搬运工」

    • Model:定义数据格式(比如用户必须有id和password);
    • DAO:写 SQL 操作数据库(比如「用 username 查用户」)。
  1. 极简配置与错误处理
    • Config:存数据库地址、端口(哪怕写死在代码里,先跑通);
    • Error Handler:出错时至少返回500,别让页面白屏。

可选模块:别给自己加戏

  • ORM?表少的话直接写原生 SQL 更快(比如select * from user where name = ?);
  • 日志?console.log加个时间戳,调试够用就行。

核心逻辑:Controller → Service → DAO,一条线走到底,多余抽象都是负担。

img_v3_02oq_1cbe27f7-c17f-484c-871e-3cc7db72427g.jpg

二、中型应用:加模块是为了「少踩坑」

用户从几百涨到几万,业务从「增删改查」变成「带流程的业务」(比如下单要扣库存、减余额、记日志),团队也扩到 3-10 人 —— 这时候「混乱」会拖慢所有人。

典型痛点

  • 重复代码爆炸:每个 Controller 都写「登录检查」「操作日志」,改一处要同步改 10 个地方;
  • 数据格式混乱:前端今天传userName,明天传user_name,Service 里全是兼容逻辑;
  • 错误返回随机:有的接口返回字符串,有的返回对象,前端吐槽「又崩了」。

该加哪些模块?「缺啥补啥」

在小型项目基础上,按需加模块解决具体问题,就像面馆扩大后加保安、质检员:

  1. Middleware:给所有请求「设关卡」

把「登录检查」「日志记录」这些通用逻辑抽出来,所有请求先过一遍 Middleware。

// 伪代码:登录检查Middleware
app.use((req, res, next) => {
  if (!req.cookies.token) {
    return res.status(401).send('请先登录');
  }
  next(); // 通过则进下一步
});

改一次 Middleware,全系统生效,再也不用在每个 Controller 里重复写校验。

  1. DTO + Pipe:给数据「画红线」
// DTO定义(NestJS示例)
class CreateOrderDto {
  @IsNumber() // 必须是数字
  goodsId: number;
  @IsNumber()
  @Min(1) // 至少1件
  quantity: number;
}

这样 Service 里就不用处理格式问题,专心写「扣库存」逻辑。

    • DTO:定义前端传参规则(比如创建订单必须有goodsId(数字)、quantity(>0));
    • Pipe:自动校验参数(比如quantity是负数就直接返回错误)。
  1. 其他可选模块
    • 增强日志:按模块输出(比如「订单模块」「用户模块」),记录接口耗时(方便排查慢接口);
    • 内存缓存:给高频接口(比如商品详情)加缓存,减少数据库压力(比如用Map存热点数据);
    • 分环境配置:开发 / 测试 / 生产用不同数据库地址,避免硬编码(比如用dotenv区分环境)。

核心逻辑:用模块解决「重复」和「混乱」,让团队协作更顺,而不是为了「架构好看」。

下图展示架构示意图,可选模块也全部列出,其中

  • 核心业务模块 - 绿色系
  • 安全与校验模块 - 蓝色系
  • 基础设施模块 - 紫色系
  • 数据流相关 - 橙色系 img_v3_02oq_3ec3267e-a5b3-41e5-a35f-5f82e3aaeb1g.jpg

三、大型系统:模块要「扛住压力」

到了高并发场景(比如电商秒杀、社交平台),团队拆成多个子团队,系统开始往分布式 / 微服务走 —— 这时候模块要支撑「高可用」「强安全」和「跨团队协作」。

典型痛点

  • 单体扛不住:流量高峰时数据库崩了,整个系统瘫痪;
  • 权限管理复杂:不同角色能访问的接口完全不同(比如游客看商品,管理员改价格);
  • 跨服务排查难:一个请求经过 5 个服务,出问题不知道在哪一环卡壳。

模块要「精细化」,但核心链路不变

在中型基础上,加更专业的模块应对压力:

  1. Guard:接口权限「细粒度控制」

比 Middleware 更灵活,能按「接口 + 角色」控制权限。比如:

// Guard示例(NestJS)
@Injectable()
class AdminGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const user = context.switchToHttp().getRequest().user;
    return user.role === 'admin'; // 只有管理员能访问
  }
}
// 用在需要权限的接口上
@Controller('goods')
class GoodsController {
  @Post()
  @UseGuards(AdminGuard) // 只有管理员能创建商品
  create() {}
}

2. 分布式工具链

    • 分布式缓存(Redis):多实例共享缓存,避免重复查库;
    • 消息队列(Kafka/RabbitMQ):秒杀时先把订单请求扔进队列,异步处理(防止系统被冲垮);
    • 配置中心(Nacos):多环境动态更新配置(比如突然改 Redis 地址,不用重启服务);
    • 分布式追踪(Jaeger):画一条请求链路图,看经过哪些服务、在哪耗时最长。
  1. 更细的异常处理

用 Filter 细分异常类型(比如参数错、权限错、数据库错),配合全局 Error Handler 返回标准化错误(前端再也不用猜格式)。

下图展示架构示意图,可选模块也全部列出,其中

  • 核心业务模块 - 绿色系
  • 安全与校验模块 - 蓝色系
  • 基础设施模块 - 紫色系
  • 数据流相关 - 橙色系

img_v3_02oq_201e8eef-38a4-4a6d-a77c-6656c7982afg.jpg

常见误区:这些模块不是一回事!

(1)Guard、Pipe 是中间件吗?

不是。它们的分工和工作时机完全不同:

模块工作内容工作时机通俗比喻
Middleware处理所有请求的通用逻辑最早接触请求,能改请求 / 响应小区大门保安(查健康码、登记)
Guard判断 "有没有权限访问"中间件之后,控制器之前会议室门禁(没卡不让进)
Pipe校验数据格式、转换数据类型守卫之后,控制器处理数据前快递安检机(检查包裹里有没有违禁品)

(2)Pipe 和 DTO 重复吗?

完全不重复。DTO 是 "规则手册",Pipe 是 "执法人员":

  • DTO 规定 "密码必须≥8 位"(静态规则);
  • Pipe 负责检查 "这个密码是不是真的≥8 位"(动态执行)。

举例:LoginDTO写着 "password 长度≥8",PasswordPipe则在每次登录时实际检查,如果不够就返回错误。

(3)为什么大型项目必须有 Cache 和 Queue?

  • Cache:高并发场景下,数据库每秒能处理的请求有限(比如 1 万次 / 秒),而 Cache(如 Redis)能处理百万级请求 / 秒,通过缓存热点数据(如首页商品)可避免数据库过载;
  • Queue:当用户下单后需要发送短信、生成物流单等耗时操作时,直接在接口中处理会导致响应变慢,用 Queue 异步处理可让接口快速返回,后续由消费端慢慢处理任务。

最后:架构演进的「底层逻辑」

从单体到分布式,模块越拆越多,但核心永远是「解决具体问题」:

阶段核心目标模块设计原则
小型单体快速落地核心链路(Controller→Service→DAO)+ 极简支撑
中型应用减少重复、规范协作补横切模块(Middleware/DTO/Pipe)
大型系统高可用、可运维、跨团队协作精细化模块(Guard / 队列 / 分布式工具)

记住:架构不是「炫技」,而是「实事求是」——

  • 业务简单时,别强行加一堆模块(摆摊不用建写字楼);
  • 业务复杂后,别舍不得拆模块(大公司必须分部门)。

你现在的项目处于哪个阶段?遇到过哪些「架构坑」?欢迎评论区聊聊~ 👇