MFE vs Microservices

Micro Frontend vs Microservices: Key Differences

Micro Frontend vs Microservices is one of the most commonly confused comparisons in modern software architecture. Both patterns split a monolith into smaller, independent pieces — but they operate on completely different layers of the stack. Microservices decompose the backend. Micro Frontends decompose the frontend. Understanding this distinction is critical before you decide which pattern to adopt — or whether you need both.

In this article, you'll learn what Microservices architecture is, how it differs from Micro Frontend, how they connect in a real production system with code examples, and when to use each approach.

What is Microservices Architecture?

Microservices is a backend architecture pattern where a large backend application is decomposed into small, independent services — each responsible for a single business domain, with its own database, deployment pipeline, and API.

Instead of one monolithic backend handling everything (users, products, cart, orders, payments, logistics), each domain becomes a standalone service:

MicroserviceResponsibilityPort
User ServiceAuthentication, profiles, sessions8000
Product ServiceProduct catalog, categories, search8010
Cart ServiceCart, checkout, coupons8020
Order ServiceOrder placement, tracking8030
Logistics ServiceShipping, delivery, pincode8040
Content ServiceCMS, banners, FAQs, media8050
Reviews ServiceRatings and reviews8060
Activity ServiceAnalytics, events, logs8070

Each service runs as a separate process, has its own database (or schema), and exposes a REST API or gRPC interface. Services communicate with each other over the network — never by sharing a database directly.

Microservices architecture diagram showing independent backend services each with their own database communicating through an API Gateway

For a deeper understanding of Microservices, read Martin Fowler's original article on Microservices (opens in a new tab).

What is Micro Frontend Architecture?

Micro Frontend is a frontend architecture pattern where a large frontend application is decomposed into small, independent applications — each responsible for a single UI domain, loaded at runtime by a Host (shell) application using Webpack Module Federation (opens in a new tab).

Instead of one monolithic frontend rendering all pages (products, cart, orders, account, support), each domain becomes a standalone frontend application:

Micro FrontendResponsibilityFramework
Host AppShell, routing, layoutNext.js
Products MFEProduct pages, searchNext.js
Content MFECMS pages, bannersNext.js
Cart MFEShopping bag, checkoutReact
Orders MFEOrder history, trackingReact
Account MFEProfile, addresses, wishlistReact
Auth MFELogin, signup, OTPReact
Support MFEHelp center, ticketsReact

Each MFE is a separate application with its own package.json, Webpack or Next.js config, build pipeline, and deployment. The Host app loads them at runtime via remoteEntry.js files.

New to Micro Frontend? Read What is Micro Frontend Architecture? for the complete introduction.

Side-by-Side Comparison

Here's the fundamental difference between Micro Frontend and Microservices:

FeatureMicro FrontendMicroservices
LayerFrontend (UI)Backend (API)
What it splitsUser interface into MFE appsBusiness logic into services
LanguageJavaScript, TypeScript (React, Next.js)Any — Node.js, Python, Java, Go
CommunicationModule Federation, shared store, eventsHTTP/REST, gRPC, message queues
LoadingRuntime (browser loads remoteEntry.js)Network (HTTP calls between services)
DeploymentStatic files (HTML/JS/CSS) or SSR containersBackend containers (Docker + Kubernetes)
DatabaseNone (frontend has no database)Each service owns its database
Shared stateRedux store via Module Federation singletonNone — services are stateless
DiscoveryModule Federation remote URLsService registry, DNS, Kubernetes Service
ScalingScale MFE static assets via CDNScale service containers via Kubernetes
Dev serverEach MFE runs on its own localhost portEach service runs on its own port
Build toolWebpack, TurborepoDocker, CI/CD pipelines
GatewayNginx routes to MFE static filesAPI Gateway routes to service endpoints
Team ownershipFrontend team owns a UI domainBackend team owns a business domain

Key insight: Micro Frontend and Microservices are not competing patterns — they are complementary. One decomposes the frontend, the other decomposes the backend. Many production systems use both together.

How They Work Together

