Feature-Based vs Layer-Based Structure
The Folder Structure Nobody Agrees On
Every new React project starts the same debate: "Where do we put things?" And somehow, five senior engineers in a room will produce six different opinions.
The answer depends on your team size, codebase complexity, and how features map to team ownership. But there are clear patterns that work at scale, and others that collapse under their own weight.
Imagine organizing a library. Layer-based is sorting books by format: all hardcovers on shelf one, all paperbacks on shelf two, all audiobooks on shelf three. Need "The Art of War" in paperback? Search through hundreds of paperbacks. Feature-based is sorting by topic: all military strategy books together -- hardcover, paperback, and audiobook side by side. Need everything about military strategy? One shelf. That is co-location.
Layer-Based Structure
The classic approach groups files by technical role:
src/
components/
Button.tsx
CourseCard.tsx
UserAvatar.tsx
ProgressBar.tsx
QuizBlock.tsx
... (200 more files)
hooks/
useAuth.tsx
useCourseProgress.tsx
useQuizState.tsx
useDebounce.tsx
... (80 more files)
utils/
formatDate.ts
calculateProgress.ts
validateQuizAnswer.ts
... (60 more files)
services/
authService.ts
courseService.ts
analyticsService.ts
types/
user.ts
course.ts
quiz.ts
This works beautifully at 20 files. At 200? Finding the files related to "course progress" means searching across components/, hooks/, utils/, services/, and types/. Every feature is scattered across five folders.
Feature-Based Structure
Group files by what they do, not what they are:
src/
features/
auth/
components/
LoginForm.tsx
AuthGuard.tsx
hooks/
useAuth.ts
services/
authService.ts
types.ts
index.ts
course-progress/
components/
ProgressBar.tsx
ProgressRing.tsx
hooks/
useCourseProgress.ts
utils/
calculateProgress.ts
types.ts
index.ts
quiz/
components/
QuizBlock.tsx
QuizTimer.tsx
QuizResults.tsx
hooks/
useQuizState.ts
utils/
validateAnswer.ts
shuffleOptions.ts
types.ts
index.ts
shared/
components/
Button.tsx
Input.tsx
Card.tsx
hooks/
useDebounce.ts
useLocalStorage.ts
utils/
formatDate.ts
cn.ts
Everything about quizzes lives in features/quiz/. Everything shared across features lives in shared/. The boundary is explicit.
The Co-location Principle
Keep things that change together close together. This is not just a preference -- it has measurable effects on developer velocity:
- Fewer files open at once -- all related files are in one folder
- Smaller blast radius -- changes to quiz logic cannot accidentally affect auth
- Clearer ownership -- "Team A owns features/quiz/" is unambiguous
- Easier deletion -- remove a feature by deleting one folder
- 1Files that change together should live together -- co-location over categorization
- 2Every feature folder has an index.ts barrel export as its public API
- 3Cross-feature imports ONLY through barrel exports, never into internal files
- 4Shared code lives in shared/ -- promoted there only when actually reused by 3+ features
- 5Domain types live in the feature folder, not in a global types/ folder
Feature-Sliced Design
Feature-Sliced Design (FSD) is a more rigorous version of feature-based structure, developed by the frontend community and widely adopted in large Russian-speaking tech companies (Yandex, VK, Tinkoff). It adds two key ideas: layers with strict dependency rules and slices within each layer.
The strict rule: a layer can only import from layers below it. Features can import from entities and shared. Widgets can import from features, entities, and shared. Pages can import from everything. But entities can NEVER import from features, and features can NEVER import from widgets.
src/
app/
providers.tsx
router.tsx
pages/
course-page/
dashboard-page/
widgets/
course-player/
sidebar-nav/
features/
complete-lesson/
submit-quiz/
toggle-favorite/
entities/
course/
model.ts
api.ts
ui/CourseCard.tsx
user/
model.ts
api.ts
ui/UserAvatar.tsx
shared/
ui/
lib/
api/
When to Use Each Approach
| Factor | Layer-Based | Feature-Based | Feature-Sliced Design |
|---|---|---|---|
| Team size | 1-5 engineers | 5-30 engineers | 15-50+ engineers |
| Codebase size | Under 50 components | 50-300 components | 200+ components |
| Learning curve | Zero -- everyone knows it | Low -- intuitive co-location | Medium -- strict layer rules |
| Feature isolation | None -- everything can import everything | Good -- barrel exports enforce boundaries | Strict -- layer rules enforced by linting |
| Refactoring cost | High at scale -- features scattered | Low -- delete one folder | Low -- clear boundaries |
| Best for | Small projects, prototypes, solo devs | Most production apps | Large teams with strict governance needs |
Start with layer-based for prototypes. Move to feature-based when you have 5+ distinct features. Consider FSD only when team size and codebase complexity demand strict governance. Premature structure is its own form of over-engineering.
Migrating from Layer-Based to Feature-Based
You do not need to rewrite everything at once. Migrate feature by feature:
Scaling to 50+ Features
At 50 features, even feature-based structure needs additional organization:
src/
features/
learning/
quiz/
lesson-player/
course-progress/
spaced-repetition/
social/
user-profile/
leaderboard/
community-feed/
commerce/
subscription/
checkout/
pricing/
platform/
auth/
notifications/
settings/
search/
Group features into domains. Each domain is a folder containing related features. This maps naturally to team ownership: Team Learning owns features/learning/, Team Growth owns features/social/, etc.
Do not create deeply nested folder structures just because you can. Three levels deep (features/learning/quiz/components/QuizBlock.tsx) is the practical maximum. Beyond that, you spend more time navigating folders than writing code. If a feature folder has more than 15-20 files, consider splitting it into sub-features at the same level.
| What developers do | What they should do |
|---|---|
| Creating a features/ folder but still importing across feature boundaries freely Feature folders without enforced boundaries are just reorganized layer-based code. The boundary is the whole point. | Enforcing boundaries with barrel exports and ESLint rules |
| Moving everything to shared/ because it might be reused Premature extraction to shared/ creates a dumping ground. shared/ should be small -- only truly cross-cutting code. | Keeping code in features until it is actually reused by 3+ consumers |
| Adopting Feature-Sliced Design for a 3-person team with 30 components FSD adds cognitive overhead (learning the layer rules, configuring lint rules). For small teams, the overhead exceeds the benefit. | Using simple feature-based structure and adding FSD layers when complexity demands it |