Folder StructureFeature-BasedScalabilityCode OrganizationArchitecture

Feature-Based Folder Structure

How you organize files determines how fast your team ships. Traditional structures group by file type (components/, hooks/, utils/). Feature-based structures group by domain (auth/, dashboard/, cart/). One scales. The other doesn't.

20 min read13 sections
01

Overview

Feature-based folder structure organizes code by domain or feature — not by file type. Instead of putting all components in components/, all hooks in hooks/, and all services in services/, you group everything related to a feature together: auth/ contains its components, hooks, services, types, and tests.

This approach scales because adding a feature means adding a folder — not scattering files across a dozen directories. Deleting a feature means deleting one folder. Two teams working on different features never touch the same files.

In interviews, "How would you structure a large frontend app?" is one of the most common architecture questions. The answer reveals whether you think about code organization at scale — or just at the file level.

Why this matters

At 50 files, any structure works. At 500 files, structure determines whether your team ships features in days or weeks. Feature-based organization is the industry standard for production React, Vue, and Angular applications.

02

The Problem: Traditional Folder Structure

Most tutorials and starter projects organize code by file type. This feels natural at first — but it breaks down as the application grows.

Traditional (Type-Based) Structuretext
src/
├── components/
│   ├── LoginForm.tsx
│   ├── SignupForm.tsx
│   ├── Dashboard.tsx
│   ├── DashboardStats.tsx
│   ├── DashboardChart.tsx
│   ├── UserProfile.tsx
│   ├── UserAvatar.tsx
│   ├── CartItem.tsx
│   ├── CartSummary.tsx
│   ├── ProductCard.tsx
│   ├── ProductList.tsx
│   ├── SearchBar.tsx
│   ├── SearchResults.tsx
│   ├── Header.tsx
│   ├── Sidebar.tsx
│   └── ... (200+ more components)
├── hooks/
│   ├── useAuth.ts
│   ├── useDashboard.ts
│   ├── useCart.ts
│   ├── useSearch.ts
│   └── ... (50+ more hooks)
├── services/
│   ├── authService.ts
│   ├── dashboardService.ts
│   ├── cartService.ts
│   └── ... (30+ more services)
├── utils/
│   ├── authUtils.ts
│   ├── formatters.ts
│   └── validators.ts
├── types/
│   ├── auth.ts
│   ├── dashboard.ts
│   ├── cart.ts
│   └── ... (20+ more type files)
└── styles/
    ├── login.css
    ├── dashboard.css
    └── ... (40+ more stylesheets)
🔍

Scattered Feature Code

To understand the 'auth' feature, you open: components/LoginForm.tsx, hooks/useAuth.ts, services/authService.ts, types/auth.ts, styles/login.css — 5 files in 5 directories.

💥

Merge Conflicts

Two developers adding unrelated features both edit components/index.ts or hooks/index.ts. The barrel file becomes a constant source of merge conflicts.

👤

Unclear Ownership

Who owns components/? Everyone. When everyone owns a directory, nobody maintains it. Files accumulate, naming conventions drift, dead code lingers.

🗑️

Difficult Deletion

Removing a feature means hunting through every directory for related files. Miss one and you have dead code. Delete the wrong one and you break another feature.

The Real Costtext
Task: "Fix a bug in the search feature"

With type-based structure, a developer must:
  1. Open components/ → find SearchBar.tsx, SearchResults.tsx
  2. Open hooks/ → find useSearch.ts
  3. Open services/ → find searchService.ts
  4. Open types/ → find search.ts
  5. Open utils/ → find searchUtils.ts (if it exists)
  6. Open styles/ → find search.css (if it exists)
  7. Figure out which other components import from search

6+ directories, constant context-switching

Task: "Delete the search feature"

Hunt through every directory, hope you found everything,
     pray nothing else depends on searchUtils.ts

The root cause

Type-based structure optimizes for what a file is (component, hook, service). Feature-based structure optimizes for what a file does(auth, search, cart). In a large app, you almost always think in features, not file types.

03

What is Feature-Based Structure?

Feature-based structure groups all files related to a domain or feature into a single directory. Each feature folder is a self-contained module with its own components, hooks, services, types, and tests.

