Scaling Without Chaos: Building Enterprise Microfrontends

E-commerce Application 

πŸ‘₯

Team of 6

Auth

Payment

Catalog

Checkout

πŸ‘₯

πŸ‘₯

E-commerce Application 

πŸ‘₯

Auth

Payment

Catalog

Checkout

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

E-commerce Application 

πŸ‘₯

Auth

Payment

Catalog

Checkout

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

πŸ‘₯

Host / Shell

Navbar

Footer

Remote

Remote

Remote

Remote

<router-outlet />
module.exports = withNativeFederation({
  name: 'cart',

  exposes: {
    './routes': './cart/app.routes.ts',
  },
  ...
}

Pankaj P. Parkar

Senior Software Engineer

  • Angular GDE (since 2019)

  • Ex- Microsoft MVP (2015-22)

  • @ngx-lib/multiselect πŸ“¦

  • #Devwhorun πŸƒ

GitHub @pankajparkar    Twitter Follow  

About Me!

@pankajparkar

@pankajparkar

  • Loads JS bundles from other apps at runtime
  • Shares dependencies smartly β€” no duplicates
  • Stitches independent apps into one experience
  • Webpack only

Module Federation

Native Federation

  • Same concepts as Module Federation β€” 80% identical API
  • Uses ES Import Maps β€” browser-native, no bundler lock-in
  • Works with esbuild, Vite, Webpack  πŸš€
  • remoteEntry.json instead of .js β€” a plain manifest, not runtime magic
ng add @angular-architects/native-federation
npx nx g @nx/angular:application catalog --port=4201

npx nx g @nx/angular:application cart --port=4202

npx nx g @nx/angular:application checkout --port=4203

How to setup applications?

npx create-nx-workspace@latest angular-mfe
  --preset=angular-monorepo
  --appName=shell

Create shell app

install native

federation

Create remote apps

npx nx g @angular-architects/native-federation:init
  --project=catalog --port=4201 --type=remote

npx nx g @angular-architects/native-federation:init
  --project=cart --port=4202 --type=remote

npx nx g @angular-architects/native-federation:init
  --project=checkout --port=4203 --type=remote

Initialize Federation for Each App

{
  path: 'cart',
  loadChildren: () =>
    loadRemoteModule('cart', './CartModule')
      .then(m => m.CartModule)
}
exposes: {
  './routes': './apps/cart/src/app/app.routes.ts',
  './CartBadge': 
    './apps/cart/src/app/cart-badge.component.ts',
} ,
shared: {
  ...shareAll({ 
    singleton: true, 
    strictVersion: true,
    requiredVersion: 'auto', 
  }),
},

apps/cart/federation.config.ts

apps/cart/app/routes.ts

@pankajparkar

Four Qs, answer them early

 

  • Who owns the routes?
  • Where do you draw the boundary?
  • What do you actually share?
  • Can your remote survive without the shell?

Who owns the routes?

Shell does

/catalog  β†’  loadRemoteModule β†’ CatalogModule
/cart     β†’  loadRemoteModule β†’ CartModule  
/checkout β†’  loadRemoteModule β†’ CheckoutModule

Where do you draw the boundary?

Bad                        Good
/components remote    β†’    /checkout remote  (payments team)
/utils remote         β†’    /catalog remote   (catalog team)
/services remote      β†’    /cart remote      (cart team)

By domain, never by technical layer

What do you actually share?

module.exports = {
  // ... rest of config
  shared: share({
    // 1. Angular Core: Must be a singleton and very strict
    "@angular/core": { 
      singleton: true, 
      strictVersion: true, 
      requiredVersion: 'auto' 
    },
    "@angular/common": { 
      singleton: true, 
      strictVersion: true, 
      requiredVersion: 'auto' 
    },
    // 2. Utility libs: Maybe you DON'T want a singleton (each app gets its own)
    "lodash": { 
      singleton: false,
      requiredVersion: '^4.17.0',
      strictVersion: false 
    },
    "ui-library": {
      singleton: true,
      strictVersion: false
    }
  })
};

The minimum

Can your remote survive without the shell?

It must

Remote should work:

  • Loaded by shell (federation mode)
  • Standalone on its own port (dev mode)
  • Deployed independently (production)

Real questions, Straight answers

How to

  • share a state?
  • use specific component from remote/lib?
  • upgrades packages?
  • make sure update doesn't break things?
  • feature/control not to ship change to other project?
  • manage CSS leaking?

share state b/w remotes / host?

Service with Angular Signal

@Injectable({ providedIn: 'root' })
export class CartStateService {
  private readonly _cartItems = signal<CartItem[]>([]);

  readonly cartItems = this._cartItems.asReadonly();

  readonly cartCount = computed(() =>
    this._cartItems()
     .reduce((total, item) => total + item.quantity, 0)
  );

  readonly cartTotal = computed(() => ...);

  addToCart(product: Product) { ... }

  removeFromCart(productId: number) { ... }

  clearCart() {...}
}
module.exports = {
  shared: share({
    ...
    "ui-library": {
      singleton: true,
      strictVersion: false
    },
  })
};

use specific component from remote/lib?

module.exports = withNativeFederation({
  name: 'cart',
  exposes: {
    './routes': './apps/cart/src/app/app.routes.ts',
    './CartBadge': './apps/cart/src/app/cart-badge.component.ts',
  },
  ...
}
loadRemoteModule('cart', './CartBadge')
  .then(m => this.cartBadge = m.CartBadgeComponent)
  .catch(() => console.warn('Cart badge unavailable'));
<ng-container *ngComponentOutlet="cartBadge" />

The remaining, real questions

  • upgrades packages?
  • make sure update doesn't break things?
  • feature/control not to ship change to other project?
  • manage CSS leaking?

The real cost

  • N apps Γ— N pipelines
  • Cross-bundle debugging is hard
  • initial load if shell fetches many remoteEntry files
  • Overkill for < 3 frontend teams
  • New Dev Onboarding cost

Takeaways

  • Angular version alignment
  • CORS on remoteEntry.json
  • build order in CI (remotes first)
  • type your exposed modules
  • run shell + remotes with concurrently.

@pankajparkar

Scaling Without Chaos: Building Enterprise Microfrontends

By Pankaj Parkar

Scaling Without Chaos: Building Enterprise Microfrontends

  • 10