vertical-codebase

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vertical 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 calls
Problem: 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 或 /core

Vertical 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.ts
Flat > 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
,
infrastructure
, and
design-system
. Never from another feature vertical.
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 error
If two verticals need to communicate, the answer is to extract the shared code into
/shared
, not to create an exception in the linter.
仅声明垂直文件夹是不够的——通过Lint规则强制依赖方向
通用规则是:
功能垂直文件夹仅能从
shared
infrastructure
design-system
导入代码。禁止从其他功能垂直文件夹导入。
功能垂直文件夹  →  shared          ✅
功能垂直文件夹  →  infrastructure  ✅
功能垂直文件夹  →  design-system   ✅
功能垂直文件夹  →  功能垂直文件夹 ❌ Lint错误
shared            →  功能垂直文件夹 ❌ Lint错误
design-system     →  任意文件夹        ❌ Lint错误
如果两个垂直文件夹需要交互,解决方案是将共享代码提取到/shared,而非在Linter中添加例外规则。

TypeScript / JS —
eslint-plugin-boundaries

TypeScript / JS —
eslint-plugin-boundaries

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: [] },
          ],
        },
      ],
    },
  },
];
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

其他语言

LanguageTool
Python
import-linter
Go
internal/
package — compiler-enforced, no cross-package imports by default
RustCargo workspaces +
pub
/
pub(crate)
visibility
Java/KotlinPackage-private + ArchUnit
Monorepo (any)
nx
dependency rules,
pnpm
workspaces

语言工具
Python
import-linter
Go
internal/
package — compiler-enforced, no cross-package imports by default
RustCargo workspaces +
pub
/
pub(crate)
visibility
Java/KotlinPackage-private + ArchUnit
Monorepo (any)
nx
dependency rules,
pnpm
workspaces

Shared vs Design-System vs Feature

Shared vs Design-System vs Feature

This is the most common point of confusion. Use this table:
CodeWhere it goesWhy
Generic button, input, modal
/design-system
No business logic, reusable anywhere
useMediaQuery
,
useDebounce
/design-system
or
/lib
Pure utility, zero business logic
useCurrentUser
/auth
or
/user
vertical
Business logic, even if used everywhere
PageFilters
component (used on 5 pages)
/page-filters
vertical
Shared but product-specific
formatCurrency
/shared
or
/billing
Depends: generic math → shared, billing-specific → billing
Error boundary wrapper
/infrastructure
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

这是最容易混淆的部分,可参考下表:
代码类型存放位置原因
通用按钮、输入框、弹窗
/design-system
无业务逻辑,可在任意项目复用
useMediaQuery
,
useDebounce
/design-system
/lib
纯工具类,无任何业务逻辑
useCurrentUser
/auth
/user
垂直文件夹
包含业务逻辑,即使被多处使用
PageFilters
组件(在5个页面使用)
/page-filters
垂直文件夹
被共享但属于产品特有的功能
formatCurrency
/shared
/billing
取决于:通用数学逻辑 → shared,账单特有的逻辑 → billing
错误边界包装器
/infrastructure
属于横切关注点
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.ts
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.ts

Python

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.py
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.py

Go

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.tsx

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.tsx

Common 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
,
/reports
...).
4. Confusing "used everywhere" with "generic"
"
useCurrentUser
is used in 10 places, so it's design-system material"
→ 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. 将「被广泛使用」等同于「通用」
useCurrentUser
在10个地方被使用,所以应该归入design-system」
→ 使用频率 ≠ 通用性。它包含业务逻辑 → 应归入垂直文件夹或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:
  1. Audit first — list all files and identify natural clusters (what "talks to" what)
  2. Start with leaf domains — pick a self-contained feature with no dependents
  3. Move, don't copy — create the vertical, move files, fix imports
  4. Add a lint rule — enforce the boundary before moving on
  5. Repeat domain by domain — never do a big-bang rewrite
💡 Read
references/refactoring-guide.md
for a step-by-step migration plan.

当用户需要迁移现有的水平式结构时:
  1. 先审计 — 列出所有文件,识别自然的代码集群(哪些代码互相关联)
  2. 从独立领域开始 — 选择一个无依赖的自包含功能
  3. 移动而非复制 — 创建垂直文件夹,移动文件,修复导入路径
  4. 添加Lint规则 — 在继续迁移前先强制边界规则
  5. 逐个领域重复 — 永远不要进行大规模重写
💡 查看
references/refactoring-guide.md
获取分步迁移计划。

Quick Reference Card

快速参考卡片

QuestionAnswer
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中添加例外规则
何时创建新的垂直文件夹?当一个概念足够独立需要单独维护,或共享代码规模足够时