In a fully decomposed system, the architecture looks like this:

  1. User visits the website → Nginx serves the Host app
  2. Host loads MFE → Module Federation loads the Products MFE at runtime
  3. MFE makes API call → Products MFE calls /product-service/api/v1/products
  4. API Gateway routes → Nginx forwards the request to the Product microservice
  5. Microservice responds → Product Service queries its database and returns JSON
  6. MFE renders → Products MFE displays the product data in React components

Micro Frontend and Microservices working together showing MFE layer calling API Gateway which routes to corresponding backend microservices

Each Micro Frontend maps to one or more backend Microservices:

Micro FrontendCalls These Microservices
Products MFEProduct Service, Reviews Service
Cart MFECart Service, Logistics Service
Orders MFEOrder Service, Logistics Service
Account MFEUser Service
Auth MFEUser Service
Content MFEContent Service
Support MFESupport Service

The API Config Pattern

In production MFE applications, a shared API config maps each business domain to its backend microservice. Every MFE uses this config to route API calls to the correct service.

packages/api/api.config.js
// packages/api/api.config.js
// Maps each business domain to its backend microservice
export const API_CONFIG = {
  services: {
    user:     'user-service',      // Auth, profiles, sessions
    master:   'product-service',   // Products, categories, search
    cart:     'cart-service',       // Cart, checkout, coupons
    order:    'order-service',     // Order placement, tracking
    logistic: 'logistics-service', // Shipping, pincode check
    support:  'support-service',   // Tickets, chat, FAQs
    reviews:  'reviews-service',   // Ratings and reviews
    activity: 'activity-service',  // Analytics, events, logs
  },
}

Shared API Layer (All MFEs → All Microservices)

All Micro Frontends share a single axios (opens in a new tab) instance via Module Federation singleton. This shared API layer handles JWT tokens, automatic token refresh on 401 errors, and error handling — so no MFE manages auth logic individually.

The pattern is the same across different MFE portals, but the refresh endpoint differs based on the user type (customer, seller, admin):

packages/api/index.js — Customer Portal
// packages/api/index.js
// Shared axios instance — used by ALL Micro Frontends
import axios from 'axios'
import { API_CONFIG } from './api.config.js'
import { store, selectAccessToken, setAt, clearUser } from '@myapp/store'

const api = axios.create({
  baseURL: 'https://api.example.com',
  headers: { 'Content-Type': 'application/json' },
  withCredentials: true,
})

// Auto-attach JWT token from shared Redux store
api.interceptors.request.use((config) => {
  const state = store.getState()
  const token = selectAccessToken(state)
  if (token) {
    config.headers['Authorization'] = 'Bearer ' + token
  }
  config.headers['x-device-info'] = navigator.userAgent
  if (config.data instanceof FormData) {
    config.headers['Content-Type'] = undefined
  }
  return config
})

// Handle 401 — automatic token refresh
let isRefreshing = false

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config
    if (error.response?.status === 401) {
      const errorCode = error.response?.data?.code

      // Immediate logout for these error codes
      const logoutCodes = [
        'INVALID_SESSION', 'USER_INACTIVE', 'NOT_AUTHENTICATED',
        'INVALID_TOKEN', 'NO_TOKEN', 'SESSION_EXPIRED',
      ]
      if (logoutCodes.includes(errorCode)) {
        store.dispatch(clearUser())
        return Promise.reject(error)
      }

      // Token expired — refresh and retry
      if (errorCode === 'TOKEN_EXPIRED' && !originalRequest._retry) {
        originalRequest._retry = true
        isRefreshing = true
        try {
          const res = await api.post(
            '/' + API_CONFIG.services.user + '/api/v1/customer/auth/refresh-token'
          )
          if (res.data.success && res.data.accessToken) {
            store.dispatch(setAt({ at: res.data.accessToken }))
            originalRequest.headers['Authorization'] =
              'Bearer ' + res.data.accessToken
            return api(originalRequest)
          }
        } catch (refreshError) {
          store.dispatch(clearUser())
          return Promise.reject(refreshError)
        } finally {
          isRefreshing = false
        }
      }
      store.dispatch(clearUser())
    }
    return Promise.reject(error)
  }
)

