Accueil / Articles / DI frontend indépendant du framework avec InversifyJS et une racine de composition

DI frontend indépendant du framework avec InversifyJS et une racine de composition

Comment choisir un conteneur DI pour TypeScript, emballer chaque domaine en tant que ContainerModule, relier tout dans une racine de composition unique et le connecter à React, Vue et Angular.

3230 mots

Les grands ensembles de code frontend échouent rarement à cause d’un seul composant défectueux ; ils échouent parce que les règles métier s’emmêlent progressivement avec les hooks React, les clients HTTP et les API de stockage, au point qu’il devient impossible de tester ou de réutiliser quoi que ce soit indépendamment. L’injection de dépendances est le mécanisme de bas niveau qui permet de maintenir ces règles séparées, mais uniquement si la structure est conçue intentionnellement. Ce guide explique comment choisir un conteneur pour un monorepo TypeScript multi-domaine, définir des tokens sécurisés par type, isoler chaque domaine derrière un seul module, assembler le graphe dans une racine de composition, et l’exposer à React, Vue et Angular au moyen de ponts ne comptant chacun que une dizaine de lignes.

Pourquoi l’isolation doit être décidée avant de modéliser le domaine

Les conseils classiques du Domain-Driven Design recommandent de reporter les détails d’implémentation : modéliser d’abord les entités et les cas d’utilisation, puis choisir les outils par la suite. En pratique, une équipe qui entame un grand changement architectural tire avantage de résoudre dès le début une question technique, à savoir comment l’isolation des modules sera réellement assurée. Sans mécanisme convenu, les premiers domaines sont conçus de manière ad hoc et les conventions ne se rétablissent jamais.

Le principe fondamental est le principe d’inversion des dépendances : les politiques de haut niveau doivent dépendre d’abstractions, tandis que les détails concrets doivent dépendre de ces mêmes abstractions. l’injection de dépendances est la technique pratique qui permet de le mettre en œuvre. Les classes demandent des tokens abstraits plutôt que des implémentations concrètes, et un élément extérieur décide de ce à quoi ces tokens se résolvent. Dans le DDD et l’architecture propre, il ne s’agit pas d’une simple amélioration optionnelle ; le domaine ne doit pas savoir quel framework UI, quel client HTTP ou quelle couche de persistance il utilise.

La plupart des équipes front-end recourent en premier lieu à React Context (ou à provide/inject dans Vue). Cela semble suffisant : on déclare une valeur près de la racine et on peut la lire n’importe où en dessous. Les limites apparaissent rapidement : il n’y a pas de gestion du cycle de vie, aucune notion de factories ou de scopes, et chaque consommateur de cette valeur est désormais lié au runtime de React, si bien que la logique ne peut pas s’exécuter ni être testée sans lui. Le chemin menant à une véritable racine de composition est ce que couvre le reste de ce guide. Écrire un conteneur sur mesure n’est rarement justifié ; donc la première étape consiste à évaluer honnêtement les options d’IoC existantes.

Critères pour un conteneur DI front-end

