Skip to main content

Scaling React Beyond 100,000 Lines: Architecture

A React codebase grows from thousands to hundreds of thousands of lines as teams expand and features accumulate. At 50,000 lines, type-based structure breaks; you need features. At 100,000 lines, a single monolith becomes slow to build and hard to reason about. At 200,000+ lines, parallel development across teams becomes necessary: you need module federation, code ownership, governance, and sophisticated tooling. This article shows you how the largest React teams (Airbnb, Meta, Stripe) organize massive codebases, from module federation to governance models to scaling test suites.

The Scaling Journey: From 1 Team to 50

Each milestone requires new architecture:

SizeTeamsStructureProblem
1–10K lines1Flat or type-basedSimple; no coordination overhead
10–50K lines2–5Feature-basedMultiple teams need boundaries
50–100K lines5–15Layered featuresBuild time grows, test time grows
100–200K lines15–30Monorepo with packagesSingle package too large; need module federation
200K+ lines30–50+Module federation + microservicesBuild and deployment parallelized

At 100,000 lines with 20 developers, a single yarn build takes 45 seconds, and tests take 3 minutes. A team waiting for CI to pass wastes 2–3 hours per day. The solution: split the codebase into independent modules that build and test in parallel.

Code Splitting and Lazy Loading

Before scaling architecture, maximize code splitting. Ship only what the user needs.

// BEFORE: all code in one bundle (500 KB)
import { Dashboard } from './pages/Dashboard';
import { Orders } from './pages/Orders';
import { Payments } from './pages/Payments';

export default function App() {
return (
<Router>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/orders" element={<Orders />} />
<Route path="/payments" element={<Payments />} />
</Router>
);
}

Instead, lazy load:

// AFTER: code splitting with React.lazy
import { Suspense, lazy } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Orders = lazy(() => import('./pages/Orders'));
const Payments = lazy(() => import('./pages/Payments'));

export default function App() {
return (
<Suspense fallback={<Loading />}>
<Router>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/orders" element={<Orders />} />
<Route path="/payments" element={<Payments />} />
</Router>
</Suspense>
);
}

Now each route is a separate chunk. Initial bundle is 150 KB; Dashboard loads on demand.

Vite and webpack support automatic code splitting:

// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
// Split vendors
'vendor-react': ['react', 'react-dom'],
// Split by feature route
'dashboard': ['./src/pages/Dashboard'],
'orders': ['./src/pages/Orders'],
}
}
}
}
};

This is the cheapest, highest-impact optimization. Before refactoring, split your code.

Module Federation: Sharing Code Across Independent Builds

Module Federation (webpack 5+) lets multiple applications build independently and share modules at runtime.

Imagine Uber: the driver app, passenger app, and admin dashboard are separate codebases. They share a components library, authentication service, and logging. Without federation, changes to the shared library require rebuilding all three apps. With federation, the library builds once, and all three consume it.

Module Federation Setup

// packages/components/webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new ModuleFederationPlugin({
name: 'componentsLib', // unique name
filename: 'remoteEntry.js', // entry file
exposes: {
// Export Button and Card at runtime
'./Button': './src/components/Button',
'./Card': './src/components/Card',
},
shared: {
react: { singleton: true, requiredVersion: '18.0.0' },
'react-dom': { singleton: true, requiredVersion: '18.0.0' },
},
}),
],
};

// packages/app/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'mainApp',
remotes: {
// Import from componentsLib at runtime
'@components': 'componentsLib@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '18.0.0' },
'react-dom': { singleton: true, requiredVersion: '18.0.0' },
},
}),
],
};

Usage:

// packages/app/src/pages/Dashboard.jsx
// Import Button from the components library at runtime
import { Button } from '@components/Button';

export function Dashboard() {
return <Button>Deploy</Button>;
}

Build and development:

# Start components library
cd packages/components
npm run dev # runs on http://localhost:3001

# In another terminal, start app
cd packages/app
npm run dev # imports from localhost:3001

# Changes to components are instantly reflected in app

Deploy components library independently. The app's HTML points to the deployed remoteEntry.js, so it picks up the latest version at runtime. No need to rebuild or redeploy the app.

Module Federation is cutting-edge (2026) and enables true independent scaling: teams can develop, test, and deploy features in parallel.

Package Ownership and CODEOWNERS

At 100+ developers, ownership becomes critical. Establish CODEOWNERS:

# .github/CODEOWNERS

# Auth team owns everything in features/auth
packages/features/auth/ @team-auth

# Components team owns UI library
packages/ui-components/ @team-design

# Payments team owns payment features
packages/features/payments/ @team-payments

# Each CODEOWNER must approve PRs to their package

Benefits:

  • PRs to auth automatically request review from auth team.
  • Teams know who to ask about their code.
  • Changes to shared packages require sign-off from multiple teams.

Governance and Architectural Guidelines

