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:
| Size | Teams | Structure | Problem |
|---|---|---|---|
| 1–10K lines | 1 | Flat or type-based | Simple; no coordination overhead |
| 10–50K lines | 2–5 | Feature-based | Multiple teams need boundaries |
| 50–100K lines | 5–15 | Layered features | Build time grows, test time grows |
| 100–200K lines | 15–30 | Monorepo with packages | Single package too large; need module federation |
| 200K+ lines | 30–50+ | Module federation + microservices | Build 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?
- Extract independent features to separate packages.
- Use module federation to import them.
- Keep shared code in a central shared/ package.
- Deploy each module independently.
Takes 6–12 months for a 100K line codebase with careful planning.