Back to TechSheets
FrontendArchitectureMicroFrontendsTechnicalLeadershipScale

The Gravity of Sprawl: Designing Scalable Frontend Architectures for 100+ Engineer Teams

Thanga MariappanSenior Architect
7 min read
Jun 3, 2026
The Gravity of Sprawl: Designing Scalable Frontend Architectures for 100+ Engineer Teams

You wake up to a P1 incident: a shared dependency update broke the checkout flow, but only for users in Western Europe. As you dig into the monorepo, you realize nobody actually knows who owns the code that failed.

This is the reality of the "gravity of sprawl." When teams grow, codebase complexity does not scale linearly; it scales exponentially. In this post, we will dissect how to design a resilient, scalable frontend architecture that survives organizational growth without sacrificing developer velocity.

The Myth of "Just Use Micro-frontends"

When frontends become too large, the immediate knee-jerk reaction of many architects is to slice everything into micro-frontends. "It worked for microservices on the backend, so it must work for the UI!" they say.

But here is the hard truth: micro-frontends are not a free lunch. They solve organizational scaling problems, not technical ones.

The Hidden Costs of Orchestration

When you split a monolithic SPA into ten micro-frontends, you trade compile-time safety for runtime uncertainty. Instead of a single build step catching a type mismatch, you now risk a runtime crash when Team A deploys a breaking change to a shared schema that Team B relies on.

Additionally, micro-frontends often lead to:

  • Bundle Bloat: Users downloading multiple instances of React, design system components, or utility libraries.
  • Fragmented User Experience: Micro-frontends built by different teams can feel like Frankenstein's monster if there is no strict adherence to a shared design language.
  • Orchestration Overhead: Managing routing, shared authentication states, and module loading configurations at runtime.

Before choosing this path, ask yourself: Are we splitting this because our code is too tangled, or because our teams are stepping on each other's toes? If it is the former, a micro-frontend will only distribute your tangled mess across multiple network boundaries.

Domain-Driven Design (DDD) in the Frontend

To scale without breaking, we must establish clear boundaries. One of the most effective ways to do this is by applying Domain-Driven Design (DDD) principles directly to the client-side directory structure.

Instead of organizing your application by technical concerns (e.g., components/, hooks/, services/), organize it by business domains (e.g., billing/, catalog/, auth/).

Directory Layout for Isolation

Here is a pattern for domain isolation inside a monorepo:

src/
├── domains/
│   ├── catalog/
│   │   ├── components/      # Internals only
│   │   ├── hooks/           # Domain-specific state
│   │   ├── api/             # Domain network calls
│   │   └── index.ts         # Strictly defined public API export
│   ├── billing/
│   └── auth/
├── shared/                  # Truly global, domain-agnostic helpers
│   ├── ui/                  # Design system primitives
│   └── utils/

The Rule of the Public API

To prevent spaghetti dependency graphs, enforce strict boundary rules: Domain A cannot import directly from the internals of Domain B.

If the billing domain needs something from the catalog domain, it must only import from the catalog/index.ts file. This index file acts as a published contract. By using linting tools like ESLint with dependency-boundary rules, you can statically block invalid imports before they ever make it to a pull request.

State Management: Escape the Single-Store Trap

Five years ago, the standard advice was to put all application state into a single, global Redux or MobX store. Today, this is considered an architectural anti-pattern for large apps.

Global state is a gravity well. It pulls unrelated domains together, creating hidden coupling. If your checkout component is listening to changes in the user profile state, any optimization in the profile flow could accidentally trigger re-renders in checkout.

The Three Tiers of State

Instead of a global monolith, split your state into three distinct tiers:

  1. Server Cache State: Use tools like React Query, SWR, or RTK Query. This is not "application state"—it is merely a client-side cache of data that lives on a database. Keep it separate, set explicit time-to-live (TTL) limits, and let the library handle deduplication and refetching.
  2. Domain Local State: State that only matters to a specific feature flow (e.g., the items currently in a user's shopping cart). Keep this state encapsulated inside the domain boundary. Use React Context or lightweight atomic state libraries like Jotai or Zustand, scoped only to that domain's root component.
  3. Global UI State: Truly global variables (e.g., current theme, active user session, language preference). This should be kept to an absolute minimum.

By segregating your state this way, you make components highly reusable and protect your application from cascading, unpredictable re-renders.

Managing Technical Debt: The "Sinking Ship" Metaphor

As a Senior Architect, your primary job is not writing code; it is managing technical debt risk.

When a frontend system becomes legacy, teams often demand a total rewrite. But total rewrites are a trap. They take twice as long as estimated, and by the time they are finished, the business requirements have shifted, leaving you with two legacy codebases instead of one.

Instead, use the Strangler Fig Pattern.

Slowly wrap your legacy application with your new architecture. Build new features in the new style. When you touch old code to fix a bug or add a minor capability, migrate that specific slice of the codebase to the new system. Over time, the new architecture grows to strangle and replace the old one, safely and incrementally.

Key Takeaways

  • Micro-frontends solve org charts, not code quality. Do not use them to escape poor modularity. Fix your boundaries first.
  • Enforce domain boundaries. Use static analysis (like ESLint rules) to prevent domains from tightly coupling with one another.
  • Stop using a single global state. Isolate server cache, domain state, and UI state to prevent unpredictable performance degradations.
  • Avoid the rewrite trap. Rely on incremental migration patterns like the Strangler Fig to safely deprecate legacy code.

How You Can Use This

  1. Audit Your Imports: Look at your codebase today. Identify three places where one business feature imports a component directly from the deep folder structure of an unrelated feature.
  2. Define public APIs: Create an index.ts file for one of your core feature directories. Explicitly export only what is necessary, and refactor external imports to point to this entry file.
  3. Introduce Boundary Lints: Add ESLint rules (such as eslint-plugin-import or tools like Nx) to programmatically restrict cross-domain imports.

Internal Linking Suggestions

  • Deep Dive into Monorepo Tooling (Nx vs. Turborepo)
  • Designing Resilient Micro-Frontend Communication Channels
  • Strategies for Client-Side Cache Eviction

Social Captions

LinkedIn

"Micro-frontends are not a magic bullet. 🚀 Many engineering teams jump into micro-frontends to solve spaghetti code, only to end up with distributed spaghetti code at runtime.

As systems scale, the real answer lies in Domain-Driven Design (DDD) on the client side, strict import boundaries, and decentralized state management.

In my latest blog post, I unpack how to scale frontend architectures to support hundreds of engineers without losing velocity or destroying bundle performance. Let's design architectures that build up, not drag down.

Read the full breakdown below! 👇 #Frontend #SoftwareArchitecture #Javascript #WebDev #EngineeringLeadership"

Medium

"The Gravity of Sprawl: Scaling Frontend Architectures Without Losing Velocity. Learn how to design robust client-side boundaries, move away from global state singletons, and leverage the Strangler Fig pattern to refactor legacy frontends incrementally."

0Likes
Share this techsheet
Share TechSheet

Discussion

0 characters