Micro-Frontends: Scaling Teams and Codebases

ruijadom
@ruijadom
Modern frontend applications are growing in complexity, and so are the teams building them. When multiple teams need to work on different features of the same application, traditional monolithic architectures can become bottlenecks. Enter micro-frontends: an architectural pattern that brings the principles of microservices to the frontend world.
In this post, we'll explore how to implement micro-frontends using Vite and Module Federation, and why this approach can transform how your frontend team operates.
What Are Micro-Frontends?
Micro-frontends extend the concept of microservices to frontend development. Instead of building a single monolithic frontend application, you split it into smaller, independently deployable pieces that are owned by different teams.
Monolithic vs Micro-Frontend Architecture
Monolithic Frontend:
- Tight coupling between features
- Coordination required for deployments
- Single point of failure
Micro-Frontend Architecture:
Each micro-frontend:
- Can be developed independently
- Has its own deployment pipeline
- Can use different frameworks or versions (though this requires careful consideration)
- Is owned by a specific team with domain expertise
Should You Use Micro-Frontends?
Before diving into implementation, it's crucial to understand that micro-frontends aren't a silver bullet. They add architectural complexity and should only be adopted when the benefits clearly outweigh the costs.
Use Micro-Frontends When:
You have multiple autonomous teams
If you have 3+ frontend teams working on different domains (e.g., search, booking, payments), micro-frontends enable parallel development without coordination bottlenecks. Each team can own their domain end-to-end.
Your application has clear domain boundaries
Successful micro-frontends map to business domains. If you can't draw clear lines between features (e.g., "this is the search module, this is the booking module"), the architecture will fight against you.
You need independent deployment cycles
When the booking team needs to deploy bug fixes without waiting for the payment team to finish their sprint, independent deployments become valuable. This is especially critical for large organizations with compliance requirements.
Different features have different scaling needs
Perhaps your search feature needs frequent updates and optimization, while your reviews page is stable. Micro-frontends allow you to allocate resources proportionally.
You're managing technical debt incrementally
Migrating from a legacy framework? Micro-frontends let you rewrite piece by piece. Keep the old admin panel in AngularJS while building new features in React.
You have a long-lived, complex application
If you're building a platform that will evolve over 5-10 years with changing teams, micro-frontends provide the flexibility to adapt without full rewrites.
Avoid Micro-Frontends When:
You have a small team (< 8 developers)
The coordination overhead of multiple repositories, build pipelines, and integration testing will slow you down. A well-structured monolith with clear module boundaries is simpler and more efficient.
Your application is in early stages
If you're still finding product-market fit or your features are rapidly changing, the rigidity of micro-frontend boundaries will hinder experimentation. Start with a monolith and extract later if needed.
Domain boundaries are unclear
If features are tightly coupled and constantly need to share state or UI components, splitting them into micro-frontends creates artificial boundaries that lead to brittle integration code.
You lack DevOps maturity
Micro-frontends require robust CI/CD pipelines, monitoring, and version management. If you're still deploying manually or don't have proper logging, fix your infrastructure first.
Performance is your top priority
Micro-frontends introduce runtime overhead: additional JavaScript for module loading, potential duplicate dependencies, and complexity in code splitting. A well-optimized monolith will almost always be faster.
You can't afford the maintenance cost
More repositories mean more dependencies to update, more security patches, more documentation to maintain. Make sure you have the resources to manage this complexity.
The Pragmatic Approach
Start with a monolith. Really. Even companies that successfully use micro-frontends today started with simpler architectures. Here's a practical path:
- Build a modular monolith - Organize your code into clear, decoupled modules with well-defined interfaces
- Wait for the pain - Only when you experience real coordination problems (merge conflicts, deployment blockages, team bottlenecks)
- Extract strategically - Begin with one or two clear, stable domains
- Validate the approach - If it solves your problems without creating worse ones, continue gradually
Remember: the best architecture is the simplest one that solves your current problems. Micro-frontends solve organizational problems, not technical ones.
Why Vite for Micro-Frontends?
Vite has emerged as a game-changer in the build tool landscape, and it's particularly well-suited for micro-frontend architectures:
Lightning-Fast Development
Vite's ESM-based dev server provides near-instantaneous hot module replacement (HMR), even as your micro-frontends grow. Unlike traditional bundlers that rebuild entire chunks, Vite only needs to invalidate the specific module that changed.
Module Federation Support
With plugins like @originjs/vite-plugin-federation, Vite supports Module Federation—the same technology that powers webpack's micro-frontend capabilities. This allows you to share code between applications at runtime without bundling everything together.
Optimized Production Builds
Vite uses Rollup under the hood for production builds, generating highly optimized bundles with intelligent code splitting.
Setting Up Micro-Frontends with Vite
Let's walk through a practical example. We'll create a host application and two remote micro-frontends.
Project Structure
Configuring a Remote Micro-Frontend
First, let's set up the remote-search application:
npm create vite@latest remote-search -- --template react-ts
cd remote-search
npm install @originjs/vite-plugin-federation --save-devConfigure vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
react(),
federation({
name: 'remote_search',
filename: 'remoteEntry.js',
exposes: {
'./Search': './src/components/Search.tsx',
},
shared: ['react', 'react-dom']
})
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false
},
server: {
port: 5001
}
})Create the Search component (src/components/Search.tsx):
import { useState } from 'react'
interface SearchFilters {
destination: string
checkIn: string
checkOut: string
guests: number
}
export default function Search() {
const [filters, setFilters] = useState<SearchFilters>({
destination: '',
checkIn: '',
checkOut: '',
guests: 1
})
const [isSearching, setIsSearching] = useState(false)
const handleSearch = () => {
setIsSearching(true)
// Simulate API call
setTimeout(() => {
console.log('Searching with filters:', filters)
setIsSearching(false)
}, 1000)
}
return (
<div className="search-container">
<h3>Search Hotels</h3>
<div className="search-form">
<input
type="text"
placeholder="Destination"
value={filters.destination}
onChange={(e) => setFilters({ ...filters, destination: e.target.value })}
/>
<input
type="date"
value={filters.checkIn}
onChange={(e) => setFilters({ ...filters, checkIn: e.target.value })}
/>
<input
type="date"
value={filters.checkOut}
onChange={(e) => setFilters({ ...filters, checkOut: e.target.value })}
/>
<input
type="number"
min="1"
value={filters.guests}
onChange={(e) => setFilters({ ...filters, guests: parseInt(e.target.value) })}
/>
<button onClick={handleSearch} disabled={isSearching || !filters.destination}>
{isSearching ? 'Searching...' : 'Search'}
</button>
</div>
</div>
)
}Configuring the Host Application
Now set up the host application that will consume the remotes:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
react(),
federation({
name: 'host',
remotes: {
remoteSearch: 'http://localhost:5001/assets/remoteEntry.js',
remoteBooking: 'http://localhost:5002/assets/remoteEntry.js',
},
shared: ['react', 'react-dom']
})
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false
}
})Load the remote components in your host (src/App.tsx):
import { lazy, Suspense } from 'react'
const RemoteSearch = lazy(() => import('remoteSearch/Search'))
const RemoteBooking = lazy(() => import('remoteBooking/Booking'))
function App() {
return (
<div className="app-container">
<h1>Hotel Booking Platform</h1>
<div className="features-grid">
<Suspense fallback={<div>Loading Search...</div>}>
<RemoteSearch />
</Suspense>
<Suspense fallback={<div>Loading Booking...</div>}>
<RemoteBooking />
</Suspense>
</div>
</div>
)
}
export default AppBenefits for Frontend Teams
Implementing micro-frontends brings several key advantages:
1. Team Autonomy
Each team can work independently on their micro-frontend without waiting for other teams. The search team can deploy updates without coordinating with the booking team.
2. Independent Deployments
Deploy updates to individual micro-frontends without rebuilding or redeploying the entire application. This means:
- Faster deployment cycles
- Reduced risk (smaller change sets)
- Ability to rollback individual features
- Continuous delivery for specific domains
3. Technology Flexibility
While maintaining consistency is important, micro-frontends allow you to:
- Upgrade dependencies gradually (update React in one MFE at a time)
- Experiment with new libraries in isolated contexts
- Migrate from legacy code incrementally
4. Clearer Ownership
Each micro-frontend maps to a specific business domain:
This alignment between team structure and architecture (Conway's Law in action) leads to better code organization and accountability.
5. Parallel Development
Multiple teams can work simultaneously without merge conflicts or stepping on each other's toes. Your CI/CD pipeline becomes simpler as each MFE has its own build and deployment process.
6. Improved Performance at Scale
Counter-intuitively, micro-frontends can improve performance:
- Load only what you need (lazy load MFEs per route)
- Parallel loading of independent bundles
- Better caching (update one MFE without invalidating others)
- Code splitting happens naturally at MFE boundaries
Shared Dependencies
Challenge: Multiple copies of React can bloat your bundle.
Solution: Configure shared dependencies carefully:
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0'
}
}Testing
Test at multiple levels:
- Unit tests: Within each micro-frontend
- Integration tests: Test communication between MFEs
- E2E tests: Test the composed application
When Should You Use Micro-Frontends?
Micro-frontends aren't always the answer. We've already discussed this in depth at the beginning of this post, but here's a quick reminder of the decision criteria:
Consider micro-frontends when:
- Multiple autonomous teams need to work independently
- Clear domain boundaries exist in your application
- Independent deployment cycles provide real business value
- You're managing a large, long-lived application
Stick with a monolith when:
- Small team or early-stage product
- Unclear domain boundaries or tightly coupled features
- Limited DevOps maturity
- Performance is the critical constraint
The key is honest assessment: are you solving real organizational problems, or just adding complexity?
Conclusion
Micro-frontends with Vite offer a powerful way to scale both your codebase and your team. The combination of Vite's speed and Module Federation's runtime code sharing creates a development experience that's both fast and flexible.
By splitting your frontend into independently deployable pieces, you enable teams to move faster, deploy with confidence, and maintain clear ownership of their domains. The key is to carefully consider whether the added complexity is worth the benefits for your specific situation.
Start small perhaps extract a single feature as a micro-frontend and see how it feels. The beauty of this architecture is that you can adopt it incrementally, learning as you go.
Resources: