软件架构的进化史,就是和「复杂度」相爱相杀的过程!
- 萌新期:就一个人单干,业务超简单,能跑起来就算成功!
- 成长期:业务越来越复杂,团队也人挤人,主打一个拒绝重复和混乱
- 大佬期:高并发、强安全一个不少,必须把模块拆得明明白白,还要有超强的扩展能力
业务刚开始,"能用" 才是王道;等业务体量上来了,"有序" 才是 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 为主,能跑就行
必备模块:只留「核心链路」
- Controller:请求「接收站」
比如用户点「登录」,它接住username和password,直接扔给 Service,自己不做复杂处理。
javascript
// 伪代码:极简Controller
app.post('/login', (req, res) => {
const { username, password } = req.body;
const result = userService.login(username, password); // 直接甩给Service
res.send(result);
});
- Service:业务「加工车间」
只关心「按规则处理数据」,不管数据从哪来、往哪去。例如,在一个电商订单处理系统中,订单计算折扣的 Service 伪代码如下:
ini
function calculateDiscount(order) {
// 获取订单总金额
totalAmount = order.totalAmount;
// 假设满100减20的规则
if (totalAmount >= 100) {
discount = 20;
} else {
discount = 0;
}
return discount;
}
- Model/DAO:数据「搬运工」
-
- Model:定义数据格式(比如用户必须有id和password);
-
- DAO:写 SQL 操作数据库(比如「用 username 查用户」)。
- 极简配置与错误处理
-
- Config:存数据库地址、端口(哪怕写死在代码里,先跑通);
-
- Error Handler:出错时至少返回500,别让页面白屏。
可选模块:别给自己加戏
- ORM?表少的话直接写原生 SQL 更快(比如select * from user where name = ?);
- 日志?console.log加个时间戳,调试够用就行。
核心逻辑:Controller → Service → DAO,一条线走到底,多余抽象都是负担。

二、中型应用:加模块是为了「少踩坑」
用户从几百涨到几万,业务从「增删改查」变成「带流程的业务」(比如下单要扣库存、减余额、记日志),团队也扩到 3-10 人 ------ 这时候「混乱」会拖慢所有人。
典型痛点
- 重复代码爆炸:每个 Controller 都写「登录检查」「操作日志」,改一处要同步改 10 个地方;
- 数据格式混乱:前端今天传userName,明天传user_name,Service 里全是兼容逻辑;
- 错误返回随机:有的接口返回字符串,有的返回对象,前端吐槽「又崩了」。
该加哪些模块?「缺啥补啥」
在小型项目基础上,按需加模块解决具体问题,就像面馆扩大后加保安、质检员:
- Middleware:给所有请求「设关卡」
把「登录检查」「日志记录」这些通用逻辑抽出来,所有请求先过一遍 Middleware。
scss
// 伪代码:登录检查Middleware
app.use((req, res, next) => {
if (!req.cookies.token) {
return res.status(401).send('请先登录');
}
next(); // 通过则进下一步
});
改一次 Middleware,全系统生效,再也不用在每个 Controller 里重复写校验。
- DTO + Pipe:给数据「画红线」
less
// DTO定义(NestJS示例)
class CreateOrderDto {
@IsNumber() // 必须是数字
goodsId: number;
@IsNumber()
@Min(1) // 至少1件
quantity: number;
}
这样 Service 里就不用处理格式问题,专心写「扣库存」逻辑。
-
- DTO:定义前端传参规则(比如创建订单必须有goodsId(数字)、quantity(>0));
-
- Pipe:自动校验参数(比如quantity是负数就直接返回错误)。
- 其他可选模块
-
- 增强日志:按模块输出(比如「订单模块」「用户模块」),记录接口耗时(方便排查慢接口);
-
- 内存缓存:给高频接口(比如商品详情)加缓存,减少数据库压力(比如用Map存热点数据);
-
- 分环境配置:开发 / 测试 / 生产用不同数据库地址,避免硬编码(比如用dotenv区分环境)。
核心逻辑:用模块解决「重复」和「混乱」,让团队协作更顺,而不是为了「架构好看」。
下图展示架构示意图,可选模块也全部列出,其中
- 核心业务模块 - 绿色系
- 安全与校验模块 - 蓝色系
- 基础设施模块 - 紫色系
- 数据流相关 - 橙色系
三、大型系统:模块要「扛住压力」
到了高并发场景(比如电商秒杀、社交平台),团队拆成多个子团队,系统开始往分布式 / 微服务走 ------ 这时候模块要支撑「高可用」「强安全」和「跨团队协作」。
典型痛点
- 单体扛不住:流量高峰时数据库崩了,整个系统瘫痪;
- 权限管理复杂:不同角色能访问的接口完全不同(比如游客看商品,管理员改价格);
- 跨服务排查难:一个请求经过 5 个服务,出问题不知道在哪一环卡壳。
模块要「精细化」,但核心链路不变
在中型基础上,加更专业的模块应对压力:
- Guard:接口权限「细粒度控制」
比 Middleware 更灵活,能按「接口 + 角色」控制权限。比如:
less
// 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() {}
}
- 分布式工具链
-
- 分布式缓存(Redis):多实例共享缓存,避免重复查库;
-
- 消息队列(Kafka/RabbitMQ):秒杀时先把订单请求扔进队列,异步处理(防止系统被冲垮);
-
- 配置中心(Nacos):多环境动态更新配置(比如突然改 Redis 地址,不用重启服务);
-
- 分布式追踪(Jaeger):画一条请求链路图,看经过哪些服务、在哪耗时最长。
- 更细的异常处理
用 Filter 细分异常类型(比如参数错、权限错、数据库错),配合全局 Error Handler 返回标准化错误(前端再也不用猜格式)。
下图展示架构示意图,可选模块也全部列出,其中
- 核心业务模块 - 绿色系
- 安全与校验模块 - 蓝色系
- 基础设施模块 - 紫色系
- 数据流相关 - 橙色系

常见误区:这些模块不是一回事!
(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 / 队列 / 分布式工具) |
记住:架构不是「炫技」,而是「实事求是」------
- 业务简单时,别强行加一堆模块(摆摊不用建写字楼);
- 业务复杂后,别舍不得拆模块(大公司必须分部门)。
你现在的项目处于哪个阶段?遇到过哪些「架构坑」?欢迎评论区聊聊~ 👇