Skip to content

Feature-Based vs Layer-Based Structure

expert16 min read

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.

Mental Model

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.

Quiz
A team has 150 components in a flat components/ folder. A new engineer needs to modify the quiz feature. What is the likely experience?

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
Key Rules
  1. 1Files that change together should live together -- co-location over categorization
  2. 2Every feature folder has an index.ts barrel export as its public API
  3. 3Cross-feature imports ONLY through barrel exports, never into internal files
  4. 4Shared code lives in shared/ -- promoted there only when actually reused by 3+ features
  5. 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/
Quiz
In Feature-Sliced Design, a feature (submit-quiz) needs to display user information. Where should it get the User component from?

When to Use Each Approach

FactorLayer-BasedFeature-BasedFeature-Sliced Design
Team size1-5 engineers5-30 engineers15-50+ engineers
Codebase sizeUnder 50 components50-300 components200+ components
Learning curveZero -- everyone knows itLow -- intuitive co-locationMedium -- strict layer rules
Feature isolationNone -- everything can import everythingGood -- barrel exports enforce boundariesStrict -- layer rules enforced by linting
Refactoring costHigh at scale -- features scatteredLow -- delete one folderLow -- clear boundaries
Best forSmall projects, prototypes, solo devsMost production appsLarge teams with strict governance needs
Start simple, evolve when it hurts

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:

Execution Trace
Step 1: Identify a feature
Pick the most self-contained feature -- one with clear boundaries. Quiz is a good candidate.
Start with the easiest win
Step 2: Create the feature folder
Create features/quiz/ with components/, hooks/, utils/, types.ts, and index.ts
Mirror the internal structure of the feature
Step 3: Move files
Move QuizBlock, QuizTimer from components/. Move useQuizState from hooks/. Move validateAnswer from utils/.
Update all import paths
Step 4: Create barrel export
Export only public API from features/quiz/index.ts. Internal files are no longer importable from outside.
This is the key boundary
Step 5: Add lint rule
Configure ESLint boundaries plugin to prevent direct imports into feature internals
Enforce the boundary with tooling
Step 6: Repeat
Pick the next feature. Over 2-3 sprints, the codebase organically migrates.
No big-bang rewrite needed
Quiz
During migration, you find a utility used by both quiz and course-progress features. Where should it live?

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.

Common Trap

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 doWhat 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