Retour au Blog
Angular22 août 202611 min de lecture

Micro-frontends avec Angular : est-ce que ça vaut le coup dans le monde réel ?

DR

Doug Ribeiro

11 min de lecture

Les micro-frontends font partie de ces patterns architecturaux qui sonnent convaincants dans les conférences et qui sont différents une fois qu'on vit avec en production. J'ai travaillé au sein d'une architecture micro-frontend sur un projet Angular d'entreprise chez BMO, et j'ai maintenant un avis réfléchi. Ce n'est pas une réponse simple, mais c'est une réponse honnête.

La version courte : les micro-frontends résolvent de vrais problèmes pour des équipes d'une taille et structure organisationnelle spécifiques. Pour des équipes en dessous de ce seuil, ils ajoutent de la complexité sans bénéfice. Le défi est que le seuil est plus haut que ce que la plupart des discussions architecturales suggèrent, et les coûts sont plus concrets que les bénéfices.

Quel problème les micro-frontends résolvent réellement

L'argument honnête pour les micro-frontends n'est pas technologique. C'est une question de topologie d'équipe. Quand plusieurs équipes doivent développer, déployer et posséder différentes parties d'une grande application frontend de manière indépendante, une application Angular monolithique crée des coûts de coordination. L'équipe A ne peut pas déployer ses changements sans se coordonner avec l'équipe B. Un bogue dans le code de l'équipe C peut bloquer une release pour toutes les autres équipes. La propriété est floue parce que tout le monde possède le même codebase.

Les micro-frontends abordent ça en traçant des frontières dures au niveau du déploiement et de la propriété. L'équipe A possède MFE-A. Elle peut le déployer indépendamment. Ses bogues n'affectent pas le déploiement de l'équipe B. Ses choix technologiques (dans certaines limites) ne contraignent pas l'équipe B. Le problème de coordination du monolithe disparaît parce qu'il n'y a pas de monolithe partagé.

Si vous n'avez pas ce problème, si vous avez une équipe, ou deux équipes étroitement coordonnées, ou un codebase qui se déploie rarement, les micro-frontends ne résolvent rien que vous ayez réellement. Ils introduisent des coûts sans le bénéfice correspondant.

Configuration de Module Federation dans Angular

Le chemin le plus courant vers les micro-frontends en Angular est Module Federation via le package @angular-architects/module-federation, maintenu par la communauté ; Angular lui-même ne livre aucune solution officielle de micro-frontends. Ce plugin webpack permet à une application hôte de charger des modules JavaScript distants au runtime depuis des URLs déployées séparément. Chaque module distant est une application Angular déployée indépendamment qui expose des composants, des routes ou des services.

La mise en place implique de modifier la configuration webpack pour l'hôte et chaque remote. Module Federation est une fonctionnalité de webpack, elle exige donc le builder Angular basé sur webpack plutôt que le builder par défaut basé sur esbuild arrivé avec Angular 17 (Native Federation, des mêmes mainteneurs, couvre la voie esbuild). Le plugin génère les configs webpack pour vous. En pratique, les configs générées nécessitent une personnalisation pour tout cas non trivial.

javascript
// webpack.config.js for a remote MFE
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { share } = require('@angular-architects/module-federation/webpack');

module.exports = {
  output: {
    uniqueName: 'workspaceApp',
    publicPath: 'auto'
  },
  optimization: { runtimeChunk: false },
  experiments: { outputModule: true },
  plugins: [
    new ModuleFederationPlugin({
      library: { type: 'module' },
      name: 'workspaceApp',
      filename: 'remoteEntry.js',
      exposes: {
        './WorkspaceRoutes': './src/app/workspace/workspace.routes.ts'
      },
      shared: share({
        '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        '@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        'rxjs': { singleton: true, strictVersion: false, requiredVersion: 'auto' }
      })
    })
  ]
};
typescript
// app.routes.ts in the host
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';
import { environment } from '../environments/environment';

export const routes: Routes = [
  {
    path: 'workspace',
    loadChildren: () =>
      loadRemoteModule({
        type: 'module',
        remoteEntry: environment.workspaceRemoteUrl + '/remoteEntry.js',
        exposedModule: './WorkspaceRoutes'
      }).then(m => m.WORKSPACE_ROUTES)
  }
];

