vertical-codebase
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVertical Codebase Architecture
垂直代码库架构
Core Mental Model
核心思维模型
Code that changes together lives together.
The fundamental question when placing any file is:
"What does this code DO, not what TYPE of thing is it?"
Two structures to recognize:
变化相关的代码应该放在一起。
放置任意文件时的核心问题是:
「这段代码是做什么的,而不是它是什么类型的代码?」
需要识别两种结构:
❌ Horizontal (organize by technical type)
❌ 水平式(按技术类型组织)
src/
components/ ← all UI components mixed together
hooks/ ← all hooks mixed together
utils/ ← everything else
types/ ← all types
services/ ← all API callsProblem: to understand or change one feature, you jump across 4+ folders.
src/
components/ ← 所有UI组件混合存放
hooks/ ← 所有hooks混合存放
utils/ ← 其他所有工具类代码
types/ ← 所有类型定义
services/ ← 所有API调用问题:要理解或修改一个功能,需要在4个以上的文件夹之间来回跳转。
✅ Vertical (organize by domain/functionality)
✅ 垂直式(按领域/功能组织)
src/
dashboard/ ← everything related to the dashboard
billing/ ← everything billing-related
auth/ ← everything auth-related
design-system/ ← truly generic, reusable across projects
shared/ ← shared business logic (not generic enough for design-system)Benefit: one feature = one folder. Easy to find, easy to own, easy to delete.
src/
dashboard/ ← 所有与仪表盘相关的代码
billing/ ← 所有与账单相关的代码
auth/ ← 所有与认证相关的代码
design-system/ ← 真正通用、可跨项目复用的代码
shared/ ← 共享业务逻辑(通用性不足以归入design-system)优势:一个功能对应一个文件夹。易于查找、易于维护、易于删除。
The Placement Decision Tree
文件存放决策树
When deciding where a file goes, work through these questions in order:
1. Is it tied to a specific feature or domain?
YES → put it in that vertical (e.g. src/billing/)
│
└── Is it used by multiple features?
YES → does it contain business logic specific to this app?
YES → make it its own vertical or put in /shared
NO → put it in /design-system (or /lib)
NO → keep it private inside the feature vertical
2. Is it purely generic with zero business logic?
(could live in any project, any company)
YES → /design-system or /lib
3. Is it infrastructure / cross-cutting concern?
(logging, config, error handling, i18n, routing)
YES → /infrastructure or /core确定文件存放位置时,按以下顺序回答问题:
1. 它是否与特定功能或领域绑定?
是 → 放入对应的垂直文件夹(如src/billing/)
│
└── 它是否被多个功能使用?
是 → 它是否包含该应用特有的业务逻辑?
是 → 为其创建独立的垂直文件夹,或放入/shared
否 → 放入/design-system(或/lib)
否 → 保留在所属功能的垂直文件夹内,设为私有
2. 它是否是完全通用、无业务逻辑的代码?
(可在任意项目、任意公司中复用)
是 → /design-system 或 /lib
3. 它是否是基础设施/横切关注点?
(日志、配置、错误处理、i18n、路由)
是 → /infrastructure 或 /coreVertical Structure (inside a vertical)
垂直文件夹内部结构
Inside each vertical, organize however makes sense — flat is often fine:
src/billing/
billing-service.ts
billing-types.ts
billing-utils.ts
invoice-list.tsx (if frontend)
use-invoice.ts (if frontend)
invoice.test.tsFlat > nested inside a vertical. Only add sub-folders when the vertical grows large enough to justify it (50+ files).
No barrel/index files. Boundaries are enforced by the linter, not by what you choose to export.
每个垂直文件夹内部可按任意合理方式组织——通常扁平化结构即可:
src/billing/
billing-service.ts
billing-types.ts
billing-utils.ts
invoice-list.tsx (如果是前端项目)
use-invoice.ts (如果是前端项目)
invoice.test.ts垂直文件夹内部扁平化优于嵌套。仅当垂直文件夹内文件足够多(50个以上)时,才需要添加子文件夹。
不要使用barrel/index文件。通过linter规则管控边界,而非通过导出选择来限制。
Enforcing Boundaries with the Linter
用Linter管控边界
Declaring verticals is not enough — enforce the dependency direction with lint rules.
The generic rule is:
A feature vertical can only import from,shared, andinfrastructure. Never from another feature vertical.design-system
feature vertical → shared ✅
feature vertical → infrastructure ✅
feature vertical → design-system ✅
feature vertical → feature vertical ❌ lint error
shared → feature vertical ❌ lint error
design-system → anything ❌ lint errorIf two verticals need to communicate, the answer is to extract the shared code into , not to create an exception in the linter.
/shared仅声明垂直文件夹是不够的——通过Lint规则强制依赖方向。
通用规则是:
功能垂直文件夹仅能从、shared和infrastructure导入代码。禁止从其他功能垂直文件夹导入。design-system
功能垂直文件夹 → shared ✅
功能垂直文件夹 → infrastructure ✅
功能垂直文件夹 → design-system ✅
功能垂直文件夹 → 功能垂直文件夹 ❌ Lint错误
shared → 功能垂直文件夹 ❌ Lint错误
design-system → 任意文件夹 ❌ Lint错误如果两个垂直文件夹需要交互,解决方案是将共享代码提取到/shared,而非在Linter中添加例外规则。
TypeScript / JS — eslint-plugin-boundaries
eslint-plugin-boundariesTypeScript / JS — eslint-plugin-boundaries
eslint-plugin-boundariesjs
// eslint.config.js
import boundaries from "eslint-plugin-boundaries";
export default [
{
plugins: { boundaries },
settings: {
"boundaries/elements": [
{
type: "feature",
pattern: "src/!(shared|infrastructure|design-system)/*",
},
{ type: "shared", pattern: "src/shared/*" },
{ type: "infrastructure", pattern: "src/infrastructure/*" },
{ type: "design-system", pattern: "src/design-system/*" },
],
},
rules: {
"boundaries/element-types": [
"error",
{
default: "disallow",
rules: [
{
from: "feature",
allow: ["shared", "infrastructure", "design-system"],
},
{ from: "shared", allow: ["infrastructure", "design-system"] },
{ from: "infrastructure", allow: ["design-system"] },
{ from: "design-system", allow: [] },
],
},
],
},
},
];js
// eslint.config.js
import boundaries from "eslint-plugin-boundaries";
export default [
{
plugins: { boundaries },
settings: {
"boundaries/elements": [
{
type: "feature",
pattern: "src/!(shared|infrastructure|design-system)/*",
},
{ type: "shared", pattern: "src/shared/*" },
{ type: "infrastructure", pattern: "src/infrastructure/*" },
{ type: "design-system", pattern: "src/design-system/*" },
],
},
rules: {
"boundaries/element-types": [
"error",
{
default: "disallow",
rules: [
{
from: "feature",
allow: ["shared", "infrastructure", "design-system"],
},
{ from: "shared", allow: ["infrastructure", "design-system"] },
{ from: "infrastructure", allow: ["design-system"] },
{ from: "design-system", allow: [] },
],
},
],
},
},
];Other languages
其他语言
| Language | Tool |
|---|---|
| Python | |
| Go | |
| Rust | Cargo workspaces + |
| Java/Kotlin | Package-private + ArchUnit |
| Monorepo (any) | |
| 语言 | 工具 |
|---|---|
| Python | |
| Go | |
| Rust | Cargo workspaces + |
| Java/Kotlin | Package-private + ArchUnit |
| Monorepo (any) | |
Shared vs Design-System vs Feature
Shared vs Design-System vs Feature
This is the most common point of confusion. Use this table:
| Code | Where it goes | Why |
|---|---|---|
| Generic button, input, modal | | No business logic, reusable anywhere |
| | Pure utility, zero business logic |
| | Business logic, even if used everywhere |
| | Shared but product-specific |
| | Depends: generic math → shared, billing-specific → billing |
| Error boundary wrapper | | Cross-cutting concern |
The test for design-system: "Could this exist unchanged in a completely different product?"
- Yes → design-system / lib
- No → it's a vertical or shared business logic
这是最容易混淆的部分,可参考下表:
| 代码类型 | 存放位置 | 原因 |
|---|---|---|
| 通用按钮、输入框、弹窗 | | 无业务逻辑,可在任意项目复用 |
| | 纯工具类,无任何业务逻辑 |
| | 包含业务逻辑,即使被多处使用 |
| | 被共享但属于产品特有的功能 |
| | 取决于:通用数学逻辑 → shared,账单特有的逻辑 → billing |
| 错误边界包装器 | | 属于横切关注点 |
Design-System的判断标准: 「这段代码无需修改即可在完全不同的产品中使用?」
- 是 → design-system / lib
- 否 → 归入垂直文件夹或共享业务逻辑
Language-Specific Examples
各语言示例
TypeScript / Node.js
TypeScript / Node.js
src/
auth/
auth-service.ts
auth-middleware.ts
auth-types.ts
auth.test.ts
users/
user-repository.ts
user-service.ts
user-types.ts
billing/
stripe-client.ts
billing-service.ts
billing-types.ts
shared/
pagination.ts
date-utils.ts
infrastructure/
logger.ts
config.ts
database.tssrc/
auth/
auth-service.ts
auth-middleware.ts
auth-types.ts
auth.test.ts
users/
user-repository.ts
user-service.ts
user-types.ts
billing/
stripe-client.ts
billing-service.ts
billing-types.ts
shared/
pagination.ts
date-utils.ts
infrastructure/
logger.ts
config.ts
database.tsPython
Python
src/
auth/
__init__.py
service.py
middleware.py
models.py
test_auth.py
billing/
__init__.py
stripe.py
service.py
models.py
shared/
__init__.py
pagination.py
date_utils.py
infrastructure/
logger.py
config.py
database.pysrc/
auth/
__init__.py
service.py
middleware.py
models.py
test_auth.py
billing/
__init__.py
stripe.py
service.py
models.py
shared/
__init__.py
pagination.py
date_utils.py
infrastructure/
logger.py
config.py
database.pyGo
Go
internal/
auth/
handler.go
service.go
repository.go
types.go
billing/
handler.go
service.go
stripe.go
shared/
pagination.go
timeutil.go
pkg/ ← truly public/reusable packages (like design-system)
middleware/
validator/internal/
auth/
handler.go
service.go
repository.go
types.go
billing/
handler.go
service.go
stripe.go
shared/
pagination.go
timeutil.go
pkg/ ← 真正公开/可复用的包(类似design-system)
middleware/
validator/Frontend (React, Vue, Svelte...)
前端(React, Vue, Svelte...)
src/
dashboard/
DashboardPage.tsx
dashboard-store.ts
use-dashboard-data.ts
dashboard-types.ts
widgets/
WidgetCard.tsx
use-widget.ts
widget-types.ts
widget-api.ts
design-system/
Button.tsx
Modal.tsx
useMediaQuery.ts
tokens.css
shared/
use-current-user.ts
format-currency.ts
infrastructure/
router.ts
i18n.ts
error-boundary.tsxsrc/
dashboard/
DashboardPage.tsx
dashboard-store.ts
use-dashboard-data.ts
dashboard-types.ts
widgets/
WidgetCard.tsx
use-widget.ts
widget-types.ts
widget-api.ts
design-system/
Button.tsx
Modal.tsx
useMediaQuery.ts
tokens.css
shared/
use-current-user.ts
format-currency.ts
infrastructure/
router.ts
i18n.ts
error-boundary.tsxCommon Mistakes to Avoid
常见误区
1. Placing by type reflex
"It's a hook/service/util, so it goes in /hooks /services /utils"
→ Always ask WHAT it does first, not WHAT it is.
2. Over-sharing
"I might need this elsewhere, so I'll put it in /shared"
→ Keep things in their vertical until you actually need to share them. YAGNI.
3. Under-splitting
"It's used in billing AND dashboard, so it goes in /shared"
→ If the shared code is substantial, give it its own vertical (, ...).
/invoices/reports4. Confusing "used everywhere" with "generic"
"is used in 10 places, so it's design-system material"useCurrentUser
→ Popularity ≠ genericity. It has business logic → vertical or shared.
5. Nesting too deep
src/features/billing/components/invoice/list/items/InvoiceItem.tsx
→ Flat is better. Add one level of nesting only when a vertical truly warrants it.
1. 习惯性按类型存放
「这是一个hook/service/util,所以应该放在/hooks /services /utils里」
→ 永远先问「它是做什么的」,而不是「它是什么类型」。
2. 过度共享
「我以后可能在其他地方用到它,所以放在/shared里」
→ 在实际需要共享之前,保持代码在所属的垂直文件夹内。遵循YAGNI原则。
3. 拆分不足
「它在billing和dashboard中都被用到,所以放在/shared里」
→ 如果共享代码足够多,应为其创建独立的垂直文件夹(如/invoices、/reports等)。
4. 将「被广泛使用」等同于「通用」
「在10个地方被使用,所以应该归入design-system」useCurrentUser
→ 使用频率 ≠ 通用性。它包含业务逻辑 → 应归入垂直文件夹或shared。
5. 嵌套过深
src/features/billing/components/invoice/list/items/InvoiceItem.tsx
→ 扁平化结构更好。仅当垂直文件夹确实需要时,才添加一层嵌套。
Refactoring a Horizontal Codebase
重构水平式代码库
When the user has an existing horizontal structure to migrate:
- Audit first — list all files and identify natural clusters (what "talks to" what)
- Start with leaf domains — pick a self-contained feature with no dependents
- Move, don't copy — create the vertical, move files, fix imports
- Add a lint rule — enforce the boundary before moving on
- Repeat domain by domain — never do a big-bang rewrite
💡 Readfor a step-by-step migration plan.references/refactoring-guide.md
当用户需要迁移现有的水平式结构时:
- 先审计 — 列出所有文件,识别自然的代码集群(哪些代码互相关联)
- 从独立领域开始 — 选择一个无依赖的自包含功能
- 移动而非复制 — 创建垂直文件夹,移动文件,修复导入路径
- 添加Lint规则 — 在继续迁移前先强制边界规则
- 逐个领域重复 — 永远不要进行大规模重写
💡 查看获取分步迁移计划。references/refactoring-guide.md
Quick Reference Card
快速参考卡片
| Question | Answer |
|---|---|
| Where does X go? | In the vertical that OWNS the functionality |
| What's a vertical? | A folder grouping code by what it does, not what it is |
| What goes in design-system? | Zero business logic, could exist in any project |
| What goes in shared? | Business logic used by multiple verticals |
| What goes in infrastructure? | Cross-cutting concerns (logging, config, DB) |
| How do verticals talk to each other? | Direct imports, but direction enforced by linter |
| Two verticals need to share code? | Extract to /shared — never create a linter exception |
| When to create a new vertical? | When a concept is big enough to own, or when shared code warrants it |
| 问题 | 答案 |
|---|---|
| X应该放在哪里? | 归入拥有该功能所有权的垂直文件夹 |
| 什么是垂直文件夹? | 按代码功能而非类型分组的文件夹 |
| 什么代码归入design-system? | 无业务逻辑,可在任意项目中复用 |
| 什么代码归入shared? | 被多个垂直文件夹使用的业务逻辑 |
| 什么代码归入infrastructure? | 横切关注点(日志、配置、DB) |
| 垂直文件夹之间如何交互? | 直接导入,但依赖方向由Linter强制管控 |
| 两个垂直文件夹需要共享代码? | 提取到/shared — 永远不要在Linter中添加例外规则 |
| 何时创建新的垂直文件夹? | 当一个概念足够独立需要单独维护,或共享代码规模足够时 |