Goldie Blocks
Coding Rules

Codebase Versioning

Directory-first versioning protocol for APIs, modules, routes, and reusable logic

Use directory-level versioning when the codebase must support multiple versions of the same module, route family, API, or logic set at the same time.

Directory-first rule

Do not append version numbers to filenames or component names.

Bad:

src/services/user-service-v1.ts
src/components/organisms/canvas-v2/canvas-frame.tsx

Good:

src/services/v1/user-service.ts
src/components/organisms/canvas/v2/canvas-frame.tsx

Version names belong in directories (v1, v2, v3), not in leaf filenames. This keeps imports, public names, and implementation files stable inside each version.

Stable names across versions

This works like API versioning: the resource name stays steady while the version lives in the path. An HTTP API exposes users at both its v2 and its v3 path; the caller picks the contract by path. Apply the same rule to code: the exported symbol keeps the same name in every version folder, and the import path is what selects the version.

Bad:

// block-card/v3/block-card.tsx
export function BlockCardV3(...) {}

Good:

// block-card/v2/block-card.tsx and block-card/v3/block-card.tsx
export function BlockCard(...) {}

// a file inside the owning slice selects the version via the import path
import { BlockCard } from "./v3";

FSD public doors pin the live version

Inside an FSD slice, the slice's own files and stories may select a version by its vN path. Outside the slice, consumers import through the slice's public Interface; that door pins the one live version. An external deep import such as #/shared/ui/molecules/stack/v3 sidesteps the public Interface even though the version directory itself is legitimate.

During an active migration, a temporary external version path is allowed only when the migration authority names it and records its exit condition. Once the door publishes the new version, external consumers return to the door.

Do not version-suffix an export name (BlockCardV2): the version lives in the folder, never in the symbol. If one slips in, rename it in place when it is the only live version, or export the plain name from the new folder at the next version bump. Storybook titles follow the folder of the component under test (Entities/Block/BlockCard/v4), rooted at the FSD layer — see Storybook.

When to create a version folder

Create a new version folder only when the project intentionally needs parallel support for more than one version.

Use a version folder for:

  • Breaking changes — function signatures, route contracts, or data shapes change in a way that old callers cannot consume.
  • API evolution — a route, endpoint, or public interface needs a new versioned surface while the old one remains available.
  • Parallel support — legacy and new clients, screens, documents, agents, or integrations must run side by side.

Do not create a version folder just to try an implementation idea, rename a component, or refactor the current version in place. If there is no supported legacy surface, rename or refactor the current code instead.

Structure requirements

When a new version is required:

  1. Create a new folder named vN, such as v2 or v3.
  2. Copy or refactor only the necessary logic from the previous version into the new folder.
  3. Add an index.ts barrel file inside the version folder that exports that version's public members.
  4. Update the parent router, gateway, or public entry point to expose the new version path.
  5. Keep cleanup explicit: once an old version is no longer supported, remove the old folder and update docs in the same change.

Example:

src/services/user/
├── index.ts
├── v1/
│   ├── index.ts
│   ├── user-service.ts
│   └── user-service.types.ts
└── v2/
    ├── index.ts
    ├── user-service.ts
    └── user-service.types.ts

Parent entry point:

import * as V1 from "./v1";
import * as V2 from "./v2";

export const UserService = {
	v1: V1,
	v2: V2,
};

Type isolation

Version-specific interfaces, validators, request shapes, response shapes, and adapters should live inside the relevant version folder.

Avoid importing a mutable shared type from v1 into v2 when the type is part of that version's public contract. Shared helpers are acceptable only when they are genuinely version-neutral and cannot leak contract changes across versions.

Bad:

// v2/user-service.ts
import type { UserRequest } from "../v1/user-service.types";

Good:

// v2/user-service.ts
import type { UserRequest } from "./user-service.types";

This prevents type leakage where a v2 type change accidentally breaks v1.

Naming notes

Use product names such as Canvas v2 in user-facing documentation only when describing a product generation or historical surface. In source paths, prefer directory-based versioning:

src/components/organisms/canvas/v2/

Do not introduce suffix-versioned folders or files. If an existing area still uses suffix naming, move it to a directory-first structure when touching that area for substantial work.

On this page