Une chose à dire franchement, parce que cet article date d'août 2026 : la voie du builder webpack ci-dessus est l'ancienne. Le builder par défaut d'Angular est basé sur esbuild depuis la v17, et l'option maintenue pour lui est @angular-architects/native-federation. Les concepts se transposent : des remotes, un remote entry, des singletons partagés. Ce qui change, c'est le câblage. Native Federation utilise un fichier de configuration de fédération et un appel initFederation dans main.ts pointant vers un manifeste JSON, au lieu d'une config webpack. Démarrez un nouveau micro-frontend là. Je montre la config webpack parce que c'est ce que la plupart des installations d'entreprise existantes font encore tourner, et parce que la migration est un projet en soi.

Application hôte (shell)Routing, authentification, layout global. Charge les MFEs distants à la… 1 MFEs distants (équipes par fonctionnalité)Applications Angular déployées indépendamment, exposées via Module Fede… 2 Dépendances partagéesDesign system, état d'authentification, store partagé. Instances single… 3 InfrastructureCDN par MFE, pipelines CI/CD indépendants, feature flags par remote 4

Communication entre MFEs : la partie la plus difficile

Une fois que vous avez plusieurs applications Angular déployées indépendamment qui doivent communiquer, vous découvrez que le système d'injection de dépendances d'Angular n'a pas été conçu pour ça. Les services dans l'application hôte ne sont pas automatiquement disponibles pour les services dans les applications distantes. Vous ne pouvez pas injecter un service de l'hôte dans un composant distant directement.

Il existe trois patterns pour la communication inter-MFE, chacun avec des compromis significatifs.

Le premier pattern est les services singletons partagés. Vous exposez un service de l'hôte (ou d'une bibliothèque partagée) et configurez Module Federation pour le traiter comme une dépendance partagée singleton. Les MFEs distants importent et injectent ce service. Ça fonctionne, mais ça crée un couplage fort. Le remote a maintenant une dépendance au moment de la compilation sur l'interface du service de l'hôte. Les changements au service partagé nécessitent de coordonner tous les MFEs qui le consomment.

Le deuxième pattern est les événements personnalisés. Les MFEs communiquent en émettant et écoutant des CustomEvents du navigateur sur le window. C'est totalement découplé : pas d'imports partagés. Ça fonctionne avec n'importe quelle technologie, pas seulement Angular. L'inconvénient est que les CustomEvents sont typés par chaînes de caractères, n'ont pas de support TypeScript par défaut, et le débogage nécessite de comprendre une chaîne d'événements qui traverse plusieurs frontières d'application.

Le troisième pattern est un store d'état partagé : typiquement un petit store sans framework, du TypeScript pur sans aucun import Angular, ni NgRx ni signals, partagé par référence via le mécanisme de dépendance partagée de Module Federation. L'hôte et les remotes lisent et écrivent dans ce store. C'est une solution de gestion d'état global, qui a toute la complexité de l'état global à grande échelle.

typescript
// shared-mfe-store.ts
// A framework-free store, shared as a singleton through Module Federation.
// No Angular imports live in this file.
import type { AppNotification, User, Workspace } from './shared-types';

export interface SharedState {
  currentUser: User | null;
  activeWorkspace: Workspace | null;
  notifications: AppNotification[];
}

export type SharedAction =
  | { type: 'SET_USER'; user: User | null }
  | { type: 'SET_WORKSPACE'; workspace: Workspace | null };

export class SharedMFEStore {
  private _state: SharedState = this.initialState();
  private _listeners = new Set<(state: SharedState) => void>();

  getState(): SharedState {
    return { ...this._state };
  }

  dispatch(action: SharedAction): void {
    this._state = this.reduce(this._state, action);
    this._listeners.forEach(listener => listener({ ...this._state }));
  }

  // Returns the unsubscribe function. Callers must keep it and call it.
  subscribe(listener: (state: SharedState) => void): () => void {
    this._listeners.add(listener);
    return () => {
      this._listeners.delete(listener);
    };
  }

  private initialState(): SharedState {
    return { currentUser: null, activeWorkspace: null, notifications: [] };
  }

  private reduce(state: SharedState, action: SharedAction): SharedState {
    switch (action.type) {
      case 'SET_USER': return { ...state, currentUser: action.user };
      case 'SET_WORKSPACE': return { ...state, activeWorkspace: action.workspace };
      default: return state;
    }
  }
}

