DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on Originally published at prepstack.co.in

Building Enterprise React Apps with Feature-Based Architecture (Real Code + Production Metrics)

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)
Enter fullscreen mode Exit fullscreen mode

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/Modalshared/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 })));
Enter fullscreen mode Exit fullscreen mode

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)