Feature-Based Structuretext
src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   ├── SignupForm.tsx
│   │   │   └── AuthGuard.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── services/
│   │   │   └── authService.ts
│   │   ├── types/
│   │   │   └── auth.types.ts
│   │   └── index.tspublic API (barrel export)
│   │
│   ├── dashboard/
│   │   ├── components/
│   │   │   ├── Dashboard.tsx
│   │   │   ├── StatsCard.tsx
│   │   │   └── Chart.tsx
│   │   ├── hooks/
│   │   │   └── useDashboardData.ts
│   │   ├── services/
│   │   │   └── dashboardService.ts
│   │   └── index.ts
│   │
│   └── cart/
│       ├── components/
│       │   ├── CartItem.tsx
│       │   └── CartSummary.tsx
│       ├── hooks/
│       │   └── useCart.ts
│       ├── services/
│       │   └── cartService.ts
│       └── index.ts

├── shared/                        ← truly shared code
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   └── Modal.tsx
│   ├── hooks/
│   │   └── useDebounce.ts
│   └── utils/
│       └── formatDate.ts

├── app/                           ← routing / pages
│   ├── layout.tsx
│   └── page.tsx
└── styles/
    └── globals.css
1

Each feature is a self-contained module

Everything the auth feature needs — components, hooks, services, types, tests — lives inside features/auth/. No need to look anywhere else.

2

Features expose a public API via index.ts

Other features import from the barrel file: import { LoginForm, useAuth } from '@/features/auth'. Internal implementation details are hidden.

3

Shared code lives in a separate directory

Truly reusable code (Button, Input, useDebounce, formatDate) lives in shared/. If it's used by 2+ features, it belongs in shared. If it's used by one feature, it stays in that feature.

4

Features don't import each other's internals

features/cart/ should never import features/auth/components/LoginForm.tsx directly. It imports from the public API: features/auth/index.ts. This enforces boundaries.

The mental model

Think of each feature folder as a mini-library. It has a public API (index.ts), internal implementation (components, hooks, services), and clear boundaries. Other features are consumers of the API, not collaborators on the internals.

04

Example Folder Structures

The contrast between type-based and feature-based becomes stark when you see them side by side for the same application.

❌ Type-Based — E-commerce Apptext
src/
├── components/          ← 150+ files, all mixed together
│   ├── LoginForm.tsx
│   ├── ProductCard.tsx
│   ├── CartItem.tsx
│   ├── CheckoutForm.tsx
│   ├── OrderSummary.tsx
│   ├── SearchBar.tsx
│   ├── UserProfile.tsx
│   ├── ReviewCard.tsx
│   └── ... (142 more)
├── hooks/               ← 40+ hooks, no grouping
│   ├── useAuth.ts
│   ├── useProducts.ts
│   ├── useCart.ts
│   ├── useCheckout.ts
│   ├── useSearch.ts
│   └── ... (35 more)
├── services/            ← 20+ services
│   ├── authService.ts
│   ├── productService.ts
│   ├── cartService.ts
│   └── ... (17 more)
├── types/               ← 25+ type files
└── utils/               ← 15+ utility files

Problems:
  • "Where is the checkout code?" → scattered across 5 directories
  • "Who owns components/?" → everyone (nobody)
  • "Can I safely delete ReviewCard?" → check every import in 150 files
  • Adding a feature = editing 5+ directories
