Files
home_stock/docs/adr/0002-architecture-monolithe-modulaire.md
2026-01-28 19:22:30 +01:00

10 KiB

ADR-0002 — Architecture monolithe modulaire

  • Statut : accepted
  • Date : 2026-01-27

Contexte

HomeStock nécessite une architecture logicielle adaptée à un projet mono-utilisateur self-hosted avec les contraintes suivantes :

  • Simplicité de déploiement : L'application doit pouvoir être déployée facilement avec une seule commande
  • Maintenance réduite : Pas d'équipe DevOps, maintenance manuelle par l'utilisateur
  • Développement solo : Un seul développeur, besoin de cohérence et de simplicité
  • Évolutivité future : Possibilité d'évoluer vers multi-utilisateurs sans refonte complète
  • Modules logiques distincts : items, locations, categories, documents, search

Le projet démarre avec un périmètre limité (MVP) mais doit pouvoir évoluer progressivement. La question architecturale centrale est : comment organiser le code pour maximiser la maintenabilité tout en gardant une complexité opérationnelle minimale ?


Décision

Nous adoptons une architecture monolithe modulaire avec les caractéristiques suivantes :

Définition

  • Un seul dépôt Git (monorepo) contenant backend et frontend
  • Un seul processus backend (serveur FastAPI unique)
  • Modules logiques séparés avec responsabilités clairement définies
  • Communication intra-application via appels de fonctions directs
  • Base de données unique partagée entre tous les modules

Organisation backend (backend/app/)

backend/app/
├── main.py                 # Point d'entrée FastAPI
├── core/                   # Configuration, logging, database
│   ├── config.py
│   ├── database.py
│   └── logging.py
├── models/                 # Modèles SQLAlchemy (un fichier par entité)
│   ├── item.py
│   ├── location.py
│   ├── category.py
│   └── document.py
├── schemas/                # Schémas Pydantic (validation API)
│   ├── item.py
│   ├── location.py
│   └── ...
├── routers/                # Endpoints API (un fichier par ressource)
│   ├── items.py
│   ├── locations.py
│   ├── categories.py
│   ├── documents.py
│   └── search.py
├── services/               # Logique métier (orchestration)
│   ├── item_service.py
│   ├── location_service.py
│   └── ...
├── repositories/           # Accès données (abstraction BDD)
│   ├── item_repository.py
│   ├── location_repository.py
│   └── ...
└── utils/                  # Utilitaires transverses
    ├── files.py
    └── search.py

Organisation frontend (frontend/src/)

frontend/src/
├── main.tsx               # Point d'entrée React
├── App.tsx                # Composant racine + routing
├── components/            # Composants réutilisables
│   ├── common/            # Boutons, inputs, modales...
│   ├── items/             # Composants spécifiques items
│   └── locations/         # Composants spécifiques locations
├── pages/                 # Pages complètes (routes)
│   ├── Dashboard.tsx
│   ├── ItemList.tsx
│   ├── ItemDetail.tsx
│   └── ...
├── hooks/                 # Custom hooks React
│   ├── useItems.ts
│   ├── useLocations.ts
│   └── ...
├── api/                   # Client API (fetch/axios)
│   ├── items.ts
│   ├── locations.ts
│   └── ...
└── utils/                 # Utilitaires helpers
    └── formatting.ts

Principes d'organisation

  1. Séparation en couches backend : routers → services → repositories → models
  2. Dépendances unidirectionnelles : Les couches supérieures dépendent des inférieures uniquement
  3. Modules auto-contenus : Chaque module (items, locations, etc.) a ses propres fichiers dans chaque couche
  4. Pas de dépendances circulaires entre modules
  5. Code partagé dans core/ et utils/

Alternatives considérées

1. Microservices

Description : Séparer l'application en plusieurs services indépendants (service items, service locations, service search, etc.)

Avantages :

  • Scalabilité horizontale indépendante par service
  • Isolation des pannes (un service down ≠ tout down)
  • Possibilité d'utiliser des technologies différentes par service
  • Déploiement indépendant de chaque service

Inconvénients :

  • Complexité opérationnelle énorme (orchestration, networking, monitoring)
  • Nécessite API Gateway, service discovery, circuit breakers
  • Communication réseau entre services (latence, sérialisations multiples)
  • Transactions distribuées complexes (saga pattern)
  • Debugging et traçabilité difficiles
  • Overhead infrastructure (plusieurs conteneurs, load balancers, etc.)

Verdict : Rejeté - Complexité totalement disproportionnée pour un projet mono-utilisateur avec ~1 req/seconde max. Les bénéfices de scalabilité ne s'appliquent pas au cas d'usage.

2. Monolithe non structuré (big ball of mud)

Description : Tout le code dans quelques gros fichiers sans organisation claire