export const sharedStore = new SharedMFEStore();

Le store lui-même reste libre de tout Angular. Le pont vers Angular est un service séparé, et c'est là que la libération doit être gérée : subscribe retourne une fonction de désabonnement, et si personne ne l'appelle, le store garde une référence vers un composant disparu depuis longtemps.

typescript
// shared-state-bridge.service.ts
import { DestroyRef, Injectable, Signal, inject, signal } from '@angular/core';
import { sharedStore, SharedState } from 'shared/shared-mfe-store';

@Injectable({ providedIn: 'root' })
export class SharedStateBridge {
  private destroyRef = inject(DestroyRef);

  select<T>(selector: (state: SharedState) => T): Signal<T> {
    const sig = signal<T>(selector(sharedStore.getState()));
    const unsubscribe = sharedStore.subscribe(state => sig.set(selector(state)));
    this.destroyRef.onDestroy(unsubscribe);
    return sig.asReadonly();
  }
}

Défis de l'état partagé

Le défi de l'état partagé avec les MFEs est que l'état que vous avez besoin de partager est presque toujours celui qui change le plus fréquemment et qui a le plus de consommateurs. L'état d'authentification, l'utilisateur courant, les notifications globales : ce sont exactement les choses dont chaque MFE a besoin et que vous devez le plus réussir.

Le problème se complexifie avec les Angular Signals. Les signals ne traversent une frontière MFE que si @angular/core est partagé en singleton, comme dans la config ci-dessus, et si le signal lui-même est transmis via une bibliothèque ou un store partagé. Si un remote se retrouve avec sa propre copie d'Angular, les signals de l'hôte lui sont invisibles. Il faut faire un pont entre le système réactif natif d'Angular et le mécanisme de communication inter-frontières que vous avez choisi. Ce code de pontage est fragile, difficile à tester, et nécessite une compréhension profonde de comment la détection de changements d'Angular interagit avec les changements d'état externes.

Il n'est pas rare qu'un projet passe plus de temps d'ingénierie sur l'infrastructure de communication MFE que sur les fonctionnalités réelles que ces MFEs livrent. Quand la plomberie prend plus d'effort que les fonctionnalités, vous avez probablement sur-architecturé.

Complexité de déploiement

Une application Angular monolithique a un seul artefact de déploiement : une sortie de build qui va sur un CDN. Une architecture micro-frontend avec cinq MFEs a cinq artefacts de déploiement, cinq chemins CDN, une gestion de versions entre les cinq, et une application hôte qui doit référencer des URLs distantes qui fonctionnent dans chaque environnement.

Le problème de configuration des URLs est réellement difficile. En développement, les remotes tournent sur localhost:4201, 4202, etc. En staging, ils vivent sur des sous-domaines différents. En production, encore différents. L'application hôte doit connaître l'URL correcte de chaque remote dans chaque environnement. Vous avez besoin d'une configuration sensible à l'environnement qui est bundlée avec l'hôte mais référence des remotes déployés externément : un problème de configuration qui n'existe pas dans un monolithe.

La compatibilité de versions entre l'hôte et les remotes est un autre vrai problème. Si vous mettez à jour une bibliothèque partagée dans l'hôte, tous les remotes qui déclarent cette bibliothèque comme dépendance partagée recevront la version de l'hôte au runtime. Si la bibliothèque partagée a un changement cassant et qu'un remote a été construit contre l'ancienne version, vous avez un conflit de versions au runtime. La version d'Angular, la version de RxJS et toute bibliothèque de design system partagée deviennent des points potentiels de conflit au runtime.

Compromis d'autonomie d'équipe : l'évaluation honnête

Le bénéfice promis des MFEs est l'autonomie d'équipe. Chaque équipe déploie indépendamment, possède complètement son MFE, et n'est pas bloquée par les autres équipes. En pratique, l'autonomie est plus limitée que ce qui est annoncé, pour deux raisons.

Premièrement, les dépendances partagées contraignent les choix technologiques. Si l'hôte utilise Angular 21, tous les remotes doivent utiliser une version Angular compatible. Vous ne pouvez pas avoir une équipe sur Angular 19 et une autre sur Angular 21 dans le même écosystème MFE : la contrainte de bibliothèque partagée singleton fait que la divergence de versions produit des erreurs au runtime. Les équipes sont autonomes à certains égards, mais elles sont couplées au niveau des dépendances.