✅ Feature-Based — Same E-commerce Apptext
src/
├── features/
│   ├── auth/
│   │   ├── LoginForm.tsx
│   │   ├── SignupForm.tsx
│   │   ├── useAuth.ts
│   │   ├── authService.ts
│   │   ├── auth.types.ts
│   │   └── index.ts
│   │
│   ├── products/
│   │   ├── ProductCard.tsx
│   │   ├── ProductList.tsx
│   │   ├── ProductDetail.tsx
│   │   ├── useProducts.ts
│   │   ├── productService.ts
│   │   ├── product.types.ts
│   │   └── index.ts
│   │
│   ├── cart/
│   │   ├── CartItem.tsx
│   │   ├── CartSummary.tsx
│   │   ├── useCart.ts
│   │   ├── cartService.ts
│   │   ├── cart.types.ts
│   │   └── index.ts
│   │
│   ├── checkout/
│   │   ├── CheckoutForm.tsx
│   │   ├── PaymentStep.tsx
│   │   ├── OrderSummary.tsx
│   │   ├── useCheckout.ts
│   │   ├── checkoutService.ts
│   │   └── index.ts
│   │
│   ├── search/
│   │   ├── SearchBar.tsx
│   │   ├── SearchResults.tsx
│   │   ├── useSearch.ts
│   │   ├── searchService.ts
│   │   └── index.ts
│   │
│   └── reviews/
│       ├── ReviewCard.tsx
│       ├── ReviewForm.tsx
│       ├── useReviews.ts
│       └── index.ts

├── shared/
│   ├── ui/              ← reusable primitives
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Modal.tsx
│   │   └── Spinner.tsx
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   └── useLocalStorage.ts
│   └── utils/
│       ├── formatCurrency.ts
│       └── formatDate.ts

└── app/                 ← routing layer
    ├── layout.tsx
    ├── page.tsx
    └── (routes)/

Benefits:
"Where is the checkout code?"features/checkout/
"Who owns cart?"the cart team
"Can I delete reviews?"delete features/reviews/, done
Adding a feature = adding one folder
DimensionType-BasedFeature-Based
Find feature codeSearch across 5+ directoriesOpen one feature folder
Add a featureCreate files in 5+ directoriesCreate one new folder
Delete a featureHunt through every directoryDelete one folder
Merge conflictsFrequent (shared barrel files)Rare (isolated feature folders)
Team ownershipUnclear (everyone edits components/)Clear (cart team owns features/cart/)
OnboardingMust learn entire codebase structureLearn one feature folder at a time

The litmus test

Ask yourself: "If I need to work on the cart feature, how many directories do I open?" If the answer is more than one, your structure is optimized for file types, not for developer productivity.

05

Benefits of Feature-Based Structure

The benefits compound as the application and team grow. What feels like a minor organizational choice at 10 files becomes a critical productivity multiplier at 500.

✓ Done

Scalability

Adding a feature means adding a folder. The existing structure doesn't change. 10 features or 100 features — the pattern is the same. No directory grows unbounded.

✓ Done

Discoverability

New developers find code by feature name, not by guessing which directory a file is in. 'Where is the auth code?' → features/auth/. No searching required.

✓ Done

Team Independence

The cart team works in features/cart/. The auth team works in features/auth/. They never touch the same files. Merge conflicts drop dramatically.

✓ Done

Safe Deletion

Removing a feature is deleting one folder and removing its imports from the routing layer. No orphaned files in hooks/ or services/ that nobody remembers to clean up.

Barrel Export — Feature Public APItypescript
// features/auth/index.ts — the public API
// Only export what other features need

export { LoginForm } from "./LoginForm";
export { SignupForm } from "./SignupForm";
export { AuthGuard } from "./AuthGuard";
export { useAuth } from "./useAuth";
export type { User, AuthState } from "./auth.types";

// Internal implementation details are NOT exported:
// - authService.ts (used only by useAuth internally)
// - authUtils.ts (helper functions, not needed externally)
// - AuthContext.tsx (internal context provider)

// Other features import from the barrel:
// import { useAuth, LoginForm } from "@/features/auth";
// They never reach into internal files.

Encapsulation at the folder level

The barrel export (index.ts) is the feature's public API. It defines what other features can use and hides implementation details. This is the same encapsulation principle as classes or modules — applied to folders.

06

How to Design Features

The hardest part of feature-based structure is deciding what constitutes a "feature." Too granular and you have 50 tiny folders. Too broad and you have 3 massive folders. Here's how to find the right boundaries.

What Defines a Feature?text
A feature is a cohesive domain area that:

