# Décisions Architecturales - Xpeditis
**Projet**: Xpeditis - Plateforme B2B SaaS maritime
**Format**: Architecture Decision Records (ADR)
**Dernière mise à jour**: 2025-12-22
---
## Index des décisions
| ID | Date | Titre | Status |
|----|------|-------|--------|
| ADR-001 | 2025-12-22 | Adoption de l'architecture hexagonale | ✅ Acceptée |
| ADR-002 | 2025-12-22 | Suppression dépendances NestJS du domain layer | 🟡 Proposée |
| ADR-003 | 2025-12-22 | Activation TypeScript strict mode frontend | 🟡 Proposée |
| ADR-004 | 2025-12-22 | Standardisation pattern data fetching (React Query) | 🟡 Proposée |
| ADR-005 | 2025-12-22 | Migration pagination client-side vers serveur | 🟡 Proposée |
**Légende des status**:
- ✅ Acceptée: Décision implémentée
- 🟡 Proposée: En attente d'implémentation
- ❌ Rejetée: Décision écartée
- 🔄 Superseded: Remplacée par une autre décision
---
## ADR-001: Adoption de l'architecture hexagonale
**Date**: 2025-12-22 (rétroactif - décision initiale du projet)
**Status**: ✅ Acceptée et implémentée
### Contexte
L'application Xpeditis nécessite:
- Une séparation claire entre la logique métier et les détails techniques
- La capacité de changer de framework sans réécrire le métier
- Une testabilité maximale du code domaine
- L'indépendance vis-à-vis des bases de données et APIs externes
### Décision
Adopter l'**architecture hexagonale (Ports & Adapters)** pour le backend NestJS avec:
**3 couches strictement séparées**:
1. **Domain**: Logique métier pure (zéro dépendance externe)
2. **Application**: Orchestration et points d'entrée (controllers, DTOs)
3. **Infrastructure**: Adapters externes (DB, cache, email, APIs)
**Règles de dépendance**:
- Infrastructure → Application → Domain
- Jamais l'inverse
- Les interfaces (ports) sont définies dans le domain
- Les implémentations (adapters) sont dans l'infrastructure
### Conséquences
**Positives**:
- ✅ Domaine métier testable sans framework
- ✅ Changement de DB/ORM sans impact sur le métier
- ✅ Code domaine réutilisable
- ✅ Séparation claire des responsabilités
- ✅ Facilite l'onboarding des nouveaux développeurs
**Négatives**:
- ⚠️ Plus de fichiers à créer (ports, adapters, mappers)
- ⚠️ Courbe d'apprentissage initiale
- ⚠️ Verbosité accrue (mappers Domain ↔ ORM, Domain ↔ DTO)
**Risques**:
- ⚠️ Tentation de violer les règles (imports directs, shortcuts)
- ⚠️ Over-engineering pour des features simples
### Implémentation
**Structure adoptée**:
```
apps/backend/src/
├── domain/ # 🔵 Cœur métier (aucune dépendance)
│ ├── entities/
│ ├── value-objects/
│ ├── services/
│ ├── ports/
│ └── exceptions/
├── application/ # 🔌 Controllers & Use Cases
│ ├── controllers/
│ ├── dto/
│ ├── services/
│ └── mappers/
└── infrastructure/ # 🏗️ Adapters externes
├── persistence/
├── cache/
├── email/
└── carriers/
```
**Validation**:
- Tous les modules respectent la structure
- 95% de conformité (1 violation identifiée - voir ADR-002)
- Pattern Repository implémenté avec 13 repositories
### Alternatives considérées
**1. Clean Architecture (Uncle Bob)**
- Rejetée: Trop de couches (4-5) pour notre complexité
- Architecture hexagonale est plus simple et suffisante
**2. MVC traditionnel**
- Rejetée: Pas de séparation domaine/infrastructure
- Logique métier mélangée avec framework
**3. Feature Modules seuls (NestJS standard)**
- Rejetée: Domaine couplé à NestJS
- Difficile à tester et réutiliser
### Références
- [Hexagonal Architecture - Alistair Cockburn](https://alistair.cockburn.us/hexagonal-architecture/)
- [docs/architecture.md](./architecture.md)
- [CLAUDE.md](../CLAUDE.md)
---
## ADR-002: Suppression dépendances NestJS du domain layer
**Date**: 2025-12-22
**Status**: 🟡 Proposée
### Contexte
**Violation identifiée** lors de l'audit d'architecture:
- Le fichier `domain/services/booking.service.ts` importe `@nestjs/common`
- Utilise les decorators `@Injectable()` et `@Inject()`
- Utilise `NotFoundException` (exception NestJS, pas métier)
**Impact actuel**:
- Couplage du domain layer avec le framework NestJS
- Impossible de tester le service sans `TestingModule`
- Violation du principe d'inversion de dépendance
### Décision
**Supprimer toutes les dépendances NestJS du domain layer**:
1. Retirer `@Injectable()`, `@Inject()` de `BookingService`
2. Créer exception domaine `RateQuoteNotFoundException`
3. Adapter l'injection dans `bookings.module.ts`
4. Simplifier les tests unitaires
### Implémentation proposée
**Étape 1**: Refactoring du service domaine
```typescript
// domain/services/booking.service.ts
// AVANT
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
@Injectable()
export class BookingService {
constructor(
@Inject(BOOKING_REPOSITORY)
private readonly bookingRepository: BookingRepository,
) {}
}
// APRÈS
import { RateQuoteNotFoundException } from '../exceptions';
export class BookingService {
constructor(
private readonly bookingRepository: BookingRepository,
private readonly rateQuoteRepository: RateQuoteRepository,
) {}
}
```
**Étape 2**: Nouvelle exception domaine
```typescript
// domain/exceptions/rate-quote-not-found.exception.ts
export class RateQuoteNotFoundException extends Error {
constructor(public readonly rateQuoteId: string) {
super(`Rate quote with id ${rateQuoteId} not found`);
this.name = 'RateQuoteNotFoundException';
}
}
```
**Étape 3**: Adapter le module application
```typescript
// application/bookings/bookings.module.ts
@Module({
providers: [
{
provide: BookingService,
useFactory: (bookingRepo, rateQuoteRepo) => {
return new BookingService(bookingRepo, rateQuoteRepo);
},
inject: [BOOKING_REPOSITORY, RATE_QUOTE_REPOSITORY],
},
],
})
```
### Conséquences
**Positives**:
- ✅ Conformité 100% architecture hexagonale
- ✅ Domain layer totalement indépendant du framework
- ✅ Tests plus simples et plus rapides
- ✅ Réutilisabilité du code domaine
**Négatives**:
- ⚠️ Légère verbosité dans la configuration des modules
- ⚠️ Besoin de mapper les exceptions (domaine → HTTP) dans application layer
**Risques**:
- ⚠️ **FAIBLE**: Risque de régression (tests doivent passer)
- ⚠️ **FAIBLE**: Impact sur les features dépendantes de BookingService
### Validation
**Critères d'acceptation**:
- [ ] ✅ Aucun import `@nestjs/*` dans `domain/`
- [ ] ✅ Tous les tests unitaires passent
- [ ] ✅ Tests d'intégration passent
- [ ] ✅ Tests E2E passent
- [ ] ✅ Pas de régression fonctionnelle
**Commande de vérification**:
```bash
# Vérifier l'absence d'imports NestJS dans domain
grep -r "from '@nestjs" apps/backend/src/domain/
# Résultat attendu: Aucun résultat
```
### Timeline
**Estimation**: 2-3 heures
- Refactoring: 1h
- Tests: 1h
- Review: 30min
### Références
- [docs/backend/cleanup-report.md](./backend/cleanup-report.md)
- [Hexagonal Architecture Principles](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)
---
## ADR-003: Activation TypeScript strict mode frontend
**Date**: 2025-12-22
**Status**: 🟡 Proposée
### Contexte
**Problème identifié**:
- `tsconfig.json` a `"strict": false`
- Permet les erreurs de type silencieuses
- Risque de bugs runtime (`undefined is not a function`, `Cannot read property of null`)
- Code non type-safe difficile à maintenir
**Exemples de bugs non détectés sans strict mode**:
```typescript
// ❌ Accepté sans strict mode
let user: User;
console.log(user.name); // user peut être undefined
function getBooking(id?: string) {
return bookings.find(b => b.id === id); // id peut être undefined
}
```
### Décision
**Activer TypeScript strict mode** dans `tsconfig.json`:
```json
{
"compilerOptions": {
"strict": true
}
}
```
Ou activer les flags individuellement:
```json
{
"compilerOptions": {
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
```
### Conséquences
**Positives**:
- ✅ Détection des bugs au build time (pas runtime)
- ✅ Meilleure autocomplétion IDE (IntelliSense)
- ✅ Refactoring plus sûr
- ✅ Documentation implicite via les types
- ✅ Réduction des erreurs en production
**Négatives**:
- ⚠️ Corrections TypeScript nécessaires (estimation: 50-70% des fichiers)
- ⚠️ Apprentissage des patterns de null safety
- ⚠️ Code plus verbeux (guards, optional chaining)
**Risques**:
- ⚠️ **FAIBLE**: Pas de risque fonctionnel (corrections statiques)
- ⚠️ **MOYEN**: Temps de correction estimé à 2-3 jours
### Implémentation
**Phase 1: Activation progressive**
```bash
# 1. Activer strict mode
# tsconfig.json: "strict": true
# 2. Lister toutes les erreurs
npm run type-check 2>&1 | tee typescript-errors.log
# 3. Trier par fichier/type d'erreur
cat typescript-errors.log | sort | uniq -c
```
**Phase 2: Patterns de correction**
**Pattern 1**: Null checks
```typescript
// AVANT (strict: false)
function BookingDetails({ booking }) {
return
{booking.customerName}
;
}
// APRÈS (strict: true)
interface BookingDetailsProps {
booking: Booking | null;
}
function BookingDetails({ booking }: BookingDetailsProps) {
if (!booking) return Loading...
;
return {booking.customerName}
;
}
```
**Pattern 2**: Optional chaining
```typescript
// AVANT
const name = user.organization.name;
// APRÈS
const name = user?.organization?.name;
```
**Pattern 3**: Nullish coalescing
```typescript
// AVANT
const limit = params.limit || 20;
// APRÈS
const limit = params.limit ?? 20; // Gère correctement 0
```
**Phase 3: Validation**
```bash
# Vérifier qu'il n'y a plus d'erreurs TypeScript
npm run type-check
# Résultat attendu: Found 0 errors
# Build production
npm run build
# Résultat attendu: Build successful
```
### Timeline
**Estimation**: 2-3 jours
- Jour 1: Activer strict mode + lister erreurs
- Jour 2: Corriger 70% des erreurs
- Jour 3: Corriger les 30% restants + validation
### Alternatives considérées
**1. Garder strict: false**
- Rejetée: Accumulation de dette technique
- Risque de bugs en production
**2. Activation progressive flag par flag**
- Possible mais plus long
- Préférer activation directe (plus rapide)
**3. Migration vers Zod/io-ts pour runtime validation**
- Complémentaire (pas alternative)
- Peut être ajouté après strict mode
### Références
- [TypeScript Strict Mode Guide](https://www.typescriptlang.org/tsconfig#strict)
- [docs/frontend/cleanup-report.md](./frontend/cleanup-report.md)
---
## ADR-004: Standardisation pattern data fetching (React Query)
**Date**: 2025-12-22
**Status**: 🟡 Proposée
### Contexte
**Problème**: 3 patterns différents utilisés dans le frontend
**Pattern 1**: React Query + API client (✅ Recommandé)
```typescript
const { data } = useQuery({
queryKey: ['dashboard', 'kpis'],
queryFn: () => getDashboardKpis(),
});
```
**Pattern 2**: Custom hook avec fetch direct (❌ Problématique)
```typescript
const response = await fetch(`/api/v1/bookings/search`, {
headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` },
});
```
**Pattern 3**: API client direct dans composant (⚠️ Acceptable)
```typescript
const { data } = useQuery({
queryKey: ['bookings'],
queryFn: () => listBookings({ page: 1, limit: 20 }),
});
```
**Problèmes identifiés**:
- Incohérence dans le codebase
- Token management en doublon
- Error handling différent partout
- Pas de cache centralisé
- Retry logic manquante
### Décision
**Standardiser sur Pattern 1**: **React Query + API client partout**
**Raisons**:
1. Token management centralisé (dans apiClient)
2. Error handling uniforme
3. Cache management automatique
4. Retry logic configurée
5. Type safety maximale
6. Optimistic updates possibles
### Implémentation
**Refactoring type**:
**AVANT** (useBookings.ts - Pattern 2):
```typescript
export function useBookings() {
const [bookings, setBookings] = useState([]);
const [loading, setLoading] = useState(false);
const searchBookings = async (filters) => {
setLoading(true);
const response = await fetch(`/api/v1/bookings/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
},
body: JSON.stringify(filters),
});
const data = await response.json();
setBookings(data);
setLoading(false);
};
return { bookings, loading, searchBookings };
}
```
**APRÈS** (useBookings.ts - Pattern 1):
```typescript
import { useQuery } from '@tanstack/react-query';
import { advancedSearchBookings } from '@/lib/api/bookings';
export function useBookings(filters: BookingFilters) {
return useQuery({
queryKey: ['bookings', 'search', filters],
queryFn: () => advancedSearchBookings(filters),
enabled: !!filters,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// Usage dans composant
const { data, isLoading, error } = useBookings(filters);
```
**Configuration centralisée**:
```typescript
// lib/providers/query-provider.tsx
export function QueryProvider({ children }) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 3,
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
});
return (
{children}
);
}
```
### Conséquences
**Positives**:
- ✅ Code plus maintenable
- ✅ Moins de bugs (error handling centralisé)
- ✅ Meilleures performances (cache)
- ✅ Meilleure UX (loading states, retry)
- ✅ Dev experience améliorée (React Query DevTools)
**Négatives**:
- ⚠️ Refactoring nécessaire (estimation: 5-6 fichiers)
- ⚠️ Dépendance à React Query (mais déjà présent)
### Timeline
**Estimation**: 1 jour
- Refactoring hooks: 4h
- Tests: 2h
- Documentation: 1h
### Fichiers à modifier
1. `src/hooks/useBookings.ts` (supprimer fetch direct)
2. `src/hooks/useCsvRateSearch.ts` (vérifier pattern)
3. `src/hooks/useNotifications.ts` (vérifier pattern)
4. Tout autre hook custom utilisant fetch direct
### Validation
**Checklist**:
- [ ] ✅ Aucun `fetch()` direct dans hooks/composants
- [ ] ✅ Tous les calls API passent par `@/lib/api/*`
- [ ] ✅ Tous les hooks utilisent `useQuery` ou `useMutation`
- [ ] ✅ Token management unifié (via apiClient)
**Commande de vérification**:
```bash
# Chercher les fetch directs
grep -r "fetch(" apps/frontend/src/ apps/frontend/app/ | grep -v "api/client.ts"
# Résultat attendu: Aucun résultat (sauf dans client.ts)
```
### Références
- [React Query Best Practices](https://tkdodo.eu/blog/practical-react-query)
- [docs/frontend/cleanup-report.md](./frontend/cleanup-report.md)
---
## ADR-005: Migration pagination client-side vers serveur
**Date**: 2025-12-22
**Status**: 🟡 Proposée
### Contexte
**Problème actuel**:
```typescript
// app/dashboard/bookings/page.tsx (ligne 29)
listCsvBookings({ page: 1, limit: 1000 }) // ❌ Charge 1000 bookings !
// Puis pagination client-side
const currentBookings = filteredBookings.slice(startIndex, endIndex);
```
**Impact**:
- ⚠️ Transfert ~500KB-1MB de données
- ⚠️ Temps de chargement initial: 2-3 secondes
- ⚠️ Non scalable (impossible avec 10,000+ bookings)
- ⚠️ UX dégradée (loading long)
### Décision
**Implémenter pagination côté serveur** avec:
1. Requêtes paginées (20 items par page)
2. Filtres appliqués côté serveur
3. Cache React Query pour navigation rapide
4. Smooth transitions avec `keepPreviousData`
### Implémentation
**AVANT**:
```typescript
const { data: csvBookings } = useQuery({
queryKey: ['csv-bookings'],
queryFn: () => listCsvBookings({ page: 1, limit: 1000 }), // ❌ Tout charger
});
// Filtrage client-side
const filteredBookings = bookings.filter(/* ... */);
// Pagination client-side
const currentBookings = filteredBookings.slice(startIndex, endIndex);
```
**APRÈS**:
```typescript
const { data: csvBookings } = useQuery({
queryKey: ['csv-bookings', currentPage, filters],
queryFn: () => listCsvBookings({
page: currentPage,
limit: 20, // ✅ Pagination serveur
...filters, // ✅ Filtres serveur
}),
keepPreviousData: true, // ✅ Smooth transition entre pages
});
// Plus besoin de pagination client-side
const currentBookings = csvBookings?.data || [];
const totalPages = csvBookings?.meta.totalPages || 1;
```
**Vérifier API backend**:
```typescript
// Backend: apps/backend/src/application/controllers/csv-bookings.controller.ts
@Get()
async listCsvBookings(
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
@Query() filters: CsvBookingFiltersDto,
): Promise> {
// ✅ L'API supporte déjà la pagination
return this.csvBookingService.findAll({ page, limit, filters });
}
```
### Conséquences
**Positives**:
- ✅ Temps de chargement: 2s → 300ms
- ✅ Taille transfert: 500KB → 20KB
- ✅ Scalable: Supporte millions de records
- ✅ Meilleure UX: Chargement instantané
- ✅ Cache efficace: Une page = une requête
**Négatives**:
- ⚠️ Navigation entre pages = requête réseau
- Mitigé par `keepPreviousData` (pas de flash de loading)
- Mitigé par cache React Query (navigation arrière instantanée)
**Risques**:
- ⚠️ **FAIBLE**: Backend doit supporter les filtres serveur
### Validation
**Critères de performance**:
- [ ] ✅ Temps de chargement initial < 500ms
- [ ] ✅ Navigation entre pages < 300ms
- [ ] ✅ Taille transfert < 50KB par page
- [ ] ✅ Pas de flash de loading (keepPreviousData)
**Tests**:
```bash
# Test avec 10,000 bookings en base
# Avant: ~3s loading
# Après: ~300ms loading
```
### Timeline
**Estimation**: 2-3 heures
- Modification frontend: 1h
- Vérification backend: 30min
- Tests: 1h
### Fichiers à modifier
1. `app/dashboard/bookings/page.tsx` (refactoring pagination)
2. `lib/api/csv-bookings.ts` (vérifier support filtres serveur)
3. Potentiellement: `hooks/useBookings.ts` (si utilisé ailleurs)
### Références
- [React Query Pagination Guide](https://tanstack.com/query/latest/docs/react/guides/paginated-queries)
- [docs/frontend/cleanup-report.md](./frontend/cleanup-report.md)
---
## Template pour nouvelles décisions
```markdown
## ADR-XXX: [Titre de la décision]
**Date**: YYYY-MM-DD
**Status**: 🟡 Proposée / ✅ Acceptée / ❌ Rejetée / 🔄 Superseded
### Contexte
[Décrire le problème ou la situation qui nécessite une décision]
### Décision
[Décrire la décision prise et pourquoi]
### Implémentation
[Décrire comment la décision sera implémentée]
### Conséquences
**Positives**:
- [Liste des avantages]
**Négatives**:
- [Liste des inconvénients]
**Risques**:
- [Liste des risques]
### Alternatives considérées
[Décrire les autres options envisagées et pourquoi elles ont été rejetées]
### Validation
[Décrire comment valider que la décision est correctement implémentée]
### Timeline
[Estimation du temps nécessaire]
### Références
[Liens vers documentation, articles, ADRs liés]
```
---
**Fin du document**
Pour toute question ou proposition de nouvelle décision, contacter l'équipe architecture.