export default api

Why a shared API package? Without it, every MFE would implement its own token management, refresh logic, and error handling — leading to duplicated code and inconsistent behavior. The shared @myapp/api package solves this once for all MFEs. Read more in MFE Communication Patterns.

Each MFE Calls Its Microservice

With the shared API layer and API config in place, each MFE makes API calls to its corresponding microservice through clean, domain-specific functions. Notice how a single MFE can call multiple microservices — Cart MFE calls both the Cart Service and the Logistics Service:

apps/products/src/api/product.js
// Products MFE — calls the Product microservice
import api from '@myapp/api'
import { API_CONFIG } from '@myapp/api/api.config'

const BASE = '/' + API_CONFIG.services.master + '/api/v1'

export const getProducts = (params) =>
  api.get(BASE + '/products', { params })

export const getProductById = (numericId) =>
  api.get(BASE + '/products/mongo/numeric/' + numericId)

export const getNavbarMapping = (includeInactive = false) => {
  const params = new URLSearchParams()
  if (includeInactive) params.append('include_inactive', 'true')
  return api.get(BASE + '/navbar-mapping?' + params.toString())
}

How MFE Configs Connect to Microservices

The Module Federation config determines how MFEs load each other at runtime — but it also controls how the shared API package reaches microservices. The @myapp/api package is declared as a singleton in Module Federation, meaning all MFEs use the exact same axios instance with the same interceptors and token state.

Local Development and Production configs are fundamentally different. Locally, each MFE runs on its own localhost port with HTTPS certificates and CORS headers. In production, Nginx routes each path to the correct MFE's static files. The shared API package connects to microservices the same way in both — but the MFE remote URLs change completely.

React MFE Host (Webpack — ModuleFederationPlugin)

apps/host/webpack.config.js — Local
// apps/host/webpack.config.js — React Host (LOCAL)
const { ModuleFederationPlugin } = require('webpack').container
const path = require('path')
const fs = require('fs')

// HTTPS certificates for local development
const certPath = path.join(__dirname, '../../localhost.pem')
const keyPath = path.join(__dirname, '../../localhost-key.pem')

const httpsConfig = fs.existsSync(certPath) && fs.existsSync(keyPath)
  ? { type: 'https', options: {
      cert: fs.readFileSync(certPath),
      key: fs.readFileSync(keyPath),
    }}
  : { type: 'https', options: { requestCert: false, rejectUnauthorized: false } }

module.exports = {
  mode: 'development',
  output: { publicPath: '/' },
  devServer: {
    server: httpsConfig,
    port: 4000,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Host',
      // Each remote MFE runs on its own localhost port
      remotes: {
        Products:   'Products@https://localhost:4001/remoteEntry.js',
        Orders:     'Orders@https://localhost:4002/remoteEntry.js',
        Cart:       'Cart@https://localhost:4003/remoteEntry.js',
        Pricing:    'Pricing@https://localhost:4004/remoteEntry.js',
        Analytics:  'Analytics@https://localhost:4005/remoteEntry.js',
        Settings:   'Settings@https://localhost:4006/remoteEntry.js',
        Support:    'Support@https://localhost:4007/remoteEntry.js',
      },
      shared: {
        react:              { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom':        { singleton: true, requiredVersion: '^18.2.0' },
        'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' },
        '@reduxjs/toolkit':  { singleton: true, requiredVersion: '^2.6.0' },
        // Shared API package — ALL MFEs use the same axios instance
        '@myapp/api':       { singleton: true, requiredVersion: '0.0.1' },
        '@myapp/store':     { singleton: true, requiredVersion: '0.0.1' },
      },
    }),
  ],
  optimization: {
    splitChunks: false,  // Disabled in local dev
  },
}

Next.js MFE Host (NextFederationPlugin)

The Next.js Host is completely different from the React Host. It uses NextFederationPlugin (not ModuleFederationPlugin), has basePath/assetPrefix for routing, checks isServer for SSR vs client-side remoteEntry paths, and includes Content Security Policy headers that whitelist the microservice API domain.