Large teams need written guidelines to prevent chaos.

1. Architecture Decision Records (ADRs)

Document major decisions:

# ADR-001: Feature-Based Module Structure

## Context
We're growing to 50 developers and need clear boundaries.

## Decision
Organize code in features/: auth/, payments/, orders/, etc.
Each feature owns its components, hooks, services.

## Consequences
- Positive: Clear ownership, independent development
- Negative: Code duplication possible; need discipline

## Status: Accepted (2026-01-15)

Store ADRs in the repo. New developers read them and understand why the structure exists.

2. Import Rules (Enforced by ESLint)

Documented and automated:

// .eslintrc.json
{
"rules": {
"import/no-cycle": "error",
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": "./packages/features/auth",
"from": "./packages/features/payments",
"message": "auth and payments are independent; don't create coupling"
},
{
"target": "./packages/ui-components",
"from": "./packages/features",
"message": "ui-components is a library; it cannot depend on features"
}
]
}
]
}
}

3. Code Review Checklist

Enforce via PR templates:

# Code Review Checklist

- [ ] Code follows architecture guidelines (ADR-001)
- [ ] No circular dependencies introduced (run `npm run lint`)
- [ ] Imports are explicit and restricted (see .eslintrc.json)
- [ ] Tests cover new logic (>80% coverage)
- [ ] Bundle size impact measured (run `npm run analyze`)

Parallel Testing and CI/CD

At 100,000 lines, a serial test suite takes 10+ minutes. Use parallel execution:

# Run tests in 4 parallel workers
npm test -- --maxWorkers=4

# Or use a CI matrix

In GitHub Actions:

# .github/workflows/test.yml
name: Test

on: [push]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v2
- run: npm install
- run: npm test -- --shard=${{ matrix.shard }}/4

This splits tests across 4 machines, reducing total time from 12 minutes to 3 minutes.

Monorepo vs. Microservices

At 200,000+ lines, you may consider splitting into separate repositories (polyrepo) or even microservices. When should you?

Stay monorepo if:

  • Code is tightly integrated (refactoring requires touching many files).
  • Teams communicate frequently.
  • Shared libraries are actively developed.
  • You value atomic commits.

Consider polyrepo if:

  • Teams are truly independent (different products, different languages).
  • Release cycles are different (one ships weekly, another quarterly).
  • Security or isolation is critical.

Most 2026 tech companies use hybrid: monorepo for shared code (components, auth, utils), polyrepo for independent products. Stripe, for example, has a monorepo for shared libraries and separate repos for each product.

Measuring and Monitoring Large Codebases

Use metrics to catch problems early:

1. Bundle Size Trend

npm run build -- --report
# Track over time; alert if bundle grows by >5%

2. Build Time

# Time each build
npm run build --timing
# Alert if build time grows >10%

3. Test Coverage and Speed

npm test -- --coverage
# Maintain >80% coverage
# Alert if test suite takes >5 minutes

4. Dependency Graph Health

npm install circular-dependency-plugin
# Run in CI; fail if any new cycles are introduced

5. Code Ownership Coverage

# Ensure every folder has a CODEOWNER
# Alert if any files lack ownership

Key Takeaways

  • Code split aggressively: lazy load pages, split vendors, reduce initial bundle.
  • Use module federation to build and deploy independent modules in parallel.
  • Establish ownership with CODEOWNERS and clear package boundaries.
  • Document architecture decisions (ADRs) so teams understand why structure exists.
  • Enforce rules with ESLint, pre-commit hooks, and CI checks.
  • Parallelize tests and CI/CD to keep feedback fast as codebase grows.
  • Monitor bundle size, build time, and test speed; alert on regressions.

Frequently Asked Questions

At what size should I use module federation?

When a single build takes over 30 seconds, and teams are independent (different ownership, different release cadence), module federation starts paying off. Before that, optimize with code splitting and lazy loading.

Can I add module federation to an existing monorepo?

Yes, but it's complex. Start with code splitting and lazy loading. Add module federation when you're ready to decouple builds. Incremental is better than all-at-once.

What if my codebase grows to 500,000 lines?

At that scale, split the product: Uber's driver app, passenger app, and admin are separate. Each is independently a 50K–100K line codebase. Coordination happens at the module federation layer. This is how Meta, Google, and Microsoft scale.

Should I use Nx, Turborepo, or just Yarn workspaces?

For codebases under 200K lines with 20 developers, Yarn workspaces is enough. Nx and Turborepo add sophisticated caching and task orchestration for larger teams. Choose based on pain: if CI takes hours, invest in Nx or Turborepo.

How do I refactor a monolith into module federation?

  1. Extract independent features to separate packages.
  2. Use module federation to import them.
  3. Keep shared code in a central shared/ package.
  4. Deploy each module independently.

Takes 6–12 months for a 100K line codebase with careful planning.

Further Reading