The Microfrontend Hangover: Complex Engineering vs. Developer Comfort

SS Saurav Sitaula

We broke the monolith. We built independent pipelines. We created a custom Pub/Sub event bus. Our architecture was a masterpiece of Webpack 5 engineering. But a few months later, I had to ask: Did we sacrifice developer sanity at the altar of scale? A reflection on the true cost of microfrontends.

The Masterpiece

Over the last few posts, I’ve outlined the journey of transforming a frontend infrastructure.

We took a massive, slow-building e-commerce monolith and shattered it into pieces. We built a host app to serve React at runtime. We orchestrated independent deployments so the Product and Cart teams could ship without blocking each other. We even engineered a global Pub/Sub event bus to keep our applications decoupled.

On paper, the architecture was a masterpiece. When I drew the diagrams on a whiteboard, I felt like a “real” software architect.

But a few months into living with this system every day, the honeymoon phase ended. The hangover began.

The Cost of Complexity

One morning, a new junior developer joined the team. His first task was simple: change the padding on a button in the Cart Drawer, and make sure it didn’t look weird when placed next to the Product Grid.

In the old monolith days, his onboarding would have been:

git clone the-monolith
npm install
npm start

Boom. The whole app runs on localhost:3000. Change the CSS, save, hot reload, done.

Here is what his onboarding actually looked like in our new microfrontend utopia:

# Clone the monorepo
git clone the-microfrontend-orchestrator

# Navigate into the sub-workspaces he actually needed
cd host-app && npm install
cd ../product-app && npm install
cd ../cart-app && npm install

# Run the orchestration script from the root
cd ..
npm run start:all

Then his terminal exploded. Three different Webpack dev servers booting up simultaneously. The host-app running on port 3000. The product-app on port 3001. The cart-app on port 3002.

His laptop fans started screaming. He made the padding change in the Cart app. But he couldn’t see it in the Host app because he had a stale cached version of the remoteEntry.js file locally and CORS issues were blocking the dynamic fetch.

He looked at me, bewildered. “Is it supposed to be this hard to change padding?”

I didn’t have a good answer.

Development Comfort vs. Engineering Scale

That moment forced me to reckon with a truth about software architecture: Every architectural decision that solves an organizational scaling problem usually makes local development worse.

We didn’t build microfrontends because the code needed it. React didn’t care if it was in one bundle or five. We built microfrontends because the organization needed it. We had too many teams stepping on each other’s toes, and a release train that moved too slowly.

Microfrontends are an organizational pattern disguised as a technical pattern. (Conway’s Law strikes again).

We traded Development Comfort for Engineering Scale.

What We Lost (The Comfort)

  • Global Refactoring: You can’t just run a “Find and Replace All” across the project anymore. The project is split across multiple boundaries. If you rename a prop that a remote exposes, the host breaks at runtime, not build time.
  • Type Safety Boundaries: If the Product app broadcasts an event payload, and the Cart app listens to it, TypeScript can’t easily protect you if those apps are in different repositories. You have to manually share type definitions or rely on hope.
  • The “Just Works” Feeling: Booting the app locally requires understanding ports, Webpack dev server proxy rules, and CORS between local dev servers.

What We Gained (The Scale)

  • Zero-Downtime Deployments: Team A ships a bug. Team A rolls back. Team B’s feature stays live and unaffected.
  • Micro-Bundles: Users download exactly the JavaScript they need for the page they are on, and nothing more. The host-app handles the heavy lifting, and the remotes share the singleton dependencies.
  • Hard Boundaries: You literally cannot import a component from the Product app into the Cart app without going through the defined Module Federation remotes. The architecture prevents spaghetti code by making it physically impossible.

Did We Overengineer?

Engineers (myself absolutely included) suffer from Resume Driven Development. We read an article about how Netflix or Spotify solved a problem, and we immediately want to implement that solution in our app with 5,000 daily active users.

When I looked at our junior developer fighting with three terminal tabs, I asked myself: Did we overengineer this? Should we have just stuck with a monolith and bought faster CI/CD servers?

After a lot of reflection, my honest answer is: No, we didn’t overengineer it. But you probably shouldn’t start here.

Our monolith was genuinely failing us. Twelve-minute build times destroy focus. Release trains that block hotfixes lose money. The pain was real, and the microfrontend architecture cured that pain exactly as advertised.

But if I were starting a brand new project tomorrow? I would build a monolith.

The Monolith First Philosophy

I’ve come to believe that you have to earn the right to build microfrontends (or microservices).

You earn that right by feeling the pain of a monolith that has grown too large for your team to handle efficiently. If you start with microfrontends on day one, you are paying the “complexity tax” before you have the revenue (or team size) to justify it.

Start simple. Build a single React app. Put it in one repo. Use standard routing. Enjoy the fast local development, the global refactoring, and the simplicity of npm start.

When—and only when—your teams start blocking each other’s deployments, or your Webpack build time gives you enough time to make a pour-over coffee, then you reach for Module Federation.

The Equilibrium

We eventually improved the local developer experience. We built better CLI tools that abstracted away the port juggling. We set up hot-reloading across the remote boundaries. We documented the Event Bus payloads thoroughly.

The hangover faded. We reached an equilibrium.

Today, our architecture hums. I can deploy a fix to the Product Catalog on a Friday afternoon without waking up the Checkout team. The system is resilient, decoupled, and fast for the user.

But I’ll never forget the lesson. Complex engineering is a tool, not a goal. We build these intricate, distributed systems not because they are inherently “better,” but because they solve specific scaling problems.

The best architecture isn’t the most complex one. The best architecture is the simplest one that solves the problems you actually have.


P.S. — That junior developer? He figured out the three terminal tabs within a week. Six months later, he was the one who suggested moving our legacy user profile logic into its own microfrontend. The complexity was a steep learning curve, but once he crested it, he saw the power. I guess the hangover is just part of the initiation.

SS

Saurav Sitaula

Software Architect • Nepal

Back to all posts
Saurav.dev

© 2026 Saurav Sitaula.AstroNeoBrutalism