apps/host/next.config.js — Next.js Host
// apps/host/next.config.js — Next.js Host (PRODUCTION)
// Uses NextFederationPlugin — NOT ModuleFederationPlugin
const { NextFederationPlugin } = require('@module-federation/nextjs-mf')

module.exports = {
  reactStrictMode: false,
  output: 'standalone',

  // Content Security Policy — whitelists microservice API domains
  async headers() {
    return [{
      source: '/:path*',
      headers: [
        {
          key: 'Content-Security-Policy',
          value: [
            "default-src 'self' https://api.example.com",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
            "connect-src 'self' https://api.example.com",
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
          ].join('; '),
        },
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
        { key: 'X-XSS-Protection', value: '1; mode=block' },
      ],
    }]
  },

  // Next.js image optimization for product images
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com', pathname: '/**' },
      { protocol: 'https', hostname: 'assets.example.com', pathname: '/**' },
    ],
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 60,
  },

  webpack(config, { isServer }) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'Host',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          // Next.js remotes — different remoteEntry path for SSR
          Content:  'Content@https://www.example.com/content/_next/static/'
            + (isServer ? 'ssr' : 'chunks') + '/remoteEntry.js',
          Products: 'Products@https://www.example.com/products/_next/static/'
            + (isServer ? 'ssr' : 'chunks') + '/remoteEntry.js',
          // React remotes — standard remoteEntry at root
          Auth:     'Auth@https://www.example.com/auth/remoteEntry.js',
          Cart:     'Cart@https://www.example.com/cart/remoteEntry.js',
          Account:  'Account@https://www.example.com/account/remoteEntry.js',
          Support:  'Support@https://www.example.com/support/remoteEntry.js',
        },
        shared: {
          // Next.js uses requiredVersion: false + eager: false
          react:              { singleton: true, requiredVersion: false, eager: false },
          'react-dom':        { singleton: true, requiredVersion: false, eager: false },
          'react-redux':      { singleton: true, requiredVersion: false, eager: false },
          '@reduxjs/toolkit':  { singleton: true, requiredVersion: false, eager: false },
          // Shared packages use strictVersion for consistency
          '@myapp/store':     { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          '@myapp/api':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          '@myapp/seo':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
        },
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
          automaticAsyncBoundary: true,
        },
      })
    )
    if (!isServer) {
      config.resolve.fallback = { fs: false, stream: false, zlib: false }
    }
    return config
  },
}

Key Differences Between React and Next.js MFE Configs

AspectReact MFE (Webpack)Next.js MFE
PluginModuleFederationPluginNextFederationPlugin
Config filewebpack.config.jsnext.config.js
remoteEntry path/remoteEntry.js (root)static/chunks/remoteEntry.js
SSR supportNoYes (isServer check)
Remote URLs (local)https://localhost:PORT/remoteEntry.jsN/A (uses Next.js dev server)
Remote URLs (prod)/path/remoteEntry.jshttps://domain/path/_next/static/chunks/remoteEntry.js
RoutingpublicPath: '/products/'basePath: '/products' + assetPrefix
CSP headersNoneFull CSP whitelisting microservice domains
Image optimizationNoneimages.remotePatterns for CDN
Shared depsrequiredVersion: '^18.2.0' (pinned)requiredVersion: false, eager: false (flexible)
splitChunksManual (disabled local, enabled prod)Handled by Next.js
HTTPS (local)Manual cert setup (localhost.pem)Built into Next.js dev server
Extra optionsNoneexposePages, enableImageLoaderFix, automaticAsyncBoundary

For a deeper dive into these configs, read MFE Folder Structure which covers the complete monorepo layout.

What Does a Microservice Look Like?

Each backend Microservice is a standalone Express.js (opens in a new tab) application. It handles only its own domain routes, manages its own database, and exposes a health check endpoint for Kubernetes (opens in a new tab) liveness probes. The CORS whitelist is critical — it must allow requests from all MFE origins (both local dev ports and production domains).

services/product-service/app.js
// services/product-service/app.js — Express.js Microservice
const express = require('express')
const cors = require('cors')
const mongoManager = require('./config/connection-mongodb')

const app = express()

// CORS whitelist — only allow requests from MFE origins
const allowedOrigins = [
  'https://localhost:3000',         // Next.js Host (local dev)
  'https://localhost:4000',         // React Seller Host (local dev)
  'https://www.example.com',        // Customer frontend (production)
  'https://seller.example.com',     // Seller portal (production)
  'https://admin.example.com',      // CMS admin (production)
]

app.use(cors({
  origin: (origin, callback) => {
    if (!origin) return callback(null, true)
    if (allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('Origin ' + origin + ' Not allowed by CORS'))
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  allowedHeaders: [
    'Content-Type', 'Authorization', 'X-Requested-With',
    'x-device-info', 'x-guest-id', 'x-platform', 'x-service-key',
  ],
}))

app.use(express.json({ limit: '10mb' }))

// Routes — this service ONLY handles product-related endpoints
app.use('/api/v1/products', productRoutes)
app.use('/api/v1/categories', categoryRoutes)
app.use('/api/v1/search', searchRoutes)
app.use('/api/v1/sellers/dashboard', sellerDashboardRoutes)

// Health check — used by Kubernetes liveness/readiness probes
app.get('/health', async (req, res) => {
  const mongoStatus = mongoManager.getStatus()
  res.status(mongoStatus.healthy ? 200 : 503).json({
    status: mongoStatus.healthy ? 'healthy' : 'degraded',
    timestamp: new Date().toISOString(),
    services: { mongodb: mongoStatus },
  })
})

const PORT = process.env.PORT || 8010
app.listen(PORT, () => {
  console.log('Product Service running on port ' + PORT)
})

Key characteristics every microservice shares:

  • Single responsibility — each service handles only its own domain endpoints
  • CORS whitelist — allows requests from MFE origins (localhost ports for dev, production domains for prod)
  • Health check/health endpoint for Kubernetes readiness/liveness probes, checking database connectivity
  • Own port — each service runs independently (8000, 8010, 8020, 8030…)
  • Own database — connects to its own MongoDB instance (not shared with other services)
  • Custom headers — accepts x-device-info, x-guest-id, x-service-key from MFE requests

Dockerized Microservice

Each microservice runs in its own Docker (opens in a new tab) container with a multi-stage build for minimal image size, non-root user for security, and a health check for Kubernetes.

services/product-service/Dockerfile
# Dockerfile — Backend Microservice (Node.js)
FROM node:18-alpine AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci

FROM node:18-alpine
RUN apk add --no-cache dumb-init
WORKDIR /usr/src/app

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY --chown=nodejs:nodejs . .
RUN mkdir -p config logs && chown -R nodejs:nodejs config logs

USER nodejs
EXPOSE 8010

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:8010/health', (r) => { r.statusCode === 200 ? process.exit(0) : process.exit(1) })"

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "app.js"]

Running MFEs + Microservices Together

In development, you need to run both the MFE applications and the backend microservices. A docker-compose.yml file orchestrates everything — MFE dev servers, microservice containers, API Gateway (Nginx), and the database.

docker-compose.yml
# docker-compose.yml — Running MFEs + Microservices locally
version: '3.8'
services:
  # ── Frontend: Micro Frontends ──
  host-app:
    build: ./apps/host
    ports:
      - "3000:3000"
    depends_on:
      - api-gateway

  products-mfe:
    build: ./apps/products
    ports:
      - "3001:3001"

  cart-mfe:
    build: ./apps/cart
    ports:
      - "3002:3002"

  # ── Backend: Microservices ──
  user-service:
    build: ./services/user-service
    ports:
      - "8000:8000"
    environment:
      - DB_HOST=mongodb
      - JWT_SECRET=${JWT_SECRET}

  product-service:
    build: ./services/product-service
    ports:
      - "8010:8010"
    environment:
      - DB_HOST=mongodb

  cart-service:
    build: ./services/cart-service
    ports:
      - "8020:8020"
    environment:
      - DB_HOST=mongodb

  content-service:
    build: ./services/content-service
    ports:
      - "8050:8050"
    environment:
      - DB_HOST=mongodb

  # ── Infrastructure ──
  api-gateway:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - user-service
      - product-service
      - cart-service
      - content-service

  mongodb:
    image: mongo:7
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

In production, the same components run as separate Kubernetes (opens in a new tab) Deployments with independent scaling, health checks, and rolling updates.

Anti-Pattern: Direct API Calls vs Shared API Package

One of the most common mistakes when combining MFE with Microservices is having each MFE create its own API instance. This leads to duplicated auth logic, token refresh race conditions, and inconsistent error handling.

products-mfe/src/api.js — WRONG
// WRONG — Each MFE creates its own axios instance
// products-mfe/src/api.js
import axios from 'axios'

const api = axios.create({ baseURL: 'https://api.example.com' })

// Every MFE duplicates token logic
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token')
  if (token) config.headers['Authorization'] = 'Bearer ' + token
  return config
})

// Every MFE duplicates refresh logic — 401 handling repeated everywhere
api.interceptors.response.use(
  (res) => res,
  async (error) => {
    if (error.response?.status === 401) {
      const res = await axios.post('/user-service/api/v1/auth/refresh-token')
      localStorage.setItem('token', res.data.accessToken)
      // Retry...
    }
    return Promise.reject(error)
  }
)

export default api
// Problem: If token refreshes in Products MFE, Cart MFE still has the OLD token
// Problem: 6 MFEs = 6 copies of auth logic = 6 places for bugs

Can You Use Micro Frontends Without Microservices?

Yes — absolutely. Micro Frontend architecture is completely independent of your backend architecture. You can:

  • Split your frontend into MFEs while keeping a monolithic backend
  • Have all MFEs call a single REST API
  • Use MFEs with a serverless backend (AWS Lambda, Cloudflare Workers)
  • Use MFEs with a BaaS (Firebase, Supabase)

The frontend decomposition does not require backend decomposition. Many teams adopt MFE first because their frontend scaling problems (merge conflicts, slow builds, deployment bottlenecks) appear before backend scaling problems.

Can You Use Microservices Without Micro Frontends?

Yes. This is actually the more common scenario. Many companies run Microservices on the backend while their frontend remains a monolith. The frontend is a single React or Next.js application that calls different microservice endpoints through an API Gateway.

This works fine when:

  • The frontend team is small (1–5 developers)
  • The frontend codebase is manageable
  • There's no need for independent frontend deployments

You only need Micro Frontends when the frontend itself becomes a bottleneck — too many developers, too many features, too-slow builds, or too many merge conflicts.

Common Misconceptions

"Micro Frontend is just Microservices for the frontend"

Partially true, but misleading. The philosophy is similar — decompose a monolith into independent, deployable units. But the implementation is fundamentally different:

  • Microservices communicate over HTTP/gRPC (network calls between servers)
  • Micro Frontends communicate via shared Redux store, callback props, and events (in-browser JavaScript)
  • Microservices each have their own database
  • Micro Frontends have no database — they render UI and call APIs
  • Microservices are backend containers (Docker + Kubernetes)
  • Micro Frontends are static files (HTML, JS, CSS served via CDN or Nginx) or SSR containers

"You need Microservices before Micro Frontends"

False. There's no dependency between them. Adopt whichever pattern solves your current bottleneck:

BottleneckSolution
Backend teams blocked by monolithic APIAdopt Microservices first
Frontend teams blocked by monolithic UIAdopt Micro Frontends first
Both teams blockedAdopt both incrementally

"Each Micro Frontend must have its own backend"

False. An MFE is a frontend application — it calls backend APIs but doesn't run a server. Multiple MFEs can call the same microservice, and a single MFE can call multiple microservices. The mapping is flexible.

When to Use Each Pattern

ScenarioRecommendation
Small team, simple appNeither — use a monolith (both frontend and backend)
Large frontend team, small backendMicro Frontend only
Small frontend team, large backendMicroservices only
Large teams on both sidesBoth — Micro Frontend + Microservices
High traffic with domain-specific load patternsBoth — scale frontend and backend independently
MVP or early-stage startupNeither — start with monolith, migrate when needed
⚠️

Do NOT adopt both patterns simultaneously from day one. Start with a monolith. Identify your bottleneck. Adopt the pattern that solves it. Then add the other when the need arises. Over-engineering early is a common and expensive mistake.

Micro Frontend vs Microservices decision flowchart showing when to adopt MFE only, Microservices only, both, or neither based on team size and bottleneck location

Real-World Adoption Patterns

CompanyFrontendBackendNotes
AmazonMicro FrontendMicroservicesEach product team owns both frontend and backend
SpotifyMicro FrontendMicroservicesIndependent squads per feature area
NetflixMonolith (React)MicroservicesBackend decomposed first, frontend stayed unified
IKEAMicro FrontendMicroservicesIndependent teams for catalog, cart, checkout
Most startupsMonolithMonolithToo small for either decomposition pattern

Notice the pattern — Microservices is typically adopted before Micro Frontends. Backend scaling problems (database bottlenecks, deployment frequency, team conflicts) tend to appear earlier than frontend scaling problems. Micro Frontends are adopted later, as the frontend team and application complexity grow.

For more on when to adopt Micro Frontend specifically, read Micro Frontend vs Monolith: How to Choose.

Summary

AspectMicro FrontendMicroservices
DecomposesFrontend UIBackend API
CommunicationModule Federation, Redux, eventsHTTP, gRPC, message queues
DeploymentStatic files or SSR containersBackend containers
DatabaseNoneEach service owns its database
ScalingCDN + NginxKubernetes pods
TeamFrontend engineersBackend engineers
Works without the other?YesYes
Best together?Yes — full-stack decompositionYes — full-stack decomposition

Micro Frontend and Microservices are not competitors — they are partners. One handles the frontend, the other handles the backend. Together, they enable full-stack independent deployment, team autonomy, and domain-driven scaling.

What's Next?

Now that you understand the difference between Micro Frontend and Microservices, the next article compares Micro Frontend vs Single Page Application (SPA) — helping you decide whether your frontend needs decomposition or if a traditional SPA is the better fit.

← Back to Micro Frontend Folder Structure

Continue to Micro Frontend vs SPA →


Frequently Asked Questions

Are Micro Frontends and Microservices the same thing?

No. Microservices decompose the backend into independent services that communicate over HTTP, gRPC, or message queues. Micro Frontends decompose the frontend into independent applications loaded at runtime via Module Federation. They operate on different layers of the stack but follow the same principle — split a monolith into independently deployable units.

Can you use Micro Frontends without Microservices?

Yes. Micro Frontend architecture is completely independent of your backend architecture. You can split your frontend into MFEs while keeping a monolithic backend, or have all MFEs call a single REST API. The frontend decomposition does not require backend decomposition.

How do Micro Frontends and Microservices work together?

In a full-stack decomposition, each Micro Frontend calls its corresponding Microservice through an API Gateway. For example, the Products MFE calls the Product Service, the Cart MFE calls the Cart Service, and the Orders MFE calls the Order Service. A shared API layer with centralized auth interceptors handles token management across all MFEs.

Is Microservices frontend or backend?

Microservices is a backend architecture pattern. Each microservice is a standalone backend application (Node.js, Python, Java, Go) that handles a specific business domain — user authentication, product catalog, order processing, etc. Micro Frontend is the frontend equivalent, splitting the UI into independent applications.

Which should I adopt first — Micro Frontend or Microservices?

It depends on where your bottleneck is. If backend teams are blocked by a monolithic API, start with Microservices. If frontend teams are blocked by a monolithic UI, start with Micro Frontends. Many companies adopt Microservices first because backend scaling hits the wall earlier, then add Micro Frontends as the frontend team grows.

What are the 3 C's of Microservices?

The 3 C's of Microservices are: Componentization (each service is an independent, replaceable component), Communication (services communicate over lightweight protocols like HTTP/REST or gRPC), and Coordination (services coordinate via API Gateways, service discovery, and orchestration tools like Kubernetes). These same principles apply to Micro Frontends at the UI layer.