
从“t3code”这个工程代号讲起一套让代码不再“放哪都行”的三级体系第一次看到 t3code 这个工程代号时我把它读成了 Tier 3 Code心里还嘀咕这名字起得也太含糊了第三层代码哪个第三层后来帮团队梳理那几个核心模块的依赖关系我把几百个类的引用链顺着画了一遍才明白它真正想表达的不是某种“三层架构”而是一套关于代码归属的坐标系——用三个层级给每一段代码一个可预期的位置让你的团队不用读完整条业务链路也能判断一段代码的稳定性预期、变更频率以及它到底能依赖什么。这篇文章我想把 t3code 的三级含义、职责边界、落地动作和配套工程手段完整讲一遍。它不是某个框架的官方文档而是我这段时间在真实代码库里推行、踩坑、调整之后的总结。如果你团队里也经常出现“这个工具类到底该放哪”“为什么 Service 越来越厚”“新人改代码不知道该动哪个文件”这类争论这篇文章应该能给你一个可执行的参考方向。1. 从命名到设计t3code 到底在解决什么问题1.1 一个名字背后的编码哲学代码库有没有“位置感”决定了一个团队的协作效率。t3code 的核心思路其实很朴素不再靠人肉记忆去维护“这段代码属于哪一层”而是把代码归属变成一组简单的三档判断。传统分层架构最典型的问题不在层本身而在于分层只表达了技术栈没有表达稳定性。Controller 必然薄Service 必然厚厚到最后大家开始往 Service 里塞缓存、塞外部 SDK 调用、塞定时任务一个方法几百行谁都动不了。为什么因为“Service”这个词太笼统了它既装得下业务规则也装得下技术细节甚至装得下与业务无关的工具逻辑。当一层什么都能装的时候它最终一定会变成垃圾场。t3code 换了个切分维度不从技术角色出发而从稳定度和复用面出发。它把代码分成三档第一档领域骨架负责承载业务规则和流程编排跟着需求走变频繁第二档能力沉淀负责提供可跨模块复用的通用能力低频变更接口稳定第三档边界适配负责跟外部世界打交道把框架、数据库、第三方 SDK 变成内部可理解的形态。这三档不是三个物理层更像是一套代码的“坐标系统”。你用三个值就能定位任意一段代码的合理位置它属于谁它的稳定性预期是什么它能依赖谁1.2 三级不是分层架构而是一套“坐标系统”我给团队讲这个思路时用过一个大楼的类比。传统分层是同一层楼里的房间门牌这是 Controller 区那是 Service 区另一片是 Dao 区。而 t3code 更像是楼里的竖向分区一层是承重墙领域骨架怎么变都不能轻举妄动的地方动了就是改业务规则二层是标准间能力沉淀谁来了都能住公共设施接口稳定三层是临街铺面边界适配招牌天天换对应外部接口一变就得跟着改。同样一段代码该放在哪一级不取决于它在哪个技术组件里而取决于一个问题如果业务逻辑变了它要不要跟着变领域规则跟着需求变通用能力原则上不能轻易变边界代码跟着外部接口版本变。这三者的变更频率天然不同混在一起就是灾难。1.3 什么规模的代码库最适合引入 t3code我个人的实践结论是不是所有项目都需要 t3code规模才是引入门槛。当以下信号出现时值得认真考虑这套体系团队 5 人以上并且有多个业务模块共享同一个技术底座代码库里已经出现同一个工具类在多处复制、同一段逻辑两处实现且行为不一致新人进入项目后无法靠目录结构判断“这段代码应该在哪里改”每一次小需求变更都会牵扯到看似无关的底层代码。反过来如果是单机小服务、团队两三个人、代码总量不大那不需要强行套 t3code一个规范的目录约定就够用了。过度设计同样会造成负担。2. 三级结构的职责划分每层代码应该长什么样先放一张我在内部文档里用过的对照表它把三级代码的核心特征说得很明白层级名称典型内容变更频率允许依赖一级领域骨架领域对象、业务流程、规则引擎、领域事件随需求频繁变更语言生态、同级骨架二级能力沉淀缓存封装、配置读取、消息发送、通用计算低频变更一级骨架只读、同级能力三级边界适配RestController、Repository、外部 SDK 封装、消息监听随外部版本变化一、二、三级均可依赖这只是一张参考表实际落地时可以调整。但有一个方向不能变三级必须是一级和二级的支撑者不能反过来让领域逻辑去迁就外部协议。2.1 第一级承载业务的“骨架代码”第一级代码最典型的特征读代码时能看出业务意图而不是看出技术细节。比如“订单超时未支付则自动取消”这条规则它的表述应该接近业务语言而不是接近数据库字段。// 一级领域骨架 public class OrderTimeoutPolicy { private final OrderRepository orderRepository; // 注意这里是接口不是 JdbcTemplate private final Clock clock; public void cancelExpiredOrders() { ListOrder expiredOrders orderRepository.findExpiredBefore(clock.instant()); for (Order order : expiredOrders) { order.cancel(TIMEOUT); } } }我见过太多反例一段明明是纯业务规则的逻辑里面直接嵌了 MyBatis 注解、Redis key 拼接、甚至第三方短信平台的模板 ID。一旦外部细节变化业务规则就跟着改这等于把承重墙和临街铺面糊在了一起。一级代码的硬性要求是不掺杂框架注解不直接依赖外部 SDK。它只依赖语言生态、自身同级代码以及少量必要的二级能力接口。如果一段一级代码里出现Autowired FeignClient这就是一个明确的信号——你把边界逻辑渗入骨架了。2.2 第二级稳定沉淀的“能力代码”第二级代码不关心具体的业务形态但能提供可复用的通用能力。典型的例子包括缓存访问封装CacheService屏蔽 Redis 客户端细节分布式 ID 生成器幂等控制组件通用的限流器、重试器日志埋点与追踪上下文。二级代码最核心的评价标准是接口稳定。它今天长什么样半年后基本还是什么样所以调用方可以放心依赖它。// 二级能力沉淀 public interface CacheService { T T get(String key, ClassT type); void set(String key, Object value, Duration ttl); void evict(String key); }这里有一个非常容易踩的坑二级代码不能反向依赖一级的领域对象。比如CacheService里如果出现OrderCacheKeyBuilder那说明二级能力已经和某个具体业务耦合了它的复用面就塌了。二级永远不应该 import 一级的领域模型它只面对“通用输入”。2.3 第三级与外部世界打交道的“边界代码”第三级是所有注解最重、样板代码最多的部分。它的职责不是实现业务而是翻译把 HTTP 请求翻译成领域调用把数据库行翻译成领域对象把第三方回调翻译成领域事件。// 三级边界适配 RestController public class OrderController { private final OrderApplicationService orderService; PostMapping(/orders) public CreateOrderResponse create(RequestBody CreateOrderRequest request) { CreateOrderCommand command request.toCommand(); // 翻译点 String orderId orderService.create(command); return CreateOrderResponse.of(orderId); } }边界代码允许依赖框架、允许依赖 SDK、允许依赖一二级代码。它存在的意义就是“挡在外面”不让外部协议的细节污染内部。这里最常犯的错误是直接把外部 DTO 当成领域模型传遍全项目——用户体系那边定义了个UserDTO业务核心代码里到处都是这个类后来第三方接口一升级全项目跟着改。正确做法是在边界层完成转换进入一级、二级的都是内部对象。3. 落地 t3code 的关键动作目录、命名、依赖规则3.1 目录结构怎么落理解了三级职责之后第一件事就是让目录结构本身成为坐标。下面是一个偏 Java 风格的示例但思想可以平移到任何语言com.company.project ├── core # 一级领域骨架 │ ├── domain # 领域对象、聚合根、值对象 │ ├── service # 业务流程编排、用例实现 │ └── event # 领域事件定义 ├── ability # 二级能力沉淀 │ ├── cache │ ├── idgen │ ├── notify │ └── counter └── boundary # 三级边界适配 ├── web # Controller、Request/Response、参数校验 ├── persistence # Repository 实现、ORM 映射、数据表操作 ├── integration # 外部 SDK 封装、RPC 客户端、消息监听 └── config # 框架配置、装配代码需要强调一点目录名本身不重要重要的是团队看到目录就能回答“这段代码属于哪一级”。如果贵团队习惯叫adapter、application、infrastructure完全可以保留自己的术语只要三档划分明确即可。3.2 命名规则的三个硬约束命名是 t3code 最容易见效、也最容易被忽视的抓手。我总结了三个硬约束第一目录必须体现层级。新代码的第一落点永远由目录决定而不是由“我心情决定”。如果一段代码不在core、ability、boundary任何一个目录里那它默认就是不合规的。第二类名必须表达职责与归属。一级类名偏向业务名词如OrderTimeoutPolicy、PaymentSettler二级类名偏向“能力技术词”如CacheService、IdGenerator三级类名偏向“技术角色资源词”如OrderJdbcRepository、WechatPayClient。第三警惕一切意义不明的后缀。我尤其提醒团队Util、Helper、Common是 t3code 里最需要警惕的命名。凡是叫StringUtils、DateHelper、CommonService的类都要额外回答一个问题——“你属于哪一级”如果不确定那就先不要创建它。这类模糊命名的类往往是跨层依赖的万恶之源。3.3 依赖方向的守护机制光有约定不够代码是会烂的是需要守护的。我用的守护机制分为三层从轻到重第一层是代码评审。评审者先看“这段代码放对位置了没”再看实现细节。第二层是架构测试。用 ArchUnit 或者自研脚本把“三级不能依赖一级”“二级不能依赖三级实现类”这类核心规则固化成自动化检查。第三层是目录移动与依赖扫描。每次大规模调整目录时先跑一遍依赖扫描把不合规的引用链路全部列出来逐个确认归属再动手。这里有一个我踩过的坑一开始我把架构测试规则铺得很细导致误报非常多团队渐渐开始忽略检查结果。后来砍到只剩两条高频规则大家都愿意遵守了。守护机制宁可少而准不要多而烦。4. 让 t3code 真正生效的工程配套模板、评审、自动化4.1 脚手架模板让新代码自动归位很多团队推规范失败不是因为规范不对而是因为“从零开始写一个新模块时不知道按什么框架去写”。我的做法是维护一份带三级结构的脚手架模板工程任何新服务 clone 下来就能用。模板工程里三级的示例代码都要有而且每个示例都经过精心设计core下放一个不带任何框架注解的领域服务示例ability下放一个跟业务无关的通用能力示例boundary下放一个标准的 Controller 和 Repository 实现示例。有一个小技巧我故意在模板的示例代码里留了一个“错误示范”——在某个边界代码里混入一条领域规则让新人一眼能看出这里不对劲。很多新同学在熟悉项目时看到这个错误会跑来问“这里是不是写错了”。这时候我只需要反问一句“那它应该属于哪一级”对方立刻就懂了。这个效果比背十遍规范文档都强。4.2 代码评审维度先看层级再看实现t3code 推行起来以后我把团队代码评审的关注顺序改成了这段代码放在哪一级目录下合理吗它依赖的东西是允许的吗有没有跨层如果核心业务规则变了这段代码要不要跟着变实现本身有没有偷懒有没有重复造轮子前三个问题都是层级问题最后一个才是实现问题。这个顺序很重要因为实现问题再漂亮如果层级放错了后续的维护成本一定会累积。举个真实例子某个 PR 里同事在 Controller 里写了十几行“先查库存、再扣减、再记录日志”的业务流程。从实现层面看代码不算错但按照 t3code 的评审维度问题非常明显流程编排属于一级领域骨架的职责不应该出现在三级边界层里。评审时我直接指出“这段逻辑应该下沉到 core 的 ApplicationService 中Controller 只保留参数解析和响应转换”。同事很快就改完了代码反而比原来更短。4.3 依赖检查的自动化方案自动化的目标不是替代评审而是把评审者容易漏掉的部分兜住。我推荐至少实现一条简单的 import 检查规则边界代码之外的任何层级不得直接依赖具体的外部 SDK 客户端类。以 Java 的 ArchUnit 为例一条类似这样的规则就很有价值AnalyzeClasses(packages com.company.project.core) public class ArchitectureRuleTest { ArchTest static final ArchRule CORE_SHOULD_NOT_DEPEND_ON_BOUNDARY noClasses() .that() .resideInAPackage(..core..) .should() .dependOnClassesThat() .resideInAPackage(..boundary..); ArchTest static final ArchRule ABILITY_SHOULD_NOT_DEPEND_ON_BOUNDARY noClasses() .that() .resideInAPackage(..ability..) .should() .dependOnClassesThat() .resideInAPackage(..boundary..); }如果你不想引入额外测试框架也可以写一个简单的脚本扫描所有 import 语句凡是core包出现boundary包或者具体第三方客户端包名就把这条记录打进 CI 失败队列。脚本方式更通用但表达式能力弱适合规则较少的团队。还是那句话自动化规则要克制。先把最不能容忍的两三条跨层依赖守住比一次铺五十条规则有效得多。5. 运营 t3code 之后的一些体会边界模糊与灰度控制5.1 边界模糊的三种场景理论讲起来总是清晰落到真实代码里几乎每个团队都会遇到“不知道该归哪一级”的尴尬场面。我挑了三种最高频的场景说说我的判断标准。场景一工具类到底放二级还是三级我的答案很直接看它是否符合业务语义。StringUtils、DateUtils这一类只依赖语言生态、不包含任何业务含义放二级没问题但像OrderStateMachine、SettlementCalculator这类工具方法名里全是业务词汇放一级。命名本身就是试金石凡是工具类的名字里出现了业务名词它就应该被升到一级去。场景二Controller 过厚塞满了流程判断。Controller 里出现“先查、再判断、再改、再查”这类流程式逻辑说明边界代码越权了。正确的做法是把这段流程整体下沉到一级的 ApplicationServiceController 只保留“请求转命令、命令转响应”的翻译工作。这个判断非常简单但也非常容易被人忽略——因为很多人的习惯是“Controller 里顺手把活干了”。场景三外部 SDK 的二次封装该放哪外部 SDK 封装本身是典型的三级边界代码。但如果封装完成之后提供的方法已经完全抽离了外部 SDK 的形态变得足够通用而且被多个业务模块复用那么它就有资格升到二级。判断时机很简单当第二个不同的业务模块开始用同一个封装时就该考虑把它从边界层升级到能力层。一开始就过早升级反而容易设计出抽象无力的接口。5.2 层级跨越的灰度管控别指望一次性把存量代码全部重构完。就算你心里清楚哪段代码放错了位置一次性大规模搬目录也一定会引发合并地狱、测试全红、团队心态爆炸。我的灰度做法是这样先圈定一个核心链路模块做试点把它的代码完整切到 t3code 结构试运行两个迭代观察是否存在频繁跨层改动的痛点利用大版本窗口按目录逐步移动存量代码每次移动后立刻跑依赖检查和测试维护一份“例外清单”凡是暂时无法归位的代码白纸黑字记录原因和计划迁移时间。灰度不是为了放纵乱象而是给“正确的做法”一点时间让团队在真实验证中建立信任。很多规范死在突然袭击式的重构上而不是死在规范本身。5.3 不同团队规模下的适用度最后说一个我在不同团队规模下的观察5 人以下的小团队只需要目录约定 命名规范不需要上架构测试。这时候沟通成本低靠评审完全可以兜住。5 到 20 人的团队必须引入自动化架构测试评审标准里明确“先看层级再看实现”。因为人一多跨模块的误解就会变多。20 人以上或者多团队协作t3code 的单一三级维度会不够用。这时候要在三级的基础上再叠加领域模块划分比如按订单域、支付域、会员域拆分每个域下再各自落实三级结构。否则一个庞大的core包同样会变成新的垃圾场。换句话说t3code 是一套适合作为团队公共语言的代码坐标系但它不是银弹。规模越大越需要和其他组织结构工具配合使用。写在最后我个人用下来最大的感受是t3code 的功夫根本不在“代码怎么写”而在“团队怎么达成共识”。最先要做的不是把全部代码重构一遍而是把最容易引发分歧的三类东西——通用工具、外部接口封装、领域对象——先定义清楚。这几类一旦明确至少一半的“代码放哪”之战会消失。如果你也准备在团队里试 t3code我建议从下一个新模块开始而不是从老代码开始。让新代码先长成体系的形状老代码在自然迭代中慢慢归位。这比轰轰烈烈地重构稳得多也更容易让团队真正接受它。