204|架构反模式:大泥球、分布式单体、共享数据库、循环依赖和过度设计

架构设计与系统设计约 19 分钟阅读10,712 字符
知识库文档 205 / 251

阅读时间:40-50 分钟 适用对象:高级 Java 工程师、技术专家、架构师 前置知识:微服务架构、分布式系统基础、设计模式基础


一、什么是架构反模式

架构反模式(Architecture Anti-Pattern)是指在软件系统设计中,那些看似能解决问题、实则会把系统推向深渊的设计决策。它们不是"语法错误"——系统照样能跑,业务照样能上线,但每次迭代都会让你多掉一层皮。

反模式与"坏代码"的区别在于作用范围:一段烂代码只会让一个类难以维护,一个架构反模式会让整个系统难以演进。更危险的是,反模式常常戴着"最佳实践"的面具出现——微服务本来是为了解耦,结果搞成了分布式单体;分层本来是为了隔离,结果搞成了"千层饼"。

本文拆解五个最常见的架构反模式,逐一分析它们的产生原因、识别信号、危害机制和治理方案。


二、大泥球(Big Ball of Mud)

2.1 什么是大泥球

大泥球是软件架构中最古老、最普遍的反模式。Brian Foote 和 Joseph Yoder 在 1997 年首次系统性地描述了它:一个没有可辨识结构的系统,代码像一团乱麻,模块边界模糊,依赖关系随意穿插,任何修改都像在沼泽里行走——每一步都艰难,每一步都可能踩到陷阱。

大泥球通常不是设计出来的,而是"长"出来的。一个系统最初可能结构清晰,但在持续的业务压力下,开发者不断走捷径——"先硬编码,后面再重构"——直到"后面"永远不会来。

文章配图
文章配图

2.2 大泥球的典型特征

特征 表现
无清晰模块边界 包结构按技术层(controller/service/dao)而非业务领域划分,改一个功能要跨 10+ 个包
循环依赖泛滥 A 依赖 B,B 依赖 C,C 又依赖 A,构建工具报循环依赖警告时直接关掉检查
全局状态横行 到处都是 static 变量、ThreadLocal 滥用、单例满天飞,数据流无法追踪
上帝类 一个类 3000+ 行,承担了订单处理、库存扣减、通知发送、日志记录等所有职责
拷贝粘贴编程 相似逻辑散落在 20 个地方,改一个 bug 要改 20 处
测试缺失 无法写单元测试,因为所有东西都耦合在一起,只能靠"人肉回归"

2.3 大泥球为什么会产生

根本原因只有一个:缺乏架构治理。

具体来说:

  1. 时间压力:业务方说"下周必须上线",技术债不断累积,从不偿还
  2. 缺乏架构评审:没人 review 设计决策,每个人按自己的理解写代码
  3. 人员流动:原始设计者离职,新人看不懂原有架构,只能在外围打补丁
  4. 没有重构文化:团队认为"能跑就行",重构被视为"不产出业务价值的工作"
  5. 技术债务不可见:PM 看得见新功能,看不见技术债,永远排不上优先级

2.4 大泥球的危害

  • 修改成本指数级增长:第 1 年加功能要 1 天,第 3 年要 1 周,第 5 年要 1 个月
  • 牵一发而动全身:改一个"小功能"导致 3 个线上事故
  • 新人上手周期长:新员工需要 3 个月才能独立开发,而且处处踩坑
  • 无法独立部署和扩展:所有功能绑在一起,热点模块拖垮整个系统

2.5 治理大泥球

治理大泥球不能推倒重来——Big Bang 重写大概率失败。正确的做法是渐进式改造:

第一步:冻结恶化。 制定编码规范,新代码必须遵循分层/模块化约束,禁止新增跨模块的随意依赖。可以用 ArchUnit 这类工具在 CI 中做架构约束检查。

第二步:识别接缝。 找到系统中相对独立的业务能力,比如"用户模块"、"库存模块"、"支付模块",从这些接缝处开始拆分。

