Architecture hexagonale
l'approche hexagonale est confirmé comme le bon choix pour ce projet. L'argument qui a fait mouche : l'intégration IoT devient triviale. Chaque technologie (broker MQTT, PostgreSQL, Redis, bases de données et iot existantent) n'est qu'un adaptateur branché sur un port. Besoin d'intégrer la base de données parking existante d'une autre ville ? On ajoute un adaptateur. Besoin de remplacer EMQX par un autre broker MQTT ? On change l'adaptateur, la logique métier reste intacte.
Gestion de la charge serveur : La discussion qui m'a fait creuser plus loin
Le jury m'a poussé sur comment le système gère les pics de charge. Mon rapport mentionnait le problème (500+ trames capteurs/seconde, requêtes citoyens aux heures de pointe), mais le jury voulait voir la chaîne complète de défense :
Couche 1 - Message Broker (EMQX) : Les capteurs ne tapent pas le backend directement. Ils publient sur des topics MQTT. EMQX met les messages en file d'attente. Si le backend est lent ou redémarre, les messages attendent - rien n'est perdu. Le broker est l'amortisseur entre le trafic IoT imprévisible et la capacité de traitement finie.
Couche 2 - Cache (Redis) : Le statut d'une place de parking est lu des milliers de fois par seconde par les citoyens qui vérifient la disponibilité. Mais il ne change que lorsqu'un capteur envoie un signal. Redis sert les lectures en <1ms. La base de données n'est sollicitée que pour les écritures. À lui seul, ce mécanisme élimine plus de 90% de la charge sur la BDD.
Couche 3 - Auto-scaling (Vertical + Horizontal) : Quand Redis et le broker ne suffisent plus (rush du vendredi soir, événement à proximité), l'auto-scaling entre en jeu. D'abord vertical (conteneurs plus puissants), puis horizontal (plus d'instances NestJS derrière un load balancer). Docker Compose pour le dev, avec un chemin clair vers l'orchestration si nécessaire.
Capteurs -> [Broker EMQX] -> Backend (NestJS) -> [Cache Redis] -> Clients
| | |
Absorbe les pics Scale out Sert les lectures
Le message du jury était clair : chaque couche résout un problème différent. Le broker gère les pics d'ingestion, le cache gère l'amplification des lectures, et l'auto-scaling gère la croissance soutenue. On a besoin des trois.
Des décisions aux tests : le tableau de scénarios TDD
Domaine 1 - Gestion du stationnement
| ID | Exigence source | Type | Catégorie | Scénario | Données d'entrée | Résultat attendu | Critère d'échec | Priorité |
|---|---|---|---|---|---|---|---|---|
| TST-PARK-001 | Occupation temps réel | Unitaire | Nominal | Étant donné un capteur signalant une place libre, quand l'événement est reçu, alors le statut est mis à jour en BDD + Redis en <100ms | { placeId: "P-042", status: "FREE" } |
Statut mis à jour PostgreSQL + Redis <100ms | Incohérence entre Redis et BDD, ou latence >100ms | Critique |
| TST-PARK-002 | Cartographie par zone | Intégration | Nominal | Étant donné 3 zones à 80%, 20%, 100% d'occupation, quand le superviseur charge la carte, alors les taux correspondent aux données capteurs agrégées | Zone A: 40/50, B: 10/50, C: 50/50 | Taux affichés = 80%, 20%, 100% | Écart >1% avec les données réelles | Haute |
| TST-PARK-003 | Réservation à l'avance | Unitaire | Nominal | Étant donné une place libre P-042, quand un usager réserve 14h-16h, alors le statut passe à RESERVED et la place est exclue du guidage | { placeId: "P-042", from: "14:00", to: "16:00" } |
Statut = RESERVED, exclue du guidage | Place encore proposée au guidage après réservation | Haute |
| TST-PARK-004 | Double réservation | Unitaire | Erreur | Étant donné P-042 déjà réservée 14h-16h, quand un 2e usager tente le même créneau, alors 409 Conflict retourné | { placeId: "P-042", from: "14:30", to: "15:30" } |
HTTP 409, aucune modification en BDD | Réservation acceptée (double booking) | Critique |
| TST-PARK-005 | Guidage dynamique | Intégration | Nominal | Étant donné un usager géolocalisé à l'entrée, quand il demande un guidage, alors la place libre la plus proche est retournée via PostGIS | GPS usager + état temps réel des places | Place la plus proche en distance géospatiale | Place retournée occupée ou pas la plus proche | Haute |
| TST-PARK-006 | Filtrage places PMR | Unitaire | Limite | Étant donné que seules des places PMR sont libres, quand un usager standard demande un guidage, alors les places PMR ne sont pas proposées | 50/50 standard occupées, 3/5 PMR libres | Réponse : "aucune place disponible" | Places PMR proposées à un usager non-PMR | Haute |
| TST-PARK-007 | Historisation | Unitaire | Nominal | Étant donné un changement de statut sur P-042, quand l'événement est traité, alors un enregistrement horodaté est inséré dans TimescaleDB | Transition FREE→OCCUPIED | Ligne insérée dans hypertable avec timestamp | Pas d'insertion ou timestamp manquant | Moyenne |
Domaine 2 - Gestion des capteurs IoT
| ID | Exigence source | Type | Catégorie | Scénario | Données d'entrée | Résultat attendu | Critère d'échec | Priorité |
|---|---|---|---|---|---|---|---|---|
| TST-IOT-001 | Réception trames MQTT | Intégration | Nominal |
Étant donné un capteur magnétique actif, quand il publie sur parking/sensors/{id}, alors le backend consomme et persiste en <200ms |
{ sensorId: "S-101", type: "magnetic", value: 1, battery: 85 } |
Donnée persistée <200ms, statut place mis à jour | Message perdu ou non consommé après 5s | Critique |
| TST-IOT-002 | Détection signal perdu | Unitaire | Erreur | Étant donné un capteur silencieux depuis >15min (seuil configuré), quand le cron de supervision s'exécute, alors alerte SIGNAL_LOST créée |
lastSeen: now() - 20min, seuil: 15min |
Alerte créée : type=SIGNAL_LOST | Aucune alerte malgré dépassement du seuil | Critique |
| TST-IOT-003 | Détection batterie faible | Unitaire | Limite | Étant donné un capteur à 10% de batterie (seuil critique = 15%), quand la trame est reçue, alors alerte BATTERY_LOW générée | { sensorId: "S-101", battery: 10 } |
Alerte BATTERY_LOW créée | Pas d'alerte, capteur reste en état normal | Haute |
| TST-IOT-004 | Valeurs aberrantes | Unitaire | Erreur | Étant donné un capteur envoyant des valeurs hors range (battery: -5), quand la trame est parsée, alors rejetée et loguée comme anomalie | { sensorId: "S-101", battery: -5, value: 999 } |
Trame rejetée, log d'anomalie, aucune mise à jour | Valeur aberrante persistée | Haute |
| TST-IOT-005 | Dashboard santé capteurs | E2E | Nominal | Étant donné 100 capteurs actifs, quand le superviseur consulte le dashboard santé, alors uptime, dernière communication, batterie affichés | 100 capteurs avec états variés | Tableau complet, filtrable par état, trié par criticité | Capteurs manquants ou données périmées | Haute |
| TST-IOT-006 | Débit broker MQTT | Intégration | Performance | Étant donné 500 capteurs envoyant 1 trame/s simultanément, quand EMQX reçoit le flux, alors zéro perte, latence <500ms | 500 trames/s pendant 60s | 0 message perdu, P99 <500ms | Perte >0.1% ou P99 >500ms | Critique |
Domaine 3 - Alertes & interventions
| ID | Exigence source | Type | Catégorie | Scénario | Données d'entrée | Résultat attendu | Critère d'échec | Priorité |
|---|---|---|---|---|---|---|---|---|
| TST-ALR-001 | Alertes automatiques | Unitaire | Nominal | Étant donné la règle "zone A >90% pendant >10min", quand le seuil est dépassé, alors alerte priorité HAUTE créée | Zone A à 92% depuis 12min | Alerte : type=OCCUPATION_HIGH, priority=HIGH | Aucune alerte ou mauvaise priorité | Critique |
| TST-ALR-002 | Dispatch GPS-optimisé | Intégration | Nominal | Étant donné 3 agents à 200m, 1.2km, 800m, quand une intervention est créée, alors assignée à l'agent le plus proche disponible | GPS agents + localisation intervention | Agent à 200m reçoit l'assignation <30s | Assignation à un agent plus éloigné | Haute |
| TST-ALR-003 | Escalade automatique | Unitaire | Limite | Étant donné une intervention assignée depuis 30min (SLA=25min) non traitée, quand le cron d'escalade s'exécute, alors escaladée au superviseur | Status=ASSIGNED, age=30min, SLA=25min | Statut→ESCALATED, superviseur notifié | Reste en ASSIGNED sans escalade | Haute |
| TST-ALR-004 | Cycle de vie complet | E2E | Nominal | Étant donné une alerte capteur HS, quand l'agent la traite (accepte→en cours→photo→clôture), alors chaque transition est horodatée | Intervention parcourant les 4 états | Timestamps pour chaque transition, final=CLOSED | Transition manquante ou état final incorrect | Haute |
| TST-ALR-005 | Gestion urgence | Unitaire | Erreur | Étant donné une alerte VANDALISME, quand elle est créée, alors bypass la file normale et notifie immédiatement tous les superviseurs | { type: "VANDALISM", priority: "CRITICAL" } |
Notification <5s, pas de mise en file | Délai >5s ou notification manquante | Critique |
| TST-ALR-006 | Agent hors ligne | Unitaire | Erreur | Étant donné que l'agent le plus proche est hors ligne (4G coupée), quand le dispatch tente l'assignation, alors bascule sur le 2e agent | Agent 1: offline, Agent 2: online à 800m | Assignation à l'agent 2, log échec agent 1 | Intervention reste non assignée | Haute |
Ce que je ferais différemment : réflexions post-soutenance
Après l'échange avec le jury, plusieurs choses sont devenues plus claires et méritent d'être intégrées :
1. La gestion de charge doit apparaître dans le diagramme d'architecture, pas seulement dans le texte
J'ai discuté de l'auto-scaling, du cache et du message broker dans la section "problématiques" de mon rapport, mais le diagramme d'architecture lui-même ne montrait pas ces couches. Le jury avait raison d'insister. Si le broker est votre première ligne de défense contre les flux IoT, il doit être un composant visible dans l'architecture, pas une note de bas de page.
2. Les compromis de monitoring méritent un tableau comparatif
Mentionner deux solutions de monitoring sans explication structuré, c'est une occasion manquée. Leçon retenue : si on mentionne une alternative, on doit au lecteur une comparaison structurée.
Top comments (0)