Les frameworks backend tels que NestJS, Spring et .NET intègrent déjà la conception orientée dépendances comme fonctionnalité standard résolue. Le code frontend présente des contraintes supplémentaires : coût en temps de exécution, limites de taille des fichiers compilés et nécessité de fonctionner avec plusieurs frameworks UI. Cinq critères guident l’évaluation :

  1. Agnosticisme vis-à-vis des frameworks. Le conteneur doit pouvoir fonctionner avec du TypeScript pur, sans dépendance en temps de exécution vis-à-vis de React, Vue ou Angular, et doit être compatible avec le navigateur, Node.js pour le SSR, ainsi que React Native.
  2. Modularité. Chaque domaine doit pouvoir fournir un module prédéfini dont les liaisons sont scellées à l’intérieur, de sorte que l’application ne charge que ce module. Cela permet d’éviter un fichier main.ts de 500 lignes où chaque service est enregistré manuellement.
  3. Gestion du cycle de vie. Les singleton, les instances temporaires et la résolution par scoping doivent être pris en charge de manière optimale.
  • Testabilité. Remplacer une dépendance par un mock ou un faux objet ne devrait nécessiter qu’une seule ligne de code, sans avoir à reconstruire le conteneur.
  • Impact sur l’archive. La charge supplémentaire en environnement de production doit être faible, que ce soit pour la version brute, minifiée ou compressée en gzip.
  • Les candidats

    InversifyJS

    InversifyJS est depuis longtemps le choix par défaut pour les grands projets TypeScript et constitue le conteneur IoC le plus complet de l’écosystème. Il est indépendant de tout framework et déclare les dépendances à l’aide de décorateurs. Son ContainerModule regroupe les liens en unités logiques, et il prend en charge inSingletonScope, inTransientScope et inRequestScope, des conteneurs enfants, le chargement et le déchargement de modules en temps de exécution, ainsi que des hooks d’activation. Le prix à payer est son poids : c’était l’option la plus lourde mesurée, et qualifier les versions actuelles de légères serait trompeur.

    TSyringe

    TSyringe, géré par Microsoft, privilégie la simplicité au détriment de la polyvalence. Il résout automatiquement les dépendances des constructeurs à partir des métadonnées de TypeScript, propose un ensemble riche de décorateurs et prend en charge les modes de vie habituels (Singleton, Transient, ResolutionScoped, ContainerScoped). Pour une application de taille moyenne, c’est une solution attractive et peu contraignante à mettre en place. Cependant, deux facteurs le rendent moins adapté dans une architecture multi-domaine : il n’existe pas de concept de module de première classe, ce qui oblige à construire manuellement la composition à l’aide de fonctions d’enregistrement ou de createChildContainer(), et il dépend du polyfill reflect-metadata, ce qui ajoute un poids non négligeable.

    Awilix

    Awilix évite complètement les décorateurs. Il utilise l’API ES6 Proxy pour associer les dépendances aux noms des paramètres du constructeur ou aux clés de l’objet d’arguments de la factory. Cela en fait un choix idéal pour les équipes qui ne souhaitent pas utiliser de décorateurs, et c’était le conteneur autonome le plus compact évalué. Il prend en charge les modes de vie SINGLETON, SCOPED et TRANSIENT ; les modules sont organisés à l’aide de fonctions de registration personnalisées.

    Un piège spécifique aux frontends mérite d’être souligné. Dans le mode d’injection CLASSIC, Awilix lit les noms des paramètres du constructeur en tant que chaînes de caractères, et les minificateurs renomment ces paramètres, ce qui perturbe la résolution en environnement de production. Seul le mode par défaut PROXY résiste à un tel traitement agressif.

    L’injecteur intégré d’Angular

    Angular dispose de l’un des systèmes d’injection dépendante hiérarchique les plus performants dans le développement frontend, avec une injection basée sur des tokens, des fournisseurs ciblés et un chargement différé intégrés. S’il pouvait être utilisé sans le runtime d’Angular, il serait un concurrent sérieux pour la couche de domaine. Or c’est impossible, et c’est là le problème.

    React Context

    Context n’est pas vraiment un conteneur d’injection dépendante ; c’est plutôt un moyen d’éviter le problème du « prop drilling ». Un fournisseur est placé près de la racine et les composants y accèdent à l’aide de useContext(). Toute modification de la valeur fournie force à re-render tous les composants qui en dépendent, et il n’y a ni gestion du cycle de vie, ni liaison via des factories, ni isolation des scopes. Son seul véritable avantage est qu’il ne coûte rien de supplémentaire dans le bundle.

    Vue provide et inject

    Le mécanisme de Vue transmet les valeurs le long de l’arbre des composants et partage les limites architecturales du Context : il ne constitue pas une base pratique pour un graphe de services métier. Il présente cependant un avantage ergonomique, puisque les dépendances peuvent être enregistrées globalement via un plugin au lieu d’envelopper l’arbre dans des fournisseurs imbriqués. La fonction moderne inject() d’Angular repose sur une idée similaire de contexte d’injection actif.

    Injection par constructeur simple

    L’approche sans bibliothèque transmet explicitement chaque dépendance via des constructeurs. Elle est entièrement sécurisée du point de vue des types et ne présente aucun surcoût en temps d’exécution. L’inconvénient réside dans l’échelle : une fois qu’une application compte environ 10 à 15 services métier, le câblage manuel du graphe au point d’entrée se transforme en un fichier volumineux et fragile que tout le monde évite de modifier.

    Ce que montre la comparaison

    La taille des bundles a été comparée en regroupant un fichier d’entrée qui importe l’API publique de chaque package à l’aide de esbuild --bundle --minify --platform=browser, puis en compressant le résultat avec gzip -9, ce qui isole la charge propre au conteneur du code de l’application. Quatre conclusions en ont découlé :

    1. Le DI de framework relève de l’interface utilisateur, et non du domaine métier. L’inclusion de useContext() ou de @Injectable() d’Angular dans les bibliothèques de domaine lie les règles métier à un seul framework, ce qui ne respecte pas le premier critère.
    2. L’injection manuelle ne convient qu’aux petits projets. Dans un grand monorepo d’entreprise, le point de composition se transforme en des milliers de lignes de connexions écrites manuellement.
  • InversifyJS a gagné grâce à sa modularité. C’est le seul candidat dans lequel un module (ContainerModule) constitue une valeur de première classe que une bibliothèque de domaine peut créer, encapsuler et exporter en tant qu’unité unique.
  • La configuration de compilation est importante. InversifyJS et TSyringe nécessitent experimentalDecorators: true, emitDecoratorMetadata: true ainsi que target: ES2022 dans tsconfig.json. InversifyJS v8 n’a plus besoin d’un polyfill reflect-metadata distinct. Avec des outils basés sur esbuild ou SWC (Vite, Next.js, Bun), assurez-vous que l’étape de transformation prend en charge les métadonnées des décorateurs anciens, généralement via un plugin, car esbuild ne génère pas automatiquement ces métadonnées.
  • Quatre règles pour éviter que le conteneur ne devienne un service locator

    Un conteneur en soi ne met rien à l’abri ; s’il est utilisé de manière négligente, il devient un ensemble global de services auxquels n’importe quel fichier peut accéder. L’architecture repose donc sur quatre conventions :

    • Des tokens d’injection de dépendances abstraits créés avec Symbol.for et une propriété $.
    • Chaque bibliothèque de domaine n’exporte que son module de conteneur, jamais ses classes concrètes.
    • Une seule racine de composition assemble le graphe de l’application.
    • Un pont léger par framework UI expose le conteneur aux composants.

    Tokens sécurisés par type avec Symbol.for et $

    @my-app/*-contracts à une constante du même nom, dont la propriété $ contient un Symbol.

    // libs/auth/contracts/src/lib/interfaces/auth.facade.ts
    import type { ServiceIdentifier } from 'inversify';
    
    export interface AuthFacade {
      getAccessToken(): Promise<string | null>;
      login(): Promise<void>;
      logout(): Promise<void>;
    }
    export const AuthFacade = {
      $: Symbol.for('AuthFacade') as ServiceIdentifier<AuthFacade>,
    };
    
    // libs/auth/contracts/src/lib/interfaces/pin.facade.ts
    export interface PinFacade {
      setupPin(pin: string): Promise<void>;
      verifyPin(sub: string, pin: string): Promise<PinVerifyResult>;
      changePin(sub: string, currentPin: string, newPin: string): Promise<PinVerifyResult>;
    }
    export const PinFacade = {
      $: Symbol.for('PinFacade') as ServiceIdentifier<PinFacade>,
    };
    

    Ce fragment recèle trois décisions importantes :

    1. Un seul nom pour le type et la valeur. TypeScript conserve les types et les valeurs dans des espaces de noms distincts, ce qui permet à AuthFacade d’être à la fois l’interface et le conteneur des jetons. Le compilateur choisit le sens approprié en fonction du contexte, et personne n’a besoin d’inventer des noms tels que AuthFacadeToken.
  • Symbol.for au lieu de Symbol(). Chaque appel à Symbol() renvoie une valeur entièrement nouvelle, tandis que Symbol.for() recherche la clé dans le registre global des symboles du runtime. Si un package contract est dupliqué dans deux bundles, ce qui peut arriver dans un monorepo ou avec Module Federation, les deux copies génèrent toujours le même token et les recherches continuent de fonctionner sans échouer mystérieusement. L’inconvénient est que les clés du registre sont des chaînes globales, donc elles doivent être uniques dans toute l’application.
  • Le cast ServiceIdentifier<AuthFacade>. En spécifiant $ comme ServiceIdentifier<T> d’Inversify, on lie le token à son interface au moment de la compilation, permettant ainsi aux sites de résolution d’inférer le bon type sans généricités explicites.
  • Le fragment suivant montre les deux aspects : une classe qui reçoit la façade par injection via le constructeur, et une appel direct à container.get dont le type de retour est déduit du token.

    import { inject, injectable } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    
    @injectable()
    export class LoginPageComponent {
      // Strongly-typed injection via token
      constructor(@inject(AuthFacade.$) private readonly auth: AuthFacade) {}
    }
    
    // Resolution site automatically infers return type as AuthFacade
    const facade = container.get(AuthFacade.$); // AuthFacade
    

    Le consommateur ne fait référence qu’au contrat. Il n’a aucune idée de ce qui se trouve à l’intérieur de @my-app/auth-core, et c’est justement le but.

    Un domaine, un ContainerModule

    La bibliothèque de base de chaque domaine expose une seule chose : son module DI. Les cas d’usage, les entités, les ports et les repositories restent privés. À l’intérieur du module, les cas d’usage internes sont liés entre eux, tandis que la façade publique est liée à son token de contrat.

    // libs/auth/core/src/lib/auth-container.module.ts
    import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    import { CoreAuthFacade } from './facades/core-auth.facade';
    import { LoginUseCase } from './use-cases/login.use-case';
    import { LogoutUseCase } from './use-cases/logout.use-case';
    import { GetAccessTokenUseCase } from './use-cases/get-access-token.use-case';
    
    export const authContainerModule = new ContainerModule((options: ContainerModuleLoadOptions) => {
      // Private use cases (registered to self within domain)
      options.bind(LoginUseCase).toSelf().inSingletonScope();
      options.bind(GetAccessTokenUseCase).toSelf().inSingletonScope();
      options.bind(LogoutUseCase).toSelf().inSingletonScope();
      // Public contract implementation bound to token
      options.bind(AuthFacade.$).to(CoreAuthFacade).inSingletonScope();
    });
    
    // libs/auth/core/src/index.ts - ONLY the ContainerModule is re-exported!
    export * from './lib/auth-container.module';
    

    L’encapsulation est appliquée à deux reprises :

    1. API publique. Le fichier index.ts de la bibliothèque ne réexporte que authContainerModule. Des classes comme LoginUseCase ou CoreAuthFacade ne sont tout simplement pas accessibles depuis d’autres packages.
    2. Règles de linting. La règle ESLint @nx/enforce-module-boundaries d’Nx vérifie les étiquettes des projets ; ainsi, par exemple, un projet marqué type:core ne peut pas importer le projet type:core d’un autre domaine. Consultez la documentation sur les limites des modules Nx pour savoir comment déclarer les étiquettes et les contraintes.

    Les limites d’export empêchent les imports accidentels ; la règle de linting empêche les raccourcis intentionnels via des chemins complexes. Ensemble, elles font de l’architecture quelque chose que le CI vérifie plutôt que quelque chose dont les reviewers doivent se souvenir.

    La racine de composition

    La racine de composition est l’endroit où le graphe de dépendances est créé, généralement apps/my-app/src/composition-root.ts ou une fonction appelée depuis main.ts. Une règle la régit :

    La racine de composition est le seul endroit dans le codebase autorisé à importer des implémentations concrètes, des adaptateurs d’infrastructure et des modules centraux du domaine.

    La fonction ci-dessous lie d’abord l’infrastructure globale (un client HTTP basé sur Axios et un générateur de logs construit à partir d’une usine afin qu’il puisse recevoir des transports configurés), puis charge les modules du domaine.

    // apps/my-app/src/composition-root.ts
    import { Container } from 'inversify';
    import { HttpClientPort, LoggerPort } from '@my-app/shared-contracts';
    import { AxiosHttpClient, BrowserLogger, ConsoleLogTransport, FileLogTransport } from '@my-app/shared-infrastructure';
    
    import { authContainerModule } from '@my-app/auth-core';
    import { paymentsContainerModule } from '@my-app/payments-core';
    export const initAppContainer = (): Container => {
      const container = new Container();
    
      // 1. Bind global infrastructure adapters
      container.bind(HttpClientPort.$).to(AxiosHttpClient).inSingletonScope();
      container.bind(LoggerPort.$).toDynamicValue(() => new BrowserLogger({
        transports: [
          new ConsoleLogTransport(),
          new FileLogTransport(),
        ],
      })).inSingletonScope();
    
      // 2. Load domain modules
      container.load(authContainerModule);
      container.load(paymentsContainerModule);
    
      return container;
    };
    

    Par souci de complétude, l’adaptateur BrowserLogger est une classe ordinaire qui implémente LoggerPort et distribue chaque message à ses transports respectifs. Il ne comporte aucun décorateur ; comme il est créé à l’intérieur de toDynamicValue, le conteneur n’a jamais besoin d’examiner son constructeur.

    // libs/shared/infrastructure/src/lib/logging/browser-logger.ts
    import type { LoggerPort } from '@my-app/shared-contracts';
    import type { LogTransport } from './interfaces/log-transport.interface';
    
    export interface BrowserLoggerOptions {
      transports: LogTransport[];
    }
    
    export class BrowserLogger implements LoggerPort {
      constructor(private readonly options: BrowserLoggerOptions) {}
    
      info(message: string, ...args: unknown[]): void {
        this.options.transports.forEach(t => t.write('INFO', message, args));
      }
    }
    

    Plusieurs applications à partir d’un noyau de domaine

    Puisque l’infrastructure est choisie au niveau du Composition Root, différentes applications peuvent réutiliser les mêmes paquets de domaine avec des adaptateurs différents ainsi qu’une sélection différente de modules. Une application React Native peut utiliser un stockage basé sur AsyncStorage, tandis qu’une application d’administration web utilise IndexedDB et charge un domaine de contrôle plutôt que celui des paiements.

    // apps/mobile/src/composition-root.ts — React Native target
    container.bind(StoragePort.$).to(AsyncStorageAdapter).inSingletonScope();
    container.bind(LoggerPort.$).to(RnLogger).inSingletonScope();
    container.load(authContainerModule);
    container.load(paymentsContainerModule);
    
    // apps/admin/src/composition-root.ts - Web Admin target
    container.bind(StoragePort.$).to(IndexedDbAdapter).inSingletonScope();
    container.bind(LoggerPort.$).to(BrowserLogger).inSingletonScope();
    container.load(authContainerModule);
    container.load(auditContainerModule); // Payments domain excluded entirely
    

    @my-app/auth-core est identique, caractère pour caractère, dans les versions mobile, admin et desktop. Il ne détecte jamais si le stockage est de type AsyncStorage ou IndexedDB.

    Permettre à un domaine d’utiliser un autre

    Supposons que payments-core ait besoin d’un jeton d’accès géré par auth-core. L’importation de @my-app/auth-core est interdite par les règles de délimitation. Par conséquent, le cas d’usage des paiements dépend du jeton de contrat du domaine d’authentification, qui se trouve dans le package contracts et est autorisé.

    // libs/payments/core/src/lib/use-cases/create-payment.use-case.ts
    import { inject, injectable } from 'inversify';
    
    // Import from contracts, NOT core — permitted by boundary rules
    import { AuthFacade } from '@my-app/auth-contracts';
    import { PaymentsGatewayPort } from '../ports/payments-gateway.port';
    
    @injectable()
    export class CreatePaymentUseCase {
      constructor(
        @inject(AuthFacade.$) private readonly auth: AuthFacade,
        @inject(PaymentsGatewayPort.$) private readonly gateway: PaymentsGatewayPort,
      ) {}
    
      async execute(amount: number): Promise<void> {
        const token = await this.auth.getAccessToken();
        return this.gateway.processPayment(amount, token);
      }
    }
    

    paymentsContainerModule ne lie pas AuthFacade.$ ; il le consomme uniquement. Le lien est établi lorsque la racine de composition charge les deux modules dans le même conteneur. Conséquence pratique : si une application charge les fonctionnalités de paiement sans authentification, la résolution échoue en temps de exécution ; il est donc utile d’effectuer un test de base pour chaque cible d’application afin de s’assurer que tous les tokens publics sont résolus une fois.

    Mettre en relation le conteneur et les frameworks UI

    Puisque le conteneur ne connaît rien sur les bibliothèques UI, chaque framework dispose d’un petit adaptateur comptant environ 10 à 15 lignes de code.

    React

    Le conteneur est créé une seule fois au démarrage et n’est jamais remplacé, de sorte que la valeur du contexte ne change jamais. Cela évite la cascade de réaffichage généralement associée à Context : les consommateurs reçoivent une référence stable. useInjection mémorise la recherche par conteneur et token, et lance une erreur claire en l’absence du fournisseur.

    // libs/shared/react-di/src/lib/di-context.tsx
    import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
    import type { Container, ServiceIdentifier } from 'inversify';
    
    export interface DIProviderProps {
      container: Container;
      children: ReactNode;
    }
    
    const DIContext = createContext<Container | null>(null);
    
    export const DIProvider = ({ container, children }: DIProviderProps) => (
      <DIContext.Provider value={container}>
        {children}
      </DIContext.Provider>
    );
    
    export const useInjection = <T,>(token: ServiceIdentifier<T>): T => {
      const container = useContext(DIContext);
    
      return useMemo(() => {
        if (!container) {
          throw new Error('useInjection must be used within a DIProvider');
        }
    
        return container.get(token);
      }, [container, token]);
    };
    

    Vue

    Le système de plugins de Vue ainsi qu’un InjectionKey typé rendent l’adaptateur encore plus concis. Le plugin fournit le conteneur au niveau de l’application, et un composant y résout les tokens.

    // libs/shared/vue-di/src/lib/di-plugin.ts
    import type { App, InjectionKey } from 'vue';
    import { inject } from 'vue';
    import type { Container, ServiceIdentifier } from 'inversify';
    
    export const DI_CONTAINER: InjectionKey<Container> = Symbol('DI_CONTAINER');
    
    export const diPlugin = {
      install(app: App, { container }: { container: Container }) {
        app.provide(DI_CONTAINER, container);
      },
    };
    
    export const useInjection = <T>(token: ServiceIdentifier<T>): T => {
      const container = inject(DI_CONTAINER);
    
      if (!container) {
        throw new Error('diPlugin is not installed');
      }
    
      return container.get(token);
    };
    

    Angular

    L’injecteur hiérarchique d’Angular peut déléguer au conteneur de domaine via des fournisseurs de type factory. En transmettant le conteneur à l’aide d’un InjectionToken et en créant un nouveau conteneur à chaque démarrage, on empêche les requêtes de rendu serveur simultanées d’échanger des états.

    // apps/angular-app/src/app/domain.providers.ts
    import { InjectionToken, type Provider } from '@angular/core';
    import type { Container } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    import { initAppContainer } from './composition-root';
    
    export const DI_CONTAINER = new InjectionToken<Container>('DI_CONTAINER');
    export const AUTH_FACADE = new InjectionToken<AuthFacade>('AUTH_FACADE');
    
    export const provideDomainContainer = (container: Container): Provider[] => [
      {
        provide: DI_CONTAINER,
        useValue: container,
      },
      {
        provide: AUTH_FACADE,
        useFactory: (c: Container) => c.get(AuthFacade.$),
        deps: [DI_CONTAINER],
      },
    ];
    
    // main.ts - fresh container instance created per bootstrap
    bootstrapApplication(AppComponent, {
      providers: [...provideDomainContainer(initAppContainer())],
    });
    

    Chaque composant Angular supplémentaire nécessite sa propre InjectionToken et son fournisseur de factory, ce qui fait que la liste augmente avec l’API publique ; il est possible de la générer à partir d’un petit tableau lorsque sa longueur devient importante.

    Coûts et moments où l’on peut s’en passer

    Cette configuration ajoute environ 21 KB en format compressé et nécessite l’utilisation de emitDecoratorMetadata lors de la compilation. Dans un monorepo d’entreprise comprenant dix domaines ou plus, ce coût est rapidement compensé. Pour une application avec deux ou trois écrans, l’isolation représente un surcoût inutile ; une injection via constructeur simple ou même des singleton au niveau du module suffiront mieux.

    Points clés

    • Gardez l’infrastructure d’injection du framework (Context, provide/inject, fournisseurs Angular) à la périphérie de l’interface utilisateur ; le cœur du domaine ne doit dépendre que des tokens de contrat.
  • Choisissez d’abord un conteneur pour l’histoire de son module : un domaine qui exporte un ContainerModule scellé est ce qui maintient le point d’entrée compact.
  • Utilisez des tokens Symbol.for de type ServiceIdentifier<T> afin que les packages dupliqués et l’inférence de type continuent de fonctionner correctement.
  • Imposez des limites grâce aux exports et aux règles de linting, et non uniquement par des conventions.
  • Permettez à un seul fichier, la racine de composition, d’accéder aux implémentations concrètes ; les builds multiplateformes deviennent alors une simple question de remplacement des bindings.
  • N’oubliez pas que l’injection n’est qu’un outil. L’objectif est l’inversion : si vous pouvez remplacer React par Vue, SQLite par IndexedDB ou Axios par Fetch sans modifier la couche de domaine, la stratégie a rempli sa fonction.
  • Lectures complémentaires

  • Choisir une structure de dossier React : sept layouts et leurs points de brise — Comparez les structures plates, basées sur les types, basées sur les fonctionnalités, Atomic Design, DDD, Feature-Sliced Design et monorepo pour React, et découvrez à quel niveau chacune cesse de fonctionner.
  • Déplacer Angular HttpClient vers le backend Fetch avec withFetch() — Comprenez pourquoi Angular HttpClient passe de XMLHttpRequest à fetch, comment withFetch() permet le SSR aux limites et la diffusion en flux, ainsi que ses conséquences pour vos intercepteurs.
  • Comment Angular Dependency Injection résout les services avec inject() — Comprendre comment Angular DI fournit des services via inject(), providers et injecteurs hiérarchiques, ainsi que comment définir le périmètre d’application des instances et éviter les erreurs de fournisseur manquant.
  • SOLID comme question sur le changement : Refactoriser un service Spring Boot — Apprendre à appliquer chaque principe SOLID dans un backend Spring Boot en se demandant ce qui est susceptible de changer, et comment éviter l’over-engineering que SOLID entraîne souvent.