第三步:绞杀者模式(Strangler Fig Pattern)。 逐步将旧模块的功能迁移到新模块,新旧并存一段时间,旧模块的调用逐步被新模块替代,直到旧模块可以安全下线。

第四步:建立技术债看板。 把技术债可视化,每个迭代分配固定比例(如 20%)的时间偿还技术债。

关键认知:治理大泥球不是技术问题,是组织问题。如果团队文化不改变,治理完一轮又会回到泥球状态。


三、分布式单体(Distributed Monolith)

3.1 什么是分布式单体

分布式单体是微服务时代最讽刺的反模式。团队把系统拆成了几十个微服务,但服务之间耦合得比单体还紧——改一个功能要同时部署 5 个服务,一个服务挂了整个系统不可用,服务间通过同步 RPC 形成了长调用链。

它比单体还糟糕:你获得了微服务所有的复杂度(网络延迟、分布式事务、运维成本),却没有得到微服务应有的收益(独立部署、独立扩展、技术异构)。

3.2 分布式单体的典型特征

特征 表现
同步调用链过长 A → B → C → D → E,一个请求穿过 5 个服务,RT 是各段之和再加网络开销
必须一起部署 改了 A 服务,B/C/D 也必须同时发布,因为接口契约紧密耦合
共享数据库 多个服务直连同一个数据库,一个服务改了表结构,其他服务跟着炸
共享 jar 包 用 common 包共享 DTO,改了 common 所有服务都得升版本
分布式事务 一个业务操作跨 3 个服务,用分布式事务保证一致性,复杂度爆炸
没有独立的发布节奏 所有服务跟着"大版本"一起发,微服务只是物理上的拆分,没有组织的拆分

3.3 分布式单体为什么会产生

根本原因:只拆了部署单元,没拆业务边界。

微服务的核心不是"把服务拆小",而是"按业务边界拆"。很多团队从单体往微服务迁移时,只是机械地把 controller 拆成 A 服务、service 拆成 B 服务、dao 拆成 C 服务——这是按技术层拆分,不是按业务域拆分。结果服务间的调用频率比单体内部的函数调用还高。