Deuxièmement, les préoccupations transversales nécessitent de la coordination. Les changements d'authentification, les changements de navigation globale, les mises à jour du design system partagé : tout ça nécessite de la coordination entre les équipes quelle que soit l'architecture MFE. La frontière architecturale ne fait pas disparaître la communication organisationnelle. Elle la déplace de la revue de code vers les contrats d'interface et les tests d'intégration.

Le bénéfice d'autonomie est réel, mais il est plus étroit que le discours le suggère. Vous obtenez un déploiement autonome de fonctionnalités qui vivent entièrement dans la frontière du MFE d'une équipe. Pour les fonctionnalités qui traversent les frontières, les MFEs n'aident pas et peuvent rendre les choses plus difficiles.

Où ça a du sens

Je veux être juste envers le pattern. Imaginez une grande organisation où plusieurs équipes de développement travaillent sur différents produits qui partagent une expérience portail unifiée. L'équipe A possède le MFE d'onboarding. L'équipe B possède le MFE de gestion de comptes. L'équipe C possède le MFE de rapports. Ces équipes ont des cadences de release différentes, des product managers différents et des rotations d'astreinte différentes. Elles ne devraient pas être bloquées par les déploiements des autres. C'est là que les MFEs ont un sens réel.

La surface partagée entre ces MFEs est intentionnellement étroite : un design system, un état d'authentification et un cadre de navigation global. Tout à l'intérieur de chaque zone produit est autonome. La frontière de déploiement correspond à la frontière organisationnelle. Cet alignement est la clé : quand l'architecture correspond à la structure organisationnelle, les MFEs réduisent une vraie friction.

Où c'est de la sur-ingénierie

Un raté classique, c'est l'architecture MFE appliquée à une application où une seule petite équipe possède chaque micro-frontend et prend chaque décision de déploiement. Il n'y a pas de problème d'indépendance de déploiement à résoudre quand une équipe prend toutes les décisions de déploiement. Il n'y a pas de problème d'autonomie d'équipe quand tout le monde est dans le même stand-up. Dans ce cas, l'architecture MFE ajoute des mois de complexité de mise en place, une surcharge de maintenance continue, et des situations de débogage difficiles, tout cela pour tenter de résoudre un problème organisationnel qui n'existe pas.

  • Équipe unique de moins de 8 développeurs : un monorepo avec des frontières internes fortes est presque toujours mieux
  • Applications avec moins de 5 zones produit distinctes : la surcharge d'unité de déploiement n'en vaut pas la peine
  • Équipes qui livrent moins de deux releases par semaine : le bénéfice d'indépendance de déploiement est minimal
  • Projets greenfield sans product-market fit prouvé : vous pivoterez l'architecture avant de stabiliser le produit
  • Projets où la surface partagée entre MFEs est grande : un couplage fort par l'état partagé annule l'indépendance

Mon verdict

Les micro-frontends avec Angular Module Federation sont une solution légitime à un vrai problème. Ce problème est : plusieurs équipes qui ont besoin d'une véritable indépendance de déploiement pour des fonctionnalités qui ont une petite surface partagée. Si cette description correspond précisément à votre situation, les MFEs valent l'investissement.

Pour mon projet SaaS personnel, je n'utiliserais jamais de MFEs. Développeur solo, un seul déploiement, pas de problème de coordination d'équipe. Pour une startup avec un produit et trois développeurs, je n'utiliserais pas de MFEs. Pour une grande organisation avec des équipes produit véritablement indépendantes qui partagent un portail, c'est le bon choix.

L'erreur que je vois le plus souvent est d'appliquer les MFEs parce qu'ils semblent matures et scalables, pas parce qu'un problème concret que les MFEs résolvent est réellement présent. L'architecture devrait être une réponse à de vrais problèmes, pas une optimisation préventive pour des problèmes que vous pourriez avoir un jour. Si vous n'avez pas plusieurs équipes bloquées par les déploiements des autres en ce moment, investissez ce temps d'ingénierie à livrer des fonctionnalités plutôt.

La bonne architecture n'est pas la plus sophistiquée. C'est la plus simple qui résout les problèmes que vous avez réellement, avec assez de structure pour grandir vers les problèmes que vous êtes susceptible de rencontrer.

Références

AngularMicro-FrontendsArchitectureEnterprise