L'architecture logicielle des applications Angular oscille entre deux modèles dominants : l'architecture en couches (layered) et l'architecture par feature. Beaucoup de développeurs les présentent comme opposées, voire incompatibles. En réalité, c'est un faux débat. Le vrai enjeu est de comprendre les coûts cachés de chaque approche et comment les combiner pour éviter que votre codebase devienne un marais de dépendances circulaires ou un dédale de dossiers impossibles à naviguer.
Layered : la structure classique qui rassure
L'architecture en couches—controllers, services, models, views—a dominé pendant deux décennies pour une raison simple : elle est prévisible. Chaque couche a un rôle clair. Un service appelle un repository, qui interroge l'API. Un composant injecte le service et affiche les données. C'est hiérarchique, facile à expliquer aux juniors, et les tests unitaires s'alignent naturellement sur cette séparation.
Mais cette clarté cache une fragilité. Au fur et à mesure que l'app grandit, les services deviennent des dépotoirs. Un service d'authentification finit par gérer l'auth, les permissions, la gestion de session, les logs de sécurité, et parfois la navigation. Les composants de haut niveau finissent par importer des services de bas niveau sans passer par les couches intermédiaires. Les dépendances croisées apparaissent. Vous vous retrouvez avec un graphe de dépendances que personne ne comprend vraiment, où modifier un service risque de casser trois features à l'autre bout de l'app.
Le vrai problème n'est pas la structure en couches, c'est l'absence de frontière fonctionnelle. Une app de 50 000 lignes avec 15 services partagés est difficile à maintenir parce que personne ne sait vraiment qui dépend de qui, et les changements se propagent comme une épidémie.
Feature-based : découper par responsabilité métier
Une architecture feature-based organise le code autour des fonctionnalités : un dossier /cart , un dossier /checkout , un dossier /user-profile . Chaque feature contient ses composants, ses services, ses modèles, ses pipes, ses validators. C'est un mini-module autonome.
L'avantage ? Une équipe peut travailler sur la feature /cart sans toucher à /checkout . Un nouveau développeur peut comprendre une feature en lisant un seul dossier. Les tests sont localisés. Si vous supprimez une feature, vous supprimez un dossier, point final. Pas de trace fantôme dans des services partagés.
Concrètement, votre structure ressemble à ceci :
src/app/
/features/
/cart/
/components/
/services/
/models/
/cart.module.ts
/checkout/
/components/
/services/
/models/
/checkout.module.ts
/shared/
/components/
/services/
/models/
/core/
/auth.service.ts
/http-interceptors/
Mais feature-based crée son propre piège : la duplication. Deux features ont besoin d'un composant de liste paginée ? Vous le copiez-collez, ou vous créez un service partagé ? Si c'est un service partagé, vous réintroduisez le couplage. Si c'est une copie, votre maintenance se complique.
Le hybrid model : la vraie sagesse
Les meilleurs projets Angular combinent les deux. Vous gardez une couche /core pour ce qui est réellement transversal : guards, intercepteurs, l'auth, les logs. Vous gardez une couche /shared pour les composants et utilities qui servent plusieurs features. Et vous organisez le reste par feature.
La clé est la discipline : /core dépend de rien. /shared dépend de /core , mais pas de /features . Chaque feature dépend de /core et /shared , mais pas d'autres features. Vous créez une hiérarchie d'import stricte que votre linter peut vérifier avec eslint-plugin-nx ou eslint-plugin-import .
Voici un exemple concret avec deux features : panier et paiement. Le panier gère l'ajout/suppression d'articles. Le paiement utilise le panier pour récupérer le total.
// cart/services/cart.service.ts
//@Injectable()
//@providedIn: 'root'
export class CartService {
/constructor(private http: HttpClient) {}
/addItem(product: Product): Observable<Cart> { ... }
/getTotal(): Observable<number> { ... }
}
// checkout/services/checkout.service.ts
//@Injectable()
export class CheckoutService {
/constructor(private cartService: CartService) {}
/processPayment(amount: number): Observable<PaymentResult> {
/return this.cartService.getTotal().pipe(
/switchMap(total => this.http.post('/api/payment', { amount: total }))
/);
/}
}
Le paiement dépend du panier, c'est métier normal. Mais si le panier avait besoin du paiement en retour, vous auriez une dépendance circulaire. Une couche /shared avec un CartStateService basé sur Signals ou NgRx casse ce cycle : le panier émet son état, le paiement l'écoute, aucune dépendance directe.
Pièges courants
Le piège numéro un : croire qu'une architecture choisie résout les problèmes de conception. Une app en feature-based mal pensée reste un chaos. Des features qui s'appellent les unes les autres directement, des services partagés qui ne sont partagés qu'en nom, des dépendances circulaires qui explosent à la compilation. L'architecture n'est que le conteneur ; la discipline est le contenu.
Deuxième piège : sur-ingéniérer le partage. Vous créez un dossier /shared avec 50 composants, et à la fin, seuls 5 sont réellement utilisés. Le reste crée de la friction. Préférez créer du partagé quand vous en avez besoin, pas en anticipation.
Troisième piège : confondre feature et module. Une feature n'est pas forcément un module lazy-loadable. Vous pouvez avoir une feature dans /shared qui est chargée à l'initialisation, et une feature dans /features qui est lazy. La structure de dossier et la stratégie de chargement sont deux décisions indépendantes.
Stratégie pratique pour votre prochain projet
Commencez feature-based pour les éléments métier (panier, compte utilisateur, search). Maintenez /core pour l'infrastructure (auth, http, config). Construisez /shared au fur et à mesure, pas d'un coup. Utilisez un linter pour forcer la hiérarchie des dépendances. Et surtout, documentez pourquoi vous mettez quelque chose dans /shared : "Utilisé par 3+ features" c'est bon ; "On ne sait pas" c'est mauvais.
Pour les équipes distribuées ou les grandes apps, feature-based gagne en clarté. Pour une startup avec une app monolithique de 20 KLOC, layered peut suffire si vous restez discipliné. Le choix dépend de votre équipe, pas d'un article de blog.
La vraie architecture, c'est celle que votre équipe comprend, maintient, et améliore sans friction. Pas celle qui semble la plus cool sur Hacker News.