Has its own UI (one or more pages/components)
Has its own data/state (API calls, local state)
Can be described in 1-2 words (auth, cart, search)
Could theoretically be developed by one team
Has clear boundaries (you know what's "in" and "out")

Good features:                    Bad features:
  ├── auth/                         ├── buttons/        (too granular)
  ├── dashboard/                    ├── api/            (technical layer)
  ├── cart/                         ├── data/           (too vague)
  ├── checkout/                     ├── misc/           (catch-all)
  ├── search/                       ├── helpers/        (technical layer)
  ├── user-profile/                 └── new-stuff/      (meaningless)
  ├── notifications/
  ├── settings/
  └── analytics/
1

Start with user-facing domains

Think about what the user sees: authentication, product browsing, shopping cart, checkout, profile settings. Each of these is a natural feature boundary.

2

Check the independence test

Can this feature be developed, tested, and deployed without changing other features? If yes, it's a good boundary. If it's deeply entangled with another feature, they might be one feature.

3

Keep features at 5-20 files

If a feature has 3 files, it might be too granular — consider merging with a related feature. If it has 40+ files, it's too broad — split into sub-features.

4

Name features after the domain, not the UI

Use 'auth' not 'login-page'. Use 'cart' not 'cart-sidebar'. The feature encompasses the entire domain — components, logic, services — not just one UI element.

Feature Boundary Examplestext
E-commerce App:
  features/
  ├── auth/           → login, signup, password reset, session management
  ├── products/       → product listing, detail, filtering, sorting
  ├── cart/           → add to cart, update quantity, remove, cart summary
  ├── checkout/       → shipping, payment, order confirmation
  ├── orders/         → order history, order detail, tracking
  ├── reviews/        → write review, view reviews, ratings
  ├── search/         → search bar, results, filters, suggestions
  ├── user-profile/   → profile view, edit profile, avatar
  └── notifications/  → notification list, preferences, real-time updates

SaaS Dashboard:
  features/
  ├── auth/           → login, SSO, team invites
  ├── dashboard/      → overview stats, charts, recent activity
  ├── projects/       → project list, create, settings
  ├── editor/         → document editor, toolbar, collaboration
  ├── billing/        → plans, payment methods, invoices
  ├── settings/       → account settings, team management
  └── analytics/      → usage charts, export, date range picker

When in doubt, start broad

It's easier to split a large feature into two smaller ones than to merge two small features. Start with broader boundaries and split when a feature folder grows beyond 20 files or when two sub-teams need to work independently.

07

Shared vs Feature-Specific Code

The most common question: "Where does this file go — in the feature or in shared?" The rule is simple: if it's used by one feature, it belongs in that feature. If it's used by two or more, it belongs in shared.

The Decision Ruletext
Is this code used by MORE THAN ONE feature?

  ├─ NOPut it in the feature folder
features/cart/CartItem.tsx
features/cart/useCart.ts
features/cart/cartService.ts

  └─ YESPut it in shared/
           shared/ui/Button.tsx        (used by every feature)
           shared/hooks/useDebounce.ts (used by search + autocomplete)
           shared/utils/formatDate.ts  (used everywhere)

Important: Start in the feature folder. Move to shared ONLY when
a second feature needs it. Don't pre-optimize.

"This hook MIGHT be useful elsewhere"put in shared/
"This hook IS used by auth and cart"move to shared/
Goes in Feature FolderGoes in Shared
CartItem.tsx (only used in cart)Button.tsx (used by every feature)
useCart.ts (cart-specific state)useDebounce.ts (generic utility hook)
cartService.ts (cart API calls)apiClient.ts (base HTTP client)
cart.types.ts (cart data types)common.types.ts (shared types like ID, Timestamp)
CartSummary.test.tsx (cart tests)test-utils.ts (shared test helpers)
formatCartTotal.ts (cart-specific)formatCurrency.ts (used by cart + checkout + orders)
Shared Directory Structuretext
shared/
├── ui/                    ← design system primitives
│   ├── Button.tsx
│   ├── Input.tsx
│   ├── Modal.tsx
│   ├── Spinner.tsx
│   ├── Badge.tsx
│   └── index.ts

├── hooks/                 ← generic utility hooks
│   ├── useDebounce.ts
│   ├── useLocalStorage.ts
│   ├── useMediaQuery.ts
│   └── index.ts

├── utils/                 ← pure utility functions
│   ├── formatDate.ts
│   ├── formatCurrency.ts
│   ├── cn.ts              (className merger)
│   └── index.ts

├── services/              ← shared infrastructure
│   ├── apiClient.ts       (base fetch/axios wrapper)
│   └── analytics.ts       (tracking service)

├── types/                 ← shared type definitions
│   └── common.types.ts

└── constants/
    └── routes.ts

Rules for shared/:
  • No business logic — only generic utilities
  • No feature-specific code
  • Should be stable — changes here affect every feature
  • Could theoretically be extracted into an npm package

The gravity rule

Code naturally "gravitates" toward shared/ over time. A hook starts in features/search/, then features/autocomplete/ needs it too, so you move it to shared/hooks/. This is healthy. What's unhealthy is starting everything in shared/ "just in case" — that recreates the type-based structure.

08

Real-World Example

Let's walk through structuring a real dashboard application with authentication, analytics, project management, and team settings.

Dashboard App — Feature-Based Structuretext
src/
├── app/                           ← Next.js routing layer
│   ├── layout.tsx
│   ├── page.tsxredirects to /dashboard
│   ├── login/
│   │   └── page.tsxrenders <LoginForm />
│   ├── dashboard/
│   │   └── page.tsxrenders <Dashboard />
│   ├── projects/
│   │   ├── page.tsxrenders <ProjectList />
│   │   └── [id]/
│   │       └── page.tsxrenders <ProjectDetail />
│   └── settings/
│       └── page.tsxrenders <Settings />

├── features/
│   ├── auth/
│   │   ├── LoginForm.tsx
│   │   ├── useAuth.ts
│   │   ├── AuthProvider.tsx
│   │   ├── authService.ts
│   │   ├── auth.types.ts
│   │   └── index.ts
│   │
│   ├── dashboard/
│   │   ├── Dashboard.tsxcomposes child components
│   │   ├── StatsGrid.tsx
│   │   ├── ActivityFeed.tsx
│   │   ├── QuickActions.tsx
│   │   ├── useDashboardData.ts
│   │   ├── dashboardService.ts
│   │   └── index.ts
│   │
│   ├── projects/
│   │   ├── ProjectList.tsx
│   │   ├── ProjectCard.tsx
│   │   ├── ProjectDetail.tsx
│   │   ├── CreateProjectModal.tsx
│   │   ├── useProjects.ts
│   │   ├── projectService.ts
│   │   ├── project.types.ts
│   │   └── index.ts
│   │
│   ├── analytics/
│   │   ├── AnalyticsChart.tsx
│   │   ├── DateRangePicker.tsx
│   │   ├── MetricCard.tsx
│   │   ├── useAnalytics.ts
│   │   ├── analyticsService.ts
│   │   └── index.ts
│   │
│   └── settings/
│       ├── Settings.tsx
│       ├── ProfileSection.tsx
│       ├── TeamSection.tsx
│       ├── BillingSection.tsx
│       ├── useSettings.ts
│       ├── settingsService.ts
│       └── index.ts

├── shared/
│   ├── ui/
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── Modal.tsx
│   │   ├── Table.tsx
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   └── useMediaQuery.ts
│   ├── utils/
│   │   ├── formatDate.ts
│   │   └── cn.ts
│   └── services/
│       └── apiClient.ts

└── styles/
    └── globals.css
How Pages Compose Featuresjsx
// app/dashboard/page.tsx — thin routing layer
// The page just imports and renders the feature component

import { Dashboard } from "@/features/dashboard";
import { AuthGuard } from "@/features/auth";

export default function DashboardPage() {
  return (
    <AuthGuard>
      <Dashboard />
    </AuthGuard>
  );
}

// features/dashboard/Dashboard.tsx — the actual feature
import { StatsGrid } from "./StatsGrid";
import { ActivityFeed } from "./ActivityFeed";
import { QuickActions } from "./QuickActions";
import { useDashboardData } from "./useDashboardData";

export function Dashboard() {
  const { stats, activity, isLoading } = useDashboardData();

  if (isLoading) return <Spinner />;

  return (
    <div>
      <StatsGrid stats={stats} />
      <div className="grid grid-cols-2 gap-6">
        <ActivityFeed items={activity} />
        <QuickActions />
      </div>
    </div>
  );
}

// Key insight: the page (routing layer) is thin.
// The feature (Dashboard) owns all the logic and UI.
// Pages compose features. Features compose components.

Pages are thin, features are thick

In feature-based structure, route pages are just glue — they import feature components and compose them. All the real logic, state, and UI lives inside the feature folders. This keeps the routing layer clean and the features portable.

09

Scaling Strategy

Feature-based structure isn't a one-time decision — it evolves with the application. Here's how to scale it from a small project to a large one.

Evolution of Structuretext
── Stage 1: Small App (5-15 files) ────────────────

  src/
  ├── components/
  │   ├── LoginForm.tsx
  │   ├── Dashboard.tsx
  │   └── Header.tsx
  ├── hooks/
  │   └── useAuth.ts
  └── app/
      └── page.tsx

  At this size, type-based is fine. Don't over-engineer.

── Stage 2: Growing App (30-80 files) ─────────────

  src/
  ├── features/
  │   ├── auth/
  │   │   ├── LoginForm.tsx
  │   │   ├── useAuth.ts
  │   │   └── index.ts
  │   └── dashboard/
  │       ├── Dashboard.tsx
  │       ├── StatsCard.tsx
  │       └── index.ts
  ├── shared/
  │   └── ui/
  │       └── Button.tsx
  └── app/

  Features emerge naturally. Migrate incrementally.

── Stage 3: Large App (200+ files) ────────────────

  src/
  ├── features/
  │   ├── auth/
  │   │   ├── components/
  │   │   │   ├── LoginForm.tsx
  │   │   │   └── SignupForm.tsx
  │   │   ├── hooks/
  │   │   │   └── useAuth.ts
  │   │   ├── services/
  │   │   │   └── authService.ts
  │   │   ├── __tests__/
  │   │   │   └── useAuth.test.ts
  │   │   └── index.ts
  │   └── ... (10+ features)
  ├── shared/
  └── app/

  Features get sub-directories when they grow beyond 8-10 files.

── Stage 4: Monorepo (500+ files, multiple teams) ─

  packages/
  ├── features/
  │   ├── auth/           → separate package
  │   ├── dashboard/      → separate package
  │   └── cart/           → separate package
  ├── shared-ui/          → design system package
  └── app/                → shell that composes features

  Each feature becomes an independent package with its own
  package.json, tests, and build. Teams deploy independently.
1

Start flat, add depth when needed

Begin with files directly in the feature folder (auth/LoginForm.tsx). Only add sub-directories (auth/components/LoginForm.tsx) when the feature grows beyond 8-10 files.

2

Split features when they get too large

If features/settings/ has 30+ files covering profile, billing, and team management, split into features/profile/, features/billing/, features/team/.

3

Avoid deep nesting

Maximum 3 levels deep: features/auth/components/LoginForm.tsx. If you're deeper than that, your feature boundaries are too broad or your components are too granular.

The migration path

You don't need to restructure everything at once. Migrate one feature at a time: create features/auth/, move auth-related files into it, update imports. The rest of the codebase stays untouched. Over weeks, the entire app migrates incrementally.

10

Common Mistakes

Feature-based structure is simple in concept but easy to get wrong in practice. These are the most common pitfalls.

🔀

Mixing type-based and feature-based

Having both features/auth/ AND a top-level components/ with auth-related components. Developers don't know which to use, and code ends up in both places.

Pick one approach and commit. If you're going feature-based, move ALL feature-specific code into feature folders. Only truly shared primitives (Button, Input) stay in shared/.

🏗️

Over-engineering from day one

Creating features/auth/components/forms/ inputs/EmailInput.tsx for a 10-file app. Deep nesting and sub-directories add overhead when the app is small.

Start flat. Put files directly in the feature folder. Add sub-directories only when a feature grows beyond 8-10 files. Let the structure evolve with the app.

🔬

Too many tiny features

Creating features/login/, features/signup/, features/password-reset/, features/session/ as separate features. These are all part of 'auth' and should live together.

Group related functionality into cohesive features. If two 'features' always change together and share types/services, they're one feature.

📛

Poor naming conventions

Inconsistent naming: features/auth/ uses camelCase files, features/Dashboard/ uses PascalCase folders, features/user_profile/ uses snake_case. No convention for index.ts exports.

Establish conventions early: kebab-case for folders (user-profile/), PascalCase for components (UserProfile.tsx), camelCase for hooks/services (useAuth.ts). Document in a CONTRIBUTING.md.

🔗

Features importing each other's internals

features/cart/CartItem.tsx imports features/products/productService.ts directly, bypassing the barrel export. This creates hidden coupling between features.

Features only import from each other's index.ts barrel exports. Use ESLint rules (eslint-plugin-boundaries) to enforce this. If a feature needs something from another, import from the public API.

📦

Putting everything in shared/

shared/ becomes a dumping ground with 100+ files because developers default to 'it might be reused.' This recreates the type-based structure inside shared/.

Apply the rule strictly: code starts in a feature folder. It moves to shared/ ONLY when a second feature actually needs it. Shared should be small and stable.

11

Interview Questions

These questions test whether you can think about code organization at scale — a key skill for senior frontend roles.

Q:What is a feature-based folder structure?

A: It's an approach to organizing code by domain/feature rather than by file type. Instead of grouping all components in components/, all hooks in hooks/, etc., you group everything related to a feature together: features/auth/ contains its components, hooks, services, types, and tests. Each feature is a self-contained module with a public API (barrel export). This scales better because adding/removing features is isolated to one directory.

Q:Why is feature-based structure better than type-based for large apps?

A: Three main reasons: (1) Discoverability — all code for a feature is in one place, not scattered across 5+ directories. (2) Team scaling — teams own feature folders, not shared directories, reducing merge conflicts. (3) Modularity — features are self-contained with clear boundaries, making it safe to add, modify, or delete features without affecting others. Type-based structure works for small apps but creates navigation overhead and coupling at scale.

Q:How do you decide what goes in shared/ vs a feature folder?

A: Simple rule: if code is used by only one feature, it stays in that feature folder. If it's used by two or more features, it moves to shared/. Start in the feature folder — don't pre-optimize by putting things in shared/ 'just in case.' Shared/ should contain only generic utilities (Button, useDebounce, formatDate), not business logic. Think of shared/ as an internal library that could theoretically be an npm package.

Q:How do you handle cross-feature communication?

A: Features communicate through their public APIs (barrel exports). Feature A imports from feature B's index.ts, never from internal files. For shared state, use a state management library (Zustand, Redux) or React Context at the app level. For events, use a pub/sub pattern or shared hooks. The key principle: features depend on each other's interfaces, not implementations.

Q:How would you migrate a type-based codebase to feature-based?

A: Incrementally, one feature at a time. (1) Pick the most isolated feature (e.g., auth). (2) Create features/auth/. (3) Move all auth-related files into it. (4) Update imports across the codebase. (5) Add a barrel export (index.ts). (6) Repeat for the next feature. This can be done over weeks without disrupting the team. Use ESLint import rules to prevent new code from using the old structure.

Q:What defines a good feature boundary?

A: A good feature: (1) has its own UI and data (components + API calls), (2) can be described in 1-2 words (auth, cart, search), (3) could be developed by one team independently, (4) has 5-20 files (not too granular, not too broad), (5) changes together — when you modify the feature, most changes are within its folder. If two 'features' always change together, they're probably one feature. If one feature has 40+ files, it should be split.

Q:How do you prevent features from becoming tightly coupled?

A: Three strategies: (1) Barrel exports — features only import from each other's index.ts, never internal files. (2) ESLint boundaries — use eslint-plugin-boundaries to enforce import rules at the linter level. (3) Dependency direction — establish a clear hierarchy (shared → features → app). Features can depend on shared/ but shared/ never depends on features/. Features should minimize direct dependencies on other features.

12

Practice Section

These scenarios test your ability to apply feature-based thinking to real architecture decisions.

1

You join a team with a React app that has 200+ components in a flat components/ directory, 60 hooks in hooks/, and 30 services in services/. Developers spend 20% of their time just finding files. Two teams frequently have merge conflicts in shared barrel files.

How would you restructure this codebase?

Answer: Migrate incrementally to feature-based structure. Start by identifying 8-12 natural features (auth, dashboard, search, etc.). Pick the most isolated feature first (e.g., settings). Create features/settings/, move all settings-related components, hooks, and services into it, update imports, add a barrel export. Do one feature per sprint. Set up ESLint rules to prevent new files from going into the old flat directories. Within 2-3 months, the entire codebase is migrated without a risky big-bang rewrite.

2

A developer creates a DatePicker component inside features/analytics/ because that's where they needed it first. Two months later, features/reports/ and features/scheduling/ also need a date picker. They import it directly from features/analytics/components/DatePicker.tsx.

What's wrong and how do you fix it?

Answer: Two problems: (1) DatePicker is now shared code living in a feature folder — it should move to shared/ui/DatePicker.tsx. (2) Other features are importing internal files instead of using the barrel export. Fix: move DatePicker to shared/ui/, update all imports to use shared/ui. Add an ESLint rule (eslint-plugin-boundaries) that prevents features from importing other features' internal files. This enforces the rule that cross-feature imports go through barrel exports only.

3

Your e-commerce app has a features/products/ folder with 45 files: product listing, product detail, product reviews, product comparison, product recommendations, and product admin. The folder is becoming hard to navigate.

How would you split this feature?

Answer: Split by sub-domain: features/product-catalog/ (listing, filtering, sorting — 10 files), features/product-detail/ (detail page, images, specs — 8 files), features/reviews/ (review list, write review, ratings — 8 files), features/product-comparison/ (comparison table, add to compare — 6 files), features/recommendations/ (recommendation engine, carousel — 5 files). Shared product types move to shared/types/product.types.ts. Each sub-feature gets its own barrel export and clear boundaries.

4

A junior developer asks: 'I'm building a useLocalStorage hook for the settings feature. Should I put it in features/settings/hooks/ or shared/hooks/? It's only used by settings right now, but it seems generic enough to be shared.'

What advice would you give?

Answer: Put it in features/settings/ for now. The rule is: code starts in the feature where it's first needed. If another feature needs useLocalStorage later, move it to shared/hooks/ at that point. Pre-emptively putting 'generic-looking' code in shared/ leads to a bloated shared directory full of code that's only used once. Let actual usage drive the decision, not speculation about future reuse.

5

Your team is starting a new project from scratch. The tech lead wants to set up the perfect feature-based structure on day one with sub-directories for components, hooks, services, types, and tests inside each feature — even though the app currently has 3 pages.

Is this the right approach?

Answer: No — this is over-engineering. For a 3-page app, start with a simple flat structure: features/auth/LoginForm.tsx, features/auth/useAuth.ts (no sub-directories). Add sub-directories only when a feature grows beyond 8-10 files. The structure should evolve with the app. Premature structure adds navigation overhead and makes simple changes feel heavy. Document the convention so the team knows when to add depth, but start minimal.

13

Cheat Sheet

Quick-reference rules for feature-based folder structure.

Quick Revision Cheat Sheet

Core principle: Organize by feature/domain, not by file type

Feature folder contains: Components, hooks, services, types, tests — everything for that feature

Barrel export (index.ts): Public API of the feature — other features import from here only

Shared/ directory: Generic utilities used by 2+ features (Button, useDebounce, formatDate)

Where to start new code: In the feature folder. Move to shared/ only when a second feature needs it

Good feature size: 5-20 files. Smaller = too granular. Larger = split into sub-features

Feature naming: Name after the domain (auth, cart, search), not the UI (login-page, sidebar)

Cross-feature imports: Always through barrel exports, never internal files

Nesting depth: Maximum 3 levels: features/auth/components/LoginForm.tsx

When to add sub-directories: When a feature grows beyond 8-10 files

When to split a feature: When it exceeds 20+ files or two sub-teams need independence

Migration strategy: Incremental — one feature at a time, not a big-bang rewrite

Enforce boundaries: Use eslint-plugin-boundaries to prevent cross-feature internal imports

Pages/routes are thin: Route pages just import and compose feature components