Micro-frontends
Arquitetura
Frontend
Escalabilidade
React
Module Federation

Micro-frontends: When and Why to Adopt in Scalable Projects

Micro-frontends: When and Why to Adopt in Scalable Projects

In 2025, with increasingly complex web applications, micro-frontend architecture has become a mature solution for scaling both the development and maintenance of large applications. Let's explore when and how to implement this architecture efficiently.

Micro-frontends Fundamentals

1. Basic Concepts

// Exemplo de configuração de Module Federation // webpack.config.js const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'container', remotes: { marketing: 'marketing@http://localhost:8081/remoteEntry.js', dashboard: 'dashboard@http://localhost:8082/remoteEntry.js', auth: 'auth@http://localhost:8083/remoteEntry.js', }, shared: ['react', 'react-dom'], }), ], };

2. Typical Architecture

The architecture is organized around a container application that loads, on demand, the micro-frontends of each domain, for example, one for marketing, another for the dashboard and another for authentication. Each of these micro-frontends consumes a common layer of shared components, ensuring visual and behavioral consistency without coupling teams together.

When to Adopt Micro-frontends

1. Need Indicators

interface ProjectAssessment { teamSize: number; numberOfFeatures: number; deploymentFrequency: number; technicalDebt: { monolithComplexity: number; buildTime: number; testCoverage: number; }; teamAutonomy: { independent: boolean; technicalDecisions: boolean; deploymentControl: boolean; }; } function shouldAdoptMicroFrontends( assessment: ProjectAssessment ): RecommendationReport { const score = calculateAdoptionScore(assessment); return { recommendation: score > 0.7 ? 'Adopt' : 'Evaluate Further', reasons: analyzeFactors(assessment), risks: identifyRisks(assessment), nextSteps: generateRoadmap(assessment), }; }

2. Trade-offs Analysis

AppearanceBenefitsChallenges
ScalabilityIndependent teamsInitial complexity
PerformanceOn-Demand ChargingRuntime overhead
MaintenanceIsolated codeConsistency between apps
DeployIndependentCoordination required
DevelopmentAutonomyLearning curve

Implementation with Module Federation

1. Container Application

// src/App.tsx import { lazy, Suspense } from 'react'; import { Router, Route, Switch } from 'react-router-dom'; const MarketingApp = lazy(() => import('./remotes/MarketingApp')); const DashboardApp = lazy(() => import('./remotes/DashboardApp')); const AuthApp = lazy(() => import('./remotes/AuthApp')); export default function App() { return ( <Router> <Suspense fallback={<LoadingSpinner />}> <Switch> <Route path="/auth" component={AuthApp} /> <Route path="/dashboard" component={DashboardApp} /> <Route path="/" component={MarketingApp} /> </Switch> </Suspense> </Router> ); } // src/bootstrap.tsx import { createRoot } from 'react-dom/client'; import App from './App'; const mount = (el: HTMLElement) => { const root = createRoot(el); root.render(<App />); }; const devRoot = document.querySelector('#root'); if (devRoot) { mount(devRoot); } export { mount };

2. Remote Applications

// marketing/src/bootstrap.tsx import { createRoot } from 'react-dom/client'; import { createMemoryHistory, createBrowserHistory } from 'history'; import App from './App'; interface MountOptions { defaultHistory?: typeof createMemoryHistory; initialPath?: string; onNavigate?: (location: any) => void; } const mount = ( el: HTMLElement, { defaultHistory, initialPath, onNavigate, }: MountOptions = {} ) => { const history = defaultHistory?.({ initialEntries: [initialPath || '/'], }) || createBrowserHistory(); if (onNavigate) { history.listen(onNavigate); } const root = createRoot(el); root.render(<App history={history} />); return { onParentNavigate({ pathname: nextPathname }) { const { pathname } = history.location; if (pathname !== nextPathname) { history.push(nextPathname); } }, }; }; if (process.env.NODE_ENV === 'development') { const devRoot = document.querySelector('#_marketing-dev-root'); if (devRoot) { mount(devRoot, { defaultHistory: createBrowserHistory }); } } export { mount };

3. State Sharing

// shared/store/index.ts import create from 'zustand'; interface SharedState { user: User | null; theme: 'light' | 'dark'; setUser: (user: User | null) => void; setTheme: (theme: 'light' | 'dark') => void; } const useSharedStore = create<SharedState>((set) => ({ user: null, theme: 'light', setUser: (user) => set({ user }), setTheme: (theme) => set({ theme }), })); export { useSharedStore }; // shared/events/index.ts type EventCallback = (data: any) => void; class EventBus { private events: Map<string, Set<EventCallback>>; constructor() { this.events = new Map(); } subscribe(event: string, callback: EventCallback) { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event)!.add(callback); return () => { this.events.get(event)?.delete(callback); }; } publish(event: string, data: any) { this.events.get(event)?.forEach((callback) => { callback(data); }); } } export const eventBus = new EventBus();

