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
- Séparation en couches backend : routers → services → repositories → models
- Dépendances unidirectionnelles : Les couches supérieures dépendent des inférieures uniquement
- Modules auto-contenus : Chaque module (items, locations, etc.) a ses propres fichiers dans chaque couche
- Pas de dépendances circulaires entre modules
- Code partagé dans
core/etutils/
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
- Déploiement simple :
docker-compose uplance tout le système - Transactions ACID : Toutes les opérations en base profitent des transactions SQLite
- Performance : Pas de latence réseau entre modules, appels de fonctions directs
- Debugging facile : Stack traces complètes, un seul processus à inspecter
- Refactoring sans risque : IDE et linters détectent les erreurs lors de renommages
- Tests simples : Tests d'intégration faciles (toute l'app dans le même process)
- Maintenance réduite : Un seul service à monitorer, logs centralisés naturellement
Négatives
- Scalabilité limitée : Impossible de scaler un module indépendamment (non pertinent pour mono-utilisateur)
- Couplage temporel : Si un module plante, tout plante (acceptable avec bonne gestion erreurs)
- 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) :
- Extraction progressive : Modules déjà bien séparés, extraction facilitée
- API interne → API externe : Appels de fonctions deviennent appels HTTP
- 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 :
- Martin Fowler - "Monolith First" : https://martinfowler.com/bliki/MonolithFirst.html
- Sam Newman - "Building Microservices" (recommande de démarrer en monolithe)