Every React app that survives long enough hits the same wall: the components/ folder has 240 files, every change touches a dozen of them, three teams step on each other in utils/, new hires take three weeks to find the right hook. The codebase didn't get worse — it got bigger, and the architecture stopped scaling.
The fix is feature-based architecture: organize by capability (billing, users, reports) instead of by kind (components, hooks, services). I migrated a real enterprise SaaS app this way and measured it. This is the condensed version; the full guide (the 4 rules, a complete feature in code, the lazy-load splits, and the migration playbook) is on my site 👇
Full guide: https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics
The result (same app, 240 components, before + after)
| Metric | Before (type-based) | After (feature-based) | Δ |
|---|---|---|---|
| Build time (Vite, cold) | 145s | 92s | −37% |
| Initial JS (gzipped) | 1.2 MB | 380 KB | −68% |
| Routes loading > 1 MB | 14 | 0 | gone |
| Files touched per PR | 12.4 | 5.1 | −59% |
| Merge-conflict rate | 21% | 6% | −71% |
| Onboarding to first PR | 18 days | 7 days | −61% |
| Cross-feature bugs/qtr | 23 | 4 | −83% |
Why type-based folders break
components/ hooks/ services/ works to ~30 components. Past that: ownership is fuzzy ("who owns useUserPermissions?"), every change touches everything, teams collide in utils/, dead code accumulates, and bundle splitting is impossible because billing is scattered across 8 folders. The question that matters — "where does the billing feature live?" — has the answer "everywhere."
The structure: organize by capability
src/
├── shared/ (ui, lib, api — cross-cutting, NO business logic)
├── features/
│ ├── auth/ (api, components, hooks, store, types, index.ts)
│ ├── billing/ (api, components, hooks, store, types, index.ts)
│ ├── users/
│ └── reports/
└── app/ (router, providers — composition root)
Each feature is self-contained with a single public entry point.
The 4 rules that make it work
1. One public index.ts per feature. It exposes 4 exports; the feature has 20+ internal files. That ratio is the boundary.
2. No cross-feature imports except via that index. Enforce with ESLint no-restricted-imports on features/*/* — at PR time, not in code review.
3. Cross-cutting code lives in shared/, with no business logic. Button/Modal → shared/ui. formatInvoice() → features/billing/lib. The test: if it names a domain concept, it's a feature concern.
4. Features depend on shared/ only, never each other. Billing needs the user? It takes a userId, not an import from auth. Autonomy is the point.
The biggest win: natural code-splitting
Because each feature is self-contained, lazy-loading is one line at the composition root:
const BillingRoutes = lazy(() => import('@/features/billing').then(m => ({ default: m.BillingRoutes })));
const AdminRoutes = lazy(() => import('@/features/admin').then(m => ({ default: m.AdminRoutes })));
A user who never opens /admin/* never downloads the admin code. That's what took the initial bundle 1.2 MB → 380 KB — impossible when billing is scattered across 8 folders.
Migrate incrementally, never a rewrite
Add shared//features//app/ alongside the old folders (they coexist). Move the smallest, most isolated feature first (usually auth). Lock its boundary with ESLint. Repeat per feature, largest pain first. Drain the old folders. Each step ships independently — no big-bang PR. ~3 sprints for one squad in spare cycles, payback in ~6 weeks.
The honest part
Below ~20 components this is bureaucracy — don't impose it. Above ~50 with multiple teams, it's survival. And discipline is the architecture: without the ESLint boundary rule, "feature folders" decays into "type folders with a different name" in six months.
The mental model: type-based folders optimize for "where do I put a hook?" Feature-based folders optimize for "where does this capability live?" Get that right and your app stops getting harder to change as it gets bigger — the actual definition of architecture working.
The full guide has the 4 rules in depth, a complete billing feature end-to-end (types, api, hooks, components, routes, tests), the bundle-split output, the state-per-feature pattern, the migration playbook, and the full metrics matrix:
https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics
Originally published on PrepStack.
Top comments (0)