Design Patterns

1. Application Composition

// container/src/components/MicroFrontend.tsx interface MicroFrontendProps { name: string; host: string; history?: History; } function MicroFrontend({ name, host, history }: MicroFrontendProps) { useEffect(() => { const scriptId = `micro-frontend-script-${name}`; const renderMicroFrontend = () => { // @ts-ignore window[`render${name}`]( `${name}-container`, history ); }; if (document.getElementById(scriptId)) { renderMicroFrontend(); return; } fetch(`${host}/asset-manifest.json`) .then((res) => res.json()) .then((manifest) => { const script = document.createElement('script'); script.id = scriptId; script.crossOrigin = ''; script.src = `${host}${manifest.files['main.js']}`; script.onload = () => { renderMicroFrontend(); }; document.head.appendChild(script); }); return () => { // @ts-ignore window[`unmount${name}`]?.(`${name}-container`); }; }, []); return <div id={`${name}-container`} />; }

2. Communication Between Apps

// shared/communication/index.ts type MessageHandler = (data: any) => void; class MessageBus { private handlers: Map<string, Set<MessageHandler>>; constructor() { this.handlers = new Map(); window.addEventListener('message', (event) => { const { type, data } = event.data; this.handlers.get(type)?.forEach((handler) => { handler(data); }); }); } subscribe(type: string, handler: MessageHandler) { if (!this.handlers.has(type)) { this.handlers.set(type, new Set()); } this.handlers.get(type)!.add(handler); return () => { this.handlers.get(type)?.delete(handler); }; } publish(type: string, data: any) { window.postMessage({ type, data }, '*'); } } export const messageBus = new MessageBus();

Best Practices

1. Performance

// shared/performance/index.ts interface PerformanceMetrics { ttfb: number; fcp: number; lcp: number; fid: number; cls: number; } class PerformanceMonitor { private metrics: Map<string, PerformanceMetrics>; constructor() { this.metrics = new Map(); this.setupObservers(); } private setupObservers() { new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { this.recordMetric(entry); } }).observe({ entryTypes: ['web-vital'] }); } private recordMetric(entry: PerformanceEntry) { const appName = this.getAppName(); if (!this.metrics.has(appName)) { this.metrics.set(appName, { ttfb: 0, fcp: 0, lcp: 0, fid: 0, cls: 0, }); } const metrics = this.metrics.get(appName)!; metrics[entry.name as keyof PerformanceMetrics] = entry.value; } }

2. Tests

// shared/testing/index.ts import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; interface TestConfig { route?: string; initialState?: any; mocks?: any[]; } function renderWithRouter( ui: React.ReactElement, { route = '/', initialState = {}, mocks = [], }: TestConfig = {} ) { const history = createMemoryHistory({ initialEntries: [route], }); return { ...render(ui), history, }; } export { renderWithRouter };

Challenges and Solutions

1. Visual Consistency

// shared/design-system/index.ts import { createTheme, ThemeProvider } from '@mui/material/styles'; const baseTheme = createTheme({ palette: { primary: { main: '#1976d2', }, secondary: { main: '#dc004e', }, }, typography: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', }, components: { MuiButton: { styleOverrides: { root: { textTransform: 'none', }, }, }, }, }); export { baseTheme, ThemeProvider };

2. Monitoring

// shared/monitoring/index.ts interface MonitoringConfig { appName: string; version: string; environment: string; } class ApplicationMonitor { constructor(private config: MonitoringConfig) { this.setupErrorBoundary(); this.setupPerformanceMonitoring(); this.setupUserMonitoring(); } private setupErrorBoundary() { window.addEventListener('error', (event) => { this.logError({ type: 'uncaught', error: event.error, location: event.filename, line: event.lineno, column: event.colno, }); }); } private logError(error: any) { console.error('[MFE Error]', { ...error, app: this.config.appName, version: this.config.version, environment: this.config.environment, timestamp: new Date().toISOString(), }); } }

Conclusion

Adopting micro-frontends offers significant benefits for scalable projects:

  1. Organizational Scalability: Autonomous and independent teams
  2. Technical Flexibility: Freedom to choose technologies
  3. Independent Deploy: Reduced risks and increased speed
  4. Maintainability: More organized and isolated code
  5. Gradual Evolution: Possibility of incremental migration

Next Steps

  1. Assess the real need for your project
  2. Start with a pilot in a non-critical area
  3. Establish standards and governance
  4. Invest in automation and tools
  5. Continuously monitor and adjust

Are you considering adopting micro-frontends in your project? Share your questions and experiences in the comments below!

Also read