另一个常见原因:只用了微服务基础设施,没建立微服务团队组织。 康威定律(Conway's Law)说,系统架构会反映组织的沟通结构。如果团队还是按技术职能(前端组、后端组、DBA 组)划分,而不是按业务域(订单团队、支付团队、用户团队)划分,那么无论技术栈多"微服务",做出来的系统都倾向于分布式单体。

文章配图
文章配图

3.4 分布式单体 vs 单体 vs 微服务的对比

维度 单体 分布式单体 真正的微服务
部署单元 1 个 N 个 N 个
独立部署 ❌ ❌ ✅
独立扩展 ❌ ❌ ✅
技术异构 ❌ ❌ ✅
网络开销 无 有(大量) 有(可控)
运维复杂度 低 高 中-高
团队自治 ❌ ❌ ✅
总体评价 适合小团队 最差选择 适合大团队

3.5 治理分布式单体

第一步:识别真正的业务边界。 用 DDD(领域驱动设计)的限界上下文(Bounded Context)重新审视系统。不是按表拆分,也不是按 API 拆分,而是按业务能力拆分。

第二步:消除共享数据库。 每个微服务应该拥有自己的数据存储。跨服务的数据访问通过 API 而非直接查表。这是最难的一步,可能需要数据迁移和外键关系的重构。

第三步:用异步解耦替代同步调用链。 对于不需要同步返回结果的场景,用消息队列替代 RPC 调用。比如"下单后发短信"不需要同步等待,发一条消息给通知服务即可。

第四步:建立服务契约。 明确每个服务的接口契约(API 定义、消息格式、SLA),通过契约测试保证兼容性。

一句话总结:分布式单体治不好只是因为"拆了但没完全拆"——如果你把一个紧密耦合的逻辑拆到两个服务里,它们必然还是会紧密耦合。


四、共享数据库(Shared Database)

4.1 什么是共享数据库反模式

共享数据库反模式是指多个微服务或多个业务模块直接访问同一个数据库实例的同一张表(或同一个 Schema)。这是一种"集成数据库"(Integration Database)模式,看似方便,实则是耦合的温床。

文章配图
文章配图

4.2 共享数据库的表现形式

形式一:多服务直连同一张表。 订单服务和库存服务都直接读写 inventory 表,各自维护自己的库存扣减逻辑。当库存扣减规则需要变更时,两个服务都要改,而且很容易改得不一致。

形式二:通过视图或存储过程共享。 号称"服务间不直接访问表",但通过数据库视图或存储过程对外暴露数据,本质上还是耦合。存储过程更是把业务逻辑塞进了数据库,既难版本控制又难测试。

形式三:共享同一个 Schema。 不同服务使用同一个数据库的不同表,但属于同一个 Schema。一个服务对表结构的变更(如加字段、改类型)可能影响其他服务——比如某个服务执行了一个慢查询导致整个数据库实例负载飙升,其他服务跟着遭殃。

4.3 共享数据库为什么会产生

表面上:共享数据库简单、方便,不用考虑数据同步,不用处理分布式事务,一张 JOIN 搞定所有查询。

深层上:团队没有建立"数据即边界"的意识。很多人认为微服务拆分只是"把代码拆开",数据层可以共享——这是对微服务最深的误解。

还有一种情况是历史遗留:从单体拆成微服务时,数据库没跟着拆,新服务和老服务共享同一个库。因为数据库拆分涉及数据迁移,风险大、周期长,所以一直搁置。

4.4 共享数据库的具体危害

1. 表结构变更连锁爆炸。 服务 A 加了一个 NOT NULL 字段但忘了给默认值,服务 B 的 INSERT 语句直接报错。

2. 无法独立扩展。 订单表的数据量暴涨导致整个数据库实例 CPU 飙高,用户服务虽然只有 10 万条数据,也跟着一起慢。

3. 无法独立选择存储。 订单服务需要关系型数据库,搜索服务需要 Elasticsearch,但共享数据库把所有服务都绑死在了 MySQL 上。

4. 测试数据污染。 服务 A 的集成测试写入了测试数据,服务 B 的测试读到了脏数据,测试变得不可靠。

5. 无法独立演进。 服务 A 想把订单表从 MySQL 迁移到 TiDB,但服务 B 和 C 还在用这张表,迁移变成了一场需要协调所有人的大工程。

4.5 治理共享数据库

方案一:数据库按服务拆分(Database per Service)。 每个微服务拥有自己独立的数据库实例。这是最彻底的方案,也是最难的方案。需要处理数据迁移、外键解除、跨服务数据查询等挑战。

方案二:混合过渡。 先拆表——把不同业务域的表迁移到不同的数据库实例,但允许服务间通过 API 查询数据。外键关系逐步解除,跨库 JOIN 改为服务层聚合。

方案三:数据复制(CQRS)。 对于需要跨服务查询的场景,用 CDC(Change Data Capture)将数据变更事件同步到下游服务的只读副本。比如订单服务有订单数据,报表服务通过 CDC 同步一份订单数据的只读视图。

方案四:API 组合。 对于简单的跨服务查询,在 API 网关层或 BFF 层做数据聚合,而不是在数据库层做 JOIN。

关键认知:共享数据库的反模式不是"多个服务用同一个数据库实例",而是"多个服务写同一张表"。如果只是读取别服务的表,用只读副本或 API 的方式更可控。


五、循环依赖(Circular Dependency)

5.1 什么是循环依赖

循环依赖是模块间形成了一种环状的依赖关系:A 依赖 B,B 依赖 C,C 又依赖 A。在代码层面,表现为包之间的循环引用;在服务层面,表现为服务 A 调用服务 B,服务 B 又回调服务 A;在数据层面,表现为表 A 外键引用表 B,表 B 外键引用表 A。

文章配图
文章配图

5.2 循环依赖的层次

代码层循环依赖:两个包或两个类互相引用。Spring 中通过三级缓存解决了构造器循环依赖,但 setter 循环依赖仍然是隐患。更重要的是,代码层的循环依赖破坏了模块的内聚性——A 和 B 互相依赖,说明它们本应属于同一个模块。

服务层循环依赖:订单服务调用支付服务发起支付,支付服务在支付成功后回调订单服务更新状态。两个服务形成了双向依赖,部署顺序、故障隔离、灰度发布都变得复杂。

数据层循环依赖:组织表有 parent_id 外键引用自身(自引用),这是合法的。但部门表引用员工表(manager_id),员工表又引用部门表(dept_id),就是循环依赖——INSERT 时先插哪个都违反外键约束。

5.3 循环依赖的识别

工具检测:

  • Java:JDepend、ArchUnit、SonarQube 的 squid:CircularDependenciesBetweenPackages
  • 服务层:链路追踪工具(如 Jaeger、SkyWalking)可以看到调用链是否形成环
  • 数据库:INFORMATION_SCHEMA.KEY_COLUMN_USAGE 查询外键引用关系

经验信号:

  • 每次启动服务都报循环依赖警告,但"不影响运行"就忽略了
  • 改模块 A 时,模块 B 和 C 的测试也挂了
  • 发布时发现服务 A 必须先于 B 部署,但 B 又必须等待 A 的某个接口注册

5.4 消除循环依赖

代码层消除:

  1. 提取接口/抽象:A 依赖 B 的接口而非具体实现,B 实现该接口,方向变为 A→接口←B,打破循环
  2. 引入中间层:将 A 和 B 共同依赖的逻辑提取到 C,A→C←B
  3. 合并模块:如果 A 和 B 确实密不可分,不如合并为一个模块,至少内聚性是对的
  4. 事件驱动:A 发布事件,B 订阅事件,使 A 不需要直接知道 B 的存在

服务层消除:

  1. 异步回调替代同步回调:订单服务发消息给支付服务,支付完成后发消息给订单服务,避免同步 RPC 调用链
  2. 引入中间协调服务:订单和支付都不直接回调对方,而是通过一个"交易协调服务"编排流程
  3. 重新审视服务边界:如果两个服务频繁互相调用,可能它们本应是一个服务

数据层消除:

  1. 弱化外键约束:在应用层维护引用完整性,而不是在数据库层
  2. 单向引用:选择一方作为"主"方向,另一方只存 ID 而不建外键约束
  3. 聚合根设计:在 DDD 中,聚合内部可以相互引用,但跨聚合只能通过 ID 引用

六、过度设计(Over-Engineering)

6.1 什么是过度设计

过度设计是指系统架构的复杂度远超实际业务需求的复杂度。它常常披着"最佳实践"和"前瞻性设计"的外衣——"未来流量会暴增所以先上分库分表","将来可能换数据库所以先抽象一套通用 DAO 层","架构要灵活所以先上插件化",结果业务还没跑起来,技术债先堆了一大堆。

文章配图
文章配图

6.2 过度设计的典型表现

表现 案例
过早分库分表 日活 1000 的系统用了 16 个分片,每个分片几十 MB 数据
过度抽象 一个简单的 CRUD 系统,搞了六边形架构 + DDD + CQRS + Event Sourcing
不必要的技术异构 用户量不到 1 万,已经上了 K8s + Service Mesh + 全链路追踪 + 混沌工程
万能配置化 所有业务逻辑都做成可配置的规则引擎,结果配置项比代码还难维护
为"可能"而设计 需求文档里写了"将来可能支持多语言",于是整个系统从第一天就上了国际化框架
微服务过度拆分 3 个人的团队维护了 20 个微服务,花在运维上的时间比写业务代码还多

6.3 过度设计 vs 合理前瞻性设计

维度 过度设计 合理前瞻性设计
触发条件 "也许将来……" "根据增长曲线,6 个月后……"
成本 立即投入大量成本 只做低成本预留
验证 无法验证决策是否正确 有明确的验证时间点
可逆性 引入后难以回退 预留扩展点,不使用时无成本
实例 10 万用户就上微服务 单体中按业务模块分 package,拆分时只需改部署

6.4 过度设计为什么会发生

1. 简历驱动设计(Resume-Driven Development):开发者想用新技术,不是为了解决问题,而是为了丰富简历。K8s、Service Mesh、Flink 全都往上堆,不管业务需不需要。

2. 大厂崇拜:看到 Google/Facebook/Ali 的架构方案,不管自己业务规模是否匹配,直接照搬。"他们用微服务,所以我们也用"——但他们有 5000 个工程师,你有 5 个。

3. 对"简单"的恐惧:架构师觉得"太简单了"显得不够专业,非得画一个复杂的架构图才能证明自己的价值。

4. 缺乏 YAGNI 原则意识:YAGNI = You Ain't Gonna Need It。当你为一个"将来可能"的需求投入设计时,大部分情况下,那个将来不会来。

5. 搞错了问题优先级:把精力花在解决"技术优雅性"问题上,而不是"业务痛点"上。技术是为业务服务的,不是反过来。

文章配图
文章配图

6.5 如何避免过度设计

1. 遵循 YAGNI 原则。 不要在第一个版本就实现"可能将来需要"的功能。等到真正需要时再实现,因为那时候你对需求的理解会更准确。

2. 做最简单的可行方案。 问自己:"如果去掉这个组件,系统还能满足当前需求吗?" 如果能,就去掉。

3. 用数据驱动决策。 不是"我觉得流量会涨",而是"根据近 3 个月的增长率,6 个月后 QPS 将达到 X,当前架构的瓶颈在 Y"。

4. 区分"扩展点"和"过度设计"。 预留扩展点(如定义接口而非硬编码实现)是好的,实现扩展点(如实现 5 种存储适配器但只用 1 种)是坏的。

5. 接受"演化式架构"。 好的架构不是一次性设计出来的,而是随着业务需求逐步演化出来的。单体→模块化→拆分核心服务→微服务,这是一条自然演化路径,不需要一步到位。

一句话总结:过度设计把"未来的问题"变成了"现在的问题",而未来大概率不会按你预想的方式发展。


七、五大反模式的关联关系

文章配图
文章配图

这五个反模式不是孤立的,它们之间存在着因果关系和连锁反应:

code
大泥球
  ├── 缺乏模块边界 → 数据层耦合 → 共享数据库
  ├── 为了"解耦"强行拆分 → 拆分不当 → 分布式单体
  ├── 模块间随意引用 → 循环依赖
  └── 重构困难 → 每次改造都是"重写" → 过度设计

一个典型的大泥球系统的演化路径:

  1. 单体系统运行多年,代码混乱,成为大泥球
  2. 团队决定"拆成微服务"来解决问题,但只拆了代码没拆数据库,变成分布式单体
  3. 分布式单体中,多个服务访问同一张表,形成共享数据库
  4. 服务间互相调用,形成循环依赖
  5. 为了"一次性解决所有问题",团队引入大量基础设施,变成过度设计

这就是为什么治理反模式需要系统性的方法——头痛医头只会把问题从一个反模式转移到另一个反模式。


八、建立反模式免疫系统

与其亡羊补牢,不如从源头预防。以下是建立反模式免疫系统的几个关键实践:

8.1 架构决策记录(ADR)

每个重要的架构决策都要写 ADR(Architecture Decision Record),记录:背景、决策内容、备选方案、后果。这不仅是给人看的文档,更是一种强制思考的工具——当你必须把决策写下来时,很多"想当然"的设计就会暴露出问题。

8.2 架构适应度函数(Fitness Function)

ArchUnit 测试、包依赖规则、API 契约测试,这些都可以作为 CI 的一部分,自动检查架构是否偏离了预期。比如:

java
// ArchUnit 检查:禁止 controller 直接调用 dao
classes().that().resideInAPackage("..controller..")
    .should().onlyAccessClassesThat()
    .resideInAnyPackage("..service..", "..dto..")
    .check(classes);

文章配图
文章配图

8.3 技术债务看板

把技术债务可视化,让 PM 和业务方能看到。每个迭代分配 20% 的时间偿还技术债务,这不是"浪费时间",而是防止系统走向大泥球的保险。

8.4 定期架构评审

不要等到系统出了大问题才评审架构。每个季度做一次架构健康检查,用这五个反模式作为检查清单:有没有大泥球的味道?服务间耦合度如何?有没有共享数据库?循环依赖是否在增加?有没有过度设计?

文章配图
文章配图

8.5 团队对齐

最重要的是:团队对"什么是好的架构"有共识。如果团队里一半人认为"能用就行",另一半人追求"架构优雅",那就永远无法达成一致。建立团队架构原则(Architecture Principles),比如:

  • 优先简单,非必要不加复杂度
  • 数据跟随服务,不共享数据表
  • 异步优先于同步
  • 一个迭代 20% 时间还技术债

九、总结

反模式 核心问题 一句话
大泥球 缺乏结构,长期腐烂 不是设计出来的,是"长"出来的
分布式单体 拆了部署,没拆边界 比单体多了一堆复杂度,却没有任何收益
共享数据库 数据层耦合 最隐蔽的耦合,解决起来也最痛苦
循环依赖 模块间形成环 有循环依赖的模块本应是一个模块
过度设计 复杂度远超需求 为"或许"的实现投入了"确定"的成本

架构设计最大的智慧不是"如何设计一个完美的系统",而是"如何让系统能够持续演化"。John Gall 在《系统学》中说过:"一个运转良好的复杂系统,总是从一个运转良好的简单系统演化而来的。" 反过来,一个从零开始设计的复杂系统,从来没运转良好过,也不可能被修补到运转良好——你必须从一个运转良好的简单系统重新开始。

文章配图
文章配图


十、面试官视角提问

以下问题从面试官视角出发,考察对架构反模式的深度理解和实战经验。建议在阅读完正文后独立作答,再对照参考思路。

Q1:你接手了一个"大泥球"系统,老板要求你 3 个月内把它改造成"可维护的微服务架构",你会怎么做?

参考思路

这是一个陷阱题——3 个月内把大泥球变成微服务架构基本不可能。正确的回答不是"怎么做",而是"为什么不能做,以及应该做什么"。

  1. 先评估现状:模块边界、数据库耦合度、团队能力、业务压力
  2. 向老板说明:Big Bang 重写的成功率不到 20%(参考 Standish Group 的数据),更好的做法是渐进式改造
  3. 提出务实方案:用绞杀者模式,先识别出 1-2 个高价值低风险的模块进行拆分,3 个月完成第一个模块的独立部署
  4. 同时建立架构治理机制,防止泥球继续恶化

面试官想听的是:你有没有被"老板的 deadline"带着走,还是能理性分析并 push back。

Q2:你如何判断一个系统是"分布式单体"还是"真正的微服务"?请给出具体的判断标准。

参考思路

核心判断标准不是服务数量,而是独立部署能力:

  1. 独立部署测试:改一个服务,能不能独立发布而不要求其他服务同时发布?
  2. 独立数据存储:每个服务是否有自己的数据库?有没有跨服务直接查表?
  3. 独立扩展:某个服务流量激增时,能不能单独扩容而不影响其他服务?
  4. 故障隔离:一个服务挂了,其他服务是否不受影响(降级而非崩溃)?
  5. 团队自治:一个团队能不能独立完成需求开发、测试、部署全流程,而不用协调其他团队?

如果以上 5 个问题有 3 个以上的答案是"不能",那就是分布式单体。

Q3:你们团队正在做微服务拆分,一个同事说"订单和库存本来就是强关联的,共用一张 inventory 表最方便,拆开反而麻烦"。你怎么回应?

参考思路

这个同事说得有道理——共享数据库确实"方便",但方便是短期的,痛苦是长期的:

  1. 短期:共享表写代码快,JOIN 查询方便
  2. 长期:表结构变更需要协调所有使用方、无法独立扩展、无法独立选择存储、测试互相污染

关键不是"共享数据库对不对",而是"团队有没有做好承担长期成本的准备"。如果系统规模小、团队人少、业务稳定,共享数据库未必是问题。但如果系统在快速增长,现在不拆,以后拆的成本会翻倍。

更务实的回应:不用一步到位拆,先从"只读"和"弱依赖"开始——比如库存服务提供 API 给订单服务查询库存,而不是订单服务直接查库存表。如果性能有问题,再考虑 CQRS 做数据冗余。

Q4:你如何区分"合理的前瞻性设计"和"过度设计"?请用一个你实际经历过的案例来说明。

参考思路

核心区分标准:

合理的前瞻性设计:成本低、可逆、有数据支撑。比如定义接口而非硬编码实现——现在只有一个实现,未来扩展时代码改动小,当前几乎零成本。

过度设计:成本高、不可逆、基于假设。比如系统日活 1000 就上了分库分表——部署复杂、运维成本高、出了问题难排查,而"未来流量暴增"只是一个假设。

案例示例:"我们团队曾经给一个内部管理系统设计了完整的 RBAC + ABAC 权限模型,支持角色、部门、数据行级权限。结果上线 2 年,用户只有 50 个,权限需求从来没超过'管理员'和'普通用户'。那个复杂的权限系统成了没人敢动、也没人受益的代码遗产。"

面试官想听的是:你有没有反思过自己曾经的过度设计,而不只是一味说"我从来不犯这个错"。

Q5:循环依赖在 Spring 中不是被三级缓存解决了吗?为什么还要把它当作反模式?

参考思路

Spring 的三级缓存解决的只是**"构造器循环依赖"在启动时不会报错**的问题,但循环依赖的真正危害远超启动报错:

  1. 模块内聚性问题:A 和 B 互相依赖,说明它们本应是一个模块,或者应该在中间引入一个抽象层。强行分开只会让代码理解成本变高。
  2. 测试困难:测试 A 时不得不 mock B,测试 B 时不得不 mock A,测试写得比业务代码还复杂。
  3. 重构阻力:改 A 的一个方法签名,B 的调用方也要改,如果 B 又被 C 依赖……这就是大泥球的开始。
  4. Spring 的解决方案是"技术手段掩盖设计问题"——它让循环依赖不报错,但没消除循环依赖带来的设计缺陷。就像给伤口打止痛药,不治本。

面试官想听的是:你理解问题的本质(设计缺陷),而不只是表象(启动报错)。

Q6:假设你是一个 5 人创业团队的 Tech Lead,你需要为一个新项目做技术选型。你会选择单体架构还是微服务?为什么?

参考思路

5 人团队,新项目,处于"探索业务模式"阶段——毫无疑问选择单体架构。

理由:

  1. 业务模式还没验证,需求变化快,单体改起来快
  2. 5 个人维护微服务,运维成本太高,还不如把时间花在写业务代码上
  3. 微服务解决的是"人多"的问题(组织伸缩),而不是"业务复杂"的问题

但"选单体"不等于"写大泥球":

  • 在单体内部按业务模块分 package,保持清晰的模块边界
  • 定义好模块间的接口,不随意跨模块调用
  • 数据库表按业务域分组,预留拆分的可能性
  • 等团队到了 20-30 人、业务模式验证通过后,再按模块边界逐步拆出微服务

这才是"演化式架构"的正确姿势。


本文是"Java 技术专家与架构师面试:250 个高频考点"系列的第 204 篇。下一篇预告:205|系统设计题型分类。