Architecture Improvement
Purpose
Establish clear module boundaries by feature, enforce unidirectional imports, and prevent circular dependencies. The folder structure should answer "where does this code live?" in under 10 seconds.
Universal — feature-based folder organization, unidirectional import rules, circular-dependency detection, and barrel-file discipline apply to any modular codebase. Server/Client component boundary is React-specific; other frameworks have analogous concerns.
Procedure
Precondition — is the migration worth it? Feature-based organization pays off at scale, not in small apps. If the project is small (roughly < 20-30 components, 1-2 features) and none of the Triggers apply, keep the simpler layer-based layout — empty features/x/{api,hooks,...} shells add navigation cost with no benefit. Layer-based isn't obsolete; it's the right default until a Trigger appears (can't find files / one change spans many folders / cross-feature coupling causes bugs).
Reorganize from layer-based to feature-based
- Layer-based (group by technical type):
components/, hooks/, utils/, types/
- Feature-based (vertical slice per domain):
features/auth/{api,components,hooks,stores,types,utils}/
- Each feature is a self-contained vertical slice; truly cross-cutting code stays in
shared/ (a hybrid layout is normal and expected)
Define the import rule (unidirectional)
shared/ → features/ → app/
shared/ can be imported by features/ and app/
features/ can be imported by app/ only — never by another feature
app/ (pages, routes) is the top — imported by nothing
Detect and enforce the boundary (don't rely on discipline)
- Quick manual scan: grep for cross-feature imports
- Persistent enforcement: a lint rule that fails CI on cross-feature or wrong-direction imports — discipline alone always erodes
- Resolution per violation: promote the shared piece to
shared/, OR establish a documented dependency (rare, requires ADR)
- Keep
shared/ generic, not a junk drawer — the real failure mode of feature-based architecture. Promote only code with no domain knowledge (a Button, formatDate, an HTTP client); "used by 2 features" alone is not the bar. Domain-specific code shared by two features belongs to one feature, imported by the other via a documented exception. Sub-organize shared/ (ui / lib / api / config) — never a flat dump.
- See Implementation for the exact lint config per stack
Detect and resolve circular imports (validation loop)
- Run a circular-dependency detector (
madge for JS/TS — see Implementation)
- For each cycle reported: break by extracting the shared dependency to a third module
- Type-only cycles (e.g.
import type in TS) are erased at compile time and don't break at runtime — fix real (value) cycles first; de-prioritize type-only ones
- Re-run until value-level cycles = 0 (don't ship with runtime circular imports)
Audit barrel files (index.ts re-exports)
- Keep barrels ONLY at the
shared/ public-API layer (consumed by many features)
- Never at the feature root (
features/auth/index.ts) — direct subpath imports instead: from '@/features/auth/api/login'
- Barrel files defeat tree-shaking and create circular dep risk; the convenience never justifies it inside features
Reposition server/client rendering boundaries (frameworks with a server/client split)
- Default to server rendering; mark a unit as client-only where it genuinely needs interactivity (component-local state, browser APIs, event handlers)
- Audit for over-marking — units that don't actually use client features should drop the directive to stay on the server
- Coordinate with
render-strategy-decision
- See Implementation for framework-specific detection (e.g., the
'use client' audit in React/Next.js)
Document the structure
- ADR: "Why feature-based" + import rule + barrel policy
docs/architecture.md with directory diagram
Completion Criteria
Stop & Ask (AI must pause for user approval)
- Before moving files across directory boundaries — git history and IDE bookmarks break; user confirms the new layout first
- Before introducing eslint-plugin-import enforcement that would fail current CI — coordinate with team
- Before promoting a shared piece to
shared/ — verify it's actually used by 2+ features and won't be specialized later
Output
- Folder reorganization: feature-based structure with documented import rule
- ADR:
docs/adr/ADR-NNN-feature-based-architecture.md documenting the decision (Context / Options / Decision / Consequences)
- ESLint config:
eslint-plugin-import with no-restricted-paths.zones enforcement
- Migration commits: one commit per moved subsystem; commit format
refactor(arch): move <subsystem> to feature-based layout
- Migration log (paste into PR description): which features moved, which barrel files removed, which cycles broken
Implementation
React + Next.js (default)
- Folder:
app/, features/, shared/, lib/
- Import enforcement:
eslint-plugin-import no-restricted-paths.zones in .eslintrc (CI-blocking):"import/no-restricted-paths": ["error", {
"zones": [
{ "target": "src/features/*/!(index.ts)", "from": "src/features/*", "except": ["./"] },
{ "target": "src/shared", "from": "src/features" },
{ "target": "src/shared", "from": "src/app" }
]
}]
Quick manual scan: grep -rE "from ['\"]@/features/[a-z]+/" src/features/
- Circular detection:
npx madge --circular src/
- Server/Client boundary:
grep -rn "'use client'" src/ — remove the directive from any file that uses no hooks (useState/useEffect/useReducer), no browser APIs (window/document/localStorage), and no React event handlers (onClick etc.)
- Barrels: avoid inside features; OK at
shared/ public API
Other stacks
- Vue / Nuxt: Nuxt auto-imports across
composables/, components/, utils/ — feature boundary requires explicit configuration; nuxt.config.ts has imports.dirs for fine-grained control
- SvelteKit: feature folders under
src/lib/; routes in src/routes/ ARE the app layer; barrel discouraged because Vite tree-shakes individual exports well
- Angular: feature modules + standalone components;
eslint-plugin-import + @nx/eslint-plugin for cross-feature enforcement; lazy-loaded routes via loadChildren
- Universal:
madge works for any JS/TS project; feature-based organization (vertical slices) is a universal pattern from Domain-Driven Design; circular dependencies are a smell in any language
Related skills
code-refactoring — for changes within a single file
render-strategy-decision — when reorganizing Server/Client component boundaries
component-quality — for component extraction during the restructure
Reference
- Key insight encoded: Enforce unidirectional imports — features may import only from
shared/, never from sibling features. When two features need to share, the right move is to promote the shared piece up to shared/, not to barrel-export across siblings. Barrel files are convenient but cost tree-shaking and create circular dep risk; use them only where there's a stable 3+-consumer public API.
1---2name: architecture-improvement-23description: Reorganize a project into a feature-based folder structure with unidirectional imports. Use when adding a new feature conflicts with existing structure, when teammates can't find files, when circular dependencies appear in build logs, or at the start of a new quarter. Not for changes within a single file (use code-refactoring) or component extraction (use component-quality).4license: MIT5---67# Architecture Improvement89## Purpose10Establish clear module boundaries by feature, enforce unidirectional imports, and prevent circular dependencies. The folder structure should answer "where does this code live?" in under 10 seconds.1112**Universal** — feature-based folder organization, unidirectional import rules, circular-dependency detection, and barrel-file discipline apply to any modular codebase. Server/Client component boundary is React-specific; other frameworks have analogous concerns.1314## Procedure1516**Precondition — is the migration worth it?** Feature-based organization pays off at scale, not in small apps. If the project is small (roughly < 20-30 components, 1-2 features) and none of the Triggers apply, keep the simpler layer-based layout — empty `features/x/{api,hooks,...}` shells add navigation cost with no benefit. Layer-based isn't obsolete; it's the right default until a Trigger appears (can't find files / one change spans many folders / cross-feature coupling causes bugs).17181. **Reorganize from layer-based to feature-based**19 - Layer-based (group by technical type): `components/`, `hooks/`, `utils/`, `types/`20 - Feature-based (vertical slice per domain): `features/auth/{api,components,hooks,stores,types,utils}/`21 - Each feature is a self-contained vertical slice; truly cross-cutting code stays in `shared/` (a hybrid layout is normal and expected)22232. **Define the import rule (unidirectional)**24 ```25 shared/ → features/ → app/26 ```27 - `shared/` can be imported by `features/` and `app/`28 - `features/` can be imported by `app/` only — never by another feature29 - `app/` (pages, routes) is the top — imported by nothing30313. **Detect and enforce the boundary (don't rely on discipline)**32 - Quick manual scan: grep for cross-feature imports33 - Persistent enforcement: a lint rule that fails CI on cross-feature or wrong-direction imports — discipline alone always erodes34 - Resolution per violation: promote the shared piece to `shared/`, OR establish a documented dependency (rare, requires ADR)35 - **Keep `shared/` generic, not a junk drawer** — the real failure mode of feature-based architecture. Promote only code with *no domain knowledge* (a Button, `formatDate`, an HTTP client); "used by 2 features" alone is not the bar. Domain-specific code shared by two features belongs to one feature, imported by the other via a documented exception. Sub-organize `shared/` (`ui` / `lib` / `api` / `config`) — never a flat dump.36 - See Implementation for the exact lint config per stack37384. **Detect and resolve circular imports (validation loop)**39 - Run a circular-dependency detector (`madge` for JS/TS — see Implementation)40 - For each cycle reported: break by extracting the shared dependency to a third module41 - Type-only cycles (e.g. `import type` in TS) are erased at compile time and don't break at runtime — fix real (value) cycles first; de-prioritize type-only ones42 - Re-run until value-level cycles = 0 (don't ship with runtime circular imports)43445. **Audit barrel files (`index.ts` re-exports)**45 - Keep barrels ONLY at the `shared/` public-API layer (consumed by many features)46 - Never at the feature root (`features/auth/index.ts`) — direct subpath imports instead: `from '@/features/auth/api/login'`47 - Barrel files defeat tree-shaking and create circular dep risk; the convenience never justifies it inside features48496. **Reposition server/client rendering boundaries** (frameworks with a server/client split)50 - Default to server rendering; mark a unit as client-only where it genuinely needs interactivity (component-local state, browser APIs, event handlers)51 - Audit for over-marking — units that don't actually use client features should drop the directive to stay on the server52 - Coordinate with `render-strategy-decision`53 - See Implementation for framework-specific detection (e.g., the `'use client'` audit in React/Next.js)54557. **Document the structure**56 - ADR: "Why feature-based" + import rule + barrel policy57 - `docs/architecture.md` with directory diagram5859## Completion Criteria60- [ ] Cross-feature imports = 0 (or all documented in ADR)61- [ ] `madge --circular` reports 062- [ ] Barrel files only at the 3+-consumer level63- [ ] ADR exists for the architecture decision6465## Stop & Ask (AI must pause for user approval)6667- **Before moving files across directory boundaries** — git history and IDE bookmarks break; user confirms the new layout first68- **Before introducing eslint-plugin-import enforcement** that would fail current CI — coordinate with team69- **Before promoting a shared piece to `shared/`** — verify it's actually used by 2+ features and won't be specialized later7071## Output72- **Folder reorganization**: feature-based structure with documented import rule73- **ADR**: `docs/adr/ADR-NNN-feature-based-architecture.md` documenting the decision (Context / Options / Decision / Consequences)74- **ESLint config**: `eslint-plugin-import` with `no-restricted-paths.zones` enforcement75- **Migration commits**: one commit per moved subsystem; commit format `refactor(arch): move <subsystem> to feature-based layout`76- **Migration log** (paste into PR description): which features moved, which barrel files removed, which cycles broken7778## Implementation7980### React + Next.js (default)81- Folder: `app/`, `features/`, `shared/`, `lib/`82- Import enforcement: `eslint-plugin-import` `no-restricted-paths.zones` in `.eslintrc` (CI-blocking):83 ```json84 "import/no-restricted-paths": ["error", {85 "zones": [86 { "target": "src/features/*/!(index.ts)", "from": "src/features/*", "except": ["./"] },87 { "target": "src/shared", "from": "src/features" },88 { "target": "src/shared", "from": "src/app" }89 ]90 }]91 ```92 Quick manual scan: `grep -rE "from ['\"]@/features/[a-z]+/" src/features/`93- Circular detection: `npx madge --circular src/`94- Server/Client boundary: `grep -rn "'use client'" src/` — remove the directive from any file that uses no hooks (`useState`/`useEffect`/`useReducer`), no browser APIs (`window`/`document`/`localStorage`), and no React event handlers (`onClick` etc.)95- Barrels: avoid inside features; OK at `shared/` public API9697### Other stacks98- **Vue / Nuxt**: Nuxt auto-imports across `composables/`, `components/`, `utils/` — feature boundary requires explicit configuration; `nuxt.config.ts` has `imports.dirs` for fine-grained control99- **SvelteKit**: feature folders under `src/lib/`; routes in `src/routes/` ARE the app layer; barrel discouraged because Vite tree-shakes individual exports well100- **Angular**: feature modules + standalone components; `eslint-plugin-import` + `@nx/eslint-plugin` for cross-feature enforcement; lazy-loaded routes via `loadChildren`101- **Universal**: `madge` works for any JS/TS project; feature-based organization (vertical slices) is a universal pattern from Domain-Driven Design; circular dependencies are a smell in any language102103## Related skills104- `code-refactoring` — for changes within a single file105- `render-strategy-decision` — when reorganizing Server/Client component boundaries106- `component-quality` — for component extraction during the restructure107108## Reference109- **Key insight encoded**: Enforce unidirectional imports — features may import only from `shared/`, never from sibling features. When two features need to share, the right move is to promote the shared piece up to `shared/`, not to barrel-export across siblings. Barrel files are convenient but cost tree-shaking and create circular dep risk; use them only where there's a stable 3+-consumer public API.