Avantages :

  • Démarrage très rapide
  • Pas de contraintes architecturales

Inconvénients :

  • Maintenance cauchemardesque après quelques mois
  • Code fortement couplé, difficile à tester
  • Évolution impossible sans refonte complète
  • Onboarding difficile pour contributeurs futurs

Verdict : Rejeté - Dette technique garantie, inacceptable même pour un projet solo

3. Architecture hexagonale (ports & adapters)

Description : Isolation stricte du domaine métier avec ports (interfaces) et adapters (implémentations)

Avantages :

  • Isolation forte du domaine métier
  • Testabilité maximale (mocks faciles)
  • Changement de base de données transparent

Inconvénients :

  • Boilerplate important (interfaces, adapters, DTOs multiples)
  • Courbe d'apprentissage élevée
  • Overhead pour un projet simple comme HomeStock

Verdict : ⚠️ Rejeté mais inspirant - Trop complexe pour notre besoin, mais on garde l'idée de séparation en couches (repository pattern)

4. Monolithe avec modules faiblement couplés (notre choix)

Description : Un seul déploiement avec modules logiques bien séparés et communication via interfaces claires

Avantages :

  • Simplicité opérationnelle (un seul processus, un seul conteneur)
  • Transactions ACID simples (même base de données)
  • Pas de latence réseau entre modules
  • Refactoring et debugging faciles
  • Évolution progressive possible (extraction en microservices si vraiment nécessaire)
  • Maintenance réduite

Inconvénients :

  • ⚠️ Scalabilité horizontale limitée (non critique pour mono-utilisateur)
  • ⚠️ Restart complet nécessaire pour tout changement (acceptable)

Verdict : Choisi - Meilleur équilibre complexité/bénéfices pour notre contexte


Conséquences

Positives

  1. Déploiement simple : docker-compose up lance tout le système
  2. Transactions ACID : Toutes les opérations en base profitent des transactions SQLite
  3. Performance : Pas de latence réseau entre modules, appels de fonctions directs
  4. Debugging facile : Stack traces complètes, un seul processus à inspecter
  5. Refactoring sans risque : IDE et linters détectent les erreurs lors de renommages
  6. Tests simples : Tests d'intégration faciles (toute l'app dans le même process)
  7. Maintenance réduite : Un seul service à monitorer, logs centralisés naturellement

Négatives

  1. Scalabilité limitée : Impossible de scaler un module indépendamment (non pertinent pour mono-utilisateur)
  2. Couplage temporel : Si un module plante, tout plante (acceptable avec bonne gestion erreurs)
  3. Restart complet : Modification = restart de toute l'app (dev avec hot-reload, prod peu de déploiements)

Mitigations

  • Pour éviter le couplage fort : Respecter strictement les couches (routers → services → repositories)
  • Pour faciliter évolution future : Interfaces claires entre modules, pas de dépendances circulaires
  • Pour améliorer résilience : Gestion d'erreurs robuste, retry logic, circuit breaker patterns si nécessaire

Impacts techniques

Développement

  • Pattern 3-layer : Chaque feature touche 3 fichiers minimum (router, service, repository)
  • Imports Python : Imports absolus depuis app. pour éviter imports relatifs fragiles
  • Tests : Tests unitaires par couche + tests d'intégration API end-to-end

Déploiement

  • Docker : Un seul conteneur backend (+ conteneur frontend séparé pour SPA)
  • Scaling : Scale vertical uniquement (augmenter CPU/RAM du conteneur)
  • Rollback : Simple (redéploiement image Docker précédente)

Évolution future

Si le projet nécessite vraiment des microservices (multi-utilisateurs massif, équipes séparées) :

  1. Extraction progressive : Modules déjà bien séparés, extraction facilitée
  2. API interne → API externe : Appels de fonctions deviennent appels HTTP
  3. Base partagée → bases séparées : Migrations Alembic facilitent séparation BDD

Cette architecture n'empêche pas l'évolution, elle la retarde jusqu'à ce qu'elle soit vraiment nécessaire (principe YAGNI = You Ain't Gonna Need It).


Notes

Cette décision s'inscrit dans la philosophie "Start with a monolith" popularisée par Martin Fowler et confirmée par de nombreux retours d'expérience (Shopify, GitHub, Stack Overflow ont démarré en monolithe).

Pour HomeStock, projet mono-utilisateur avec ~1 req/sec max en pic, un monolithe modulaire est largement suffisant et restera probablement optimal même à long terme.

La complexité des microservices n'apporterait aucun bénéfice tangible et augmenterait drastiquement les coûts de développement et maintenance.

Principe appliqué : "Choose boring technology" - privilégier les architectures éprouvées et simples sur les architectures à la mode mais complexes.


Contributeurs : Gilles (décideur) + Claude Code (architecte)

Références :