RecommandéProjet en cours

HealthPack

Projet personnel — Ingénieur unique · September 2026 — Present

  • Java 25
  • Spring Boot 4
  • REST
  • GraphQL
  • gRPC
  • WebSocket
  • Apache Kafka
  • Kafka Streams
  • RabbitMQ
  • PostgreSQL
  • pgvector
  • Redis
  • MinIO / S3
  • Keycloak (OIDC)
  • Spring AI
  • Claude (Anthropic API)
  • HAPI FHIR R4
  • HL7 v2
  • Docker
  • Kubernetes
  • OpenTelemetry
  • Stripe

Une plateforme de coordination hospitalière que je suis en train de concevoir : dossiers patients, planification, consultations cliniques, résultats de laboratoire, prescriptions, documents et un assistant IA, pensés comme des services indépendants qui restent synchronisés en s'annonçant mutuellement ce qui s'est produit plutôt qu'en se sollicitant les uns les autres. La conception est terminée sur les 12 diagrammes ci-dessous ; aucune ligne de code n'a encore été écrite.

Le problème de fond

L'information d'un patient vit en morceaux. Ses rendez-vous sont dans un système, ses résultats d'analyses dans un autre, ses prescriptions dans un troisième, sa facturation dans un quatrième. Aucun ne communique vraiment avec les autres.

Les conséquences sont banales et constantes. Un médecin prescrit un médicament sans voir qu'il entre en conflit avec ce qu'un confrère a prescrit le mois dernier. Un résultat critique reste non lu pendant des heures. Un patient appelle l'accueil pour poser une question déjà répondue dans un document que personne ne retrouve. Le personnel ressaisit la même information dans quatre écrans différents.

Rien de tout cela n'est un problème technologique au sens excitant du terme. C'est un problème de coordination. L'information existe — elle n'arrive simplement jamais là où elle est nécessaire, au moment où elle l'est.

La solution

HealthPack est conçu comme quinze petits services indépendants plutôt qu'un seul grand système. Chacun possède une seule responsabilité et l'assume pleinement : l'un gère les dossiers patients, un autre les rendez-vous, un autre les résultats de laboratoire, un autre les prescriptions, un autre les documents, un autre l'assistant IA.

Ils restent synchronisés en s'annonçant mutuellement ce qui se passe. Quand un résultat de laboratoire est finalisé, le service de laboratoire l'annonce une seule fois, sur lab.result.finalized. Tout ce qui s'y intéresse — la chronologie médicale du patient, le système d'alerte du médecin, la facturation, l'index de recherche de l'assistant IA — entend la même annonce via Kafka et réagit indépendamment. Personne n'a besoin de se souvenir de prévenir qui que ce soit.

C'est toute l'idée : le système s'informe lui-même de ce qui s'est passé, et les bonnes actions se déclenchent automatiquement. Kafka porte les faits — ce qui s'est produit, rejouable, conservé — et RabbitMQ porte le travail — générer ce PDF, envoyer cette notification, un seul exécutant, une seule tentative, relancée en cas d'échec.

Les points durs

Les pannes arrivent, et le système doit y survivre. Dans un ensemble de quinze services indépendants, qu'un seul soit temporairement indisponible est normal, pas exceptionnel. HealthPack est conçu pour qu'une panne reste contenue : si le service de notification tombe, les rendez-vous continuent de se réserver — les rappels s'accumulent dans RabbitMQ et repartent dès son rétablissement. Rien n'est perdu.

Rien ne doit se produire deux fois. Une prescription ne doit jamais être émise deux fois parce qu'une requête réseau a été relancée. Chaque génération de document porte une clé d'idempotence, vérifiée avant tout traitement, pour que rejouer la même commande produise le même résultat que l'exécuter une seule fois.

La rapidité là où elle compte. Le tableau de bord d'un médecin agrège l'information de cinq services différents. Fait naïvement, cela représente des dizaines de requêtes séparées et une page lente. La couche GraphQL les regroupe en un seul appel gRPC par service plutôt qu'un par ligne de patient, si bien que la page se charge en un seul aller-retour.

La confidentialité est une contrainte de conception, pas une fonctionnalité. Le consentement est vérifié avant que l'assistant IA ne lise quoi que ce soit. Les données personnelles sont retirées avant que la moindre donnée ne quitte les serveurs de l'hôpital. Ce ne sont pas des ajouts — l'architecture les suppose dès le premier diagramme, qui est aussi le seul qui existe pour l'instant : rien n'est encore codé.

Dossiers patients

Une identité unique et vérifiée par patient, pour que chaque service parle bien de la même personne. Les champs sensibles sont chiffrés individuellement, pas seulement « la base de données est chiffrée ».

Rendez-vous

Les patients réservent en ligne et voient les disponibilités réelles. Une contrainte d'exclusion en base de données rend le double-réservation d'un même créneau structurellement impossible à enregistrer, pas seulement soigneusement vérifiée.

Dossier clinique

Les médecins consignent visites, diagnostics et mesures. Tout est horodaté, rattaché à la chronologie du patient, et codé selon ICD-10, SNOMED CT et LOINC — les référentiels réellement utilisés par les hôpitaux.

Résultats de laboratoire

Les résultats arrivent directement des automates de laboratoire au format hospitalier standard (HL7 v2). Un traitement en flux continu surveille chaque résultat pour détecter les valeurs dangereuses et alerte un médecin en quelques secondes, en contournant le mode ne pas déranger.

Prescriptions

Avant d'être émise, une prescription est vérifiée contre tout ce que le patient prend déjà. Une interaction dangereuse bloque la commande et en montre précisément la raison ; une dérogation est enregistrée de façon permanente avec la justification du médecin.

Documents

Comptes rendus de sortie, rapports de laboratoire, prescriptions et factures sont générés en PDF de qualité archivage, signés cryptographiquement pour que toute falsification soit détectable. La génération se fait en arrière-plan — personne n'attend devant un écran de chargement.

Assistant IA

Patients et personnel posent leurs questions en langage naturel. L'assistant ne répond qu'à partir des documents réels du patient, cite sa source pour chaque affirmation, et le dit clairement quand les documents ne contiennent pas la réponse.

Messagerie

Chat en temps réel entre patients et cliniciens, et avec l'assistant IA. Les réponses s'affichent mot après mot au lieu d'arriver d'un bloc après une longue attente.

Notifications

Rappels de rendez-vous, résultats disponibles et valeurs critiques, livrés par notification push, e-mail ou SMS selon le choix de chacun. Les alertes critiques empruntent un chemin séparé et plus rapide qui contourne les heures de silence.

Facturation

Les factures se génèrent automatiquement à la clôture d'une consultation, les paiements par carte passant par Stripe avec un outbox transactionnel gardant les deux synchronisés.

Piste d'audit

Chaque action de chacun est enregistrée dans un journal inviolable, chaîné par hachage. Modifier un enregistrement passé casse la chaîne et devient détectable — la réglementation l'exige, et la plupart des systèmes l'implémentent mal.

L'architecture, en diagrammes

Douze diagrammes, dans l'ordre où j'expliquerais réellement ce système à un autre ingénieur : la vue d'ensemble d'abord, puis les données que possède chaque service, puis ce que chaque service expose, puis six flux de requêtes retracés de bout en bout.

01

Architecture globale — flux complet du système

Chaque service, chaque protocole, chaque courtier, sur une seule page.

C'est le diagramme que j'ouvrirais en premier. Les clients ne parlent jamais qu'à edge-gateway, qui valide chaque JWT auprès de Keycloak avant de transmettre une requête — les appels REST vont directement à un service métier, GraphQL passe par le BFF, et une montée en WebSocket va vers chat-service. Rien en aval ne revérifie l'authentification ; la passerelle est le seul endroit qui le fait, et tout le reste fait confiance aux revendications qu'elle transmet.

Les flèches doubles épaisses représentent gRPC, et elles se regroupent autour de terminology-service pour une raison précise : ce service est sollicité à presque chaque écriture clinique (valider un code ICD-10, vérifier une interaction médicamenteuse, résoudre un code LOINC), c'est donc le seul endroit où un protocole lent se ferait sentir partout. Les flèches pointillées représentent Kafka et RabbitMQ, et le diagramme est en réalité un argument pour les garder séparés : Kafka diffuse un même événement vers cinq consommateurs différents (lab.result.finalized à lui seul alimente le clinique, la facturation, l'assistant IA et les abonnements GraphQL), tandis que RabbitMQ porte des commandes vers exactement un exécutant — un PDF à générer, une notification à envoyer.

Lu de haut en bas, les couches racontent leur propre histoire : une bordure qui ne fait que valider et router, une couche de composition qui n'existe que pour épargner des allers-retours au frontend, sept services métier qui possèdent chacun une seule base PostgreSQL et rien d'autre, un service gRPC interne unique derrière tous les autres, et trois exécutants asynchrones qu'aucun client n'appelle jamais directement.

02

Diagramme de classes — Identité et noyau clinique

Patient, Practitioner, Consent, Appointment, Encounter — les entités dont tout le reste dépend.

Tout dans HealthPack finit par pointer vers un Patient, c'est donc le modèle que j'ai conçu en premier. La relation qui mérite qu'on s'y arrête est Appointment "1" --> "0..1" Encounter : produces — un rendez-vous est une réservation, une consultation est la visite réelle, et ce sont délibérément deux enregistrements distincts. Un patient peut réserver un rendez-vous et ne jamais se présenter ; une admission sans rendez-vous peut produire une consultation sans réservation derrière elle. Fusionner les deux rendrait les absences et les admissions impossibles à représenter proprement.

Consent est directement rattaché à Patient plutôt qu'enfoui dans une table de paramètres, parce que l'assistant IA le vérifie sur le chemin critique de chaque question qu'il traite — isActiveFor(ConsentScope) est appelé avant qu'un seul document ne soit récupéré, pas après. Observation et Condition appartiennent toutes deux à Encounter par composition (le losange plein), ce qui signifie qu'elles ne peuvent pas survivre à la visite où elles ont été enregistrées — une mesure a toujours un horodatage et un contexte clinique, jamais isolée.

03

Diagramme de classes — Commandes, documents et facturation

Comment une commande de laboratoire, une prescription et une facture finissent toutes par produire le même type d'artefact.

Les trois flèches pointillées qui convergent vers DocumentJob sont l'objet de ce diagramme : une LabOrder, une Prescription et une Invoice sont des concepts cliniques ou financiers totalement distincts, mais tous terminent leur cycle de la même façon — en déclenchant un PDF. Modéliser explicitement cette convergence a permis d'écrire document-service une seule fois, de façon générique, plutôt que trois fois avec trois pipelines PDF subtilement différents.

DocumentJob porte une idempotencyKey pour une raison concrète : un consommateur RabbitMQ peut recevoir le même message deux fois (c'est le compromis que fait RabbitMQ — livraison au moins une fois, jamais au plus une fois), et la prescription d'un médecin ne doit jamais être générée, signée et stockée deux fois à cause d'un appel réseau relancé. Le compteur attempts du job et sa méthode markFailed(String) existent pour qu'une génération bloquée devienne une donnée visible, pas un silence.

04

Diagramme de classes — IA, chat, notifications et audit

Comment une réponse RAG, un message de chat et un enregistrement d'audit sont réellement structurés.

RagQuery conserve plus que la réponse — elle garde inputTokens et cacheReadTokens aux côtés de la List~Citation~, parce qu'une réponse RAG sans source n'est pas un fait qu'on montre à un clinicien, et un nombre de tokens sans indicateur de lecture de cache ne dit rien de l'efficacité réelle du cache de prompt en production. Chaque Citation porte une plage de pages, pas seulement un identifiant de document, pour que l'assistant puisse désigner le paragraphe exact qu'il cite.

AuditRecord est la seule classe de ce diagramme qui se référence elle-même : AuditRecord --> AuditRecord : prevHash chain. Le hash de chaque enregistrement est calculé sur ses propres champs plus le hash de l'enregistrement précédent, si bien que modifier une entrée passée — même un seul champ — casse tous les hashs suivants. verifyChain(AuditRecord prev) est la méthode qu'un auditeur appelle réellement, et elle confirme la chaîne ou indique précisément où elle s'est rompue.

05

Méthodes de service — Surface API synchrone

Chaque méthode REST et gRPC exposée par les huit services pilotés par requête, en une seule vue.

C'est moins un diagramme de classes qu'une carte de qui appelle qui de façon synchrone. Les trois flèches de dépendance ..> vers TerminologyService depuis ClinicalService, PharmacyService et le code lié à SchedulingService confirment la décision d'architecture suggérée par le diagramme global : les recherches terminologiques sont le seul appel synchrone véritablement transversal, ce qui explique précisément pourquoi ce service est exclusivement en gRPC plutôt qu'en REST.

BffGraphQlResolvers mérite une lecture attentive : batchLoadPatients(Set~UUID~) : CompletableFuture~Map~ est la fonction de lot du DataLoader, et sa signature explique à elle seule pourquoi la couche GraphQL évite les requêtes N+1 — les résolveurs demandent un patient à la fois, DataLoader regroupe les identifiants dans un seul cycle de la boucle d'événements, et cette unique méthode les récupère tous en un seul appel gRPC. onVitalsUpdated et onLabResultReady renvoient un Flux, pas une valeur — ce sont des abonnements GraphQL, alimentés par des consommateurs Kafka en dessous, pas par du sondage.

06

Méthodes de service — Services asynchrones et temps réel

Les services qu'aucun client n'appelle jamais directement — ils ne font que réagir à une file ou un flux.

Remarquez ce qui manque à chaque méthode ici : aucun createX ni getX n'est exposé à un client. DocumentService.onRenderCommand, NotificationService.onNotifyCommand et onCriticalValue, AiRagService.onDocumentTextExtracted — chaque point d'entrée commence par on, parce que chaque point d'entrée est un gestionnaire de message, pas un endpoint. Un client interroge le statut d'un job via une petite façade REST, mais ne peut jamais demander directement à ces services de faire quoi que ce soit ; il ne peut que le demander à une file.

ChatService ..> AiRagService : gRPC server-stream est le seul appel d'allure synchrone dans un diagramme par ailleurs asynchrone, et il est là parce qu'une réponse de chat doit paraître instantanée — la faire transiter par Kafka ajouterait un saut de file que l'utilisateur remarquerait en attendant l'apparition des tokens. AiRagService.evaluateGoldenSet() est une méthode sans appelant visible ici, délibérément : elle est invoquée par la CI, pas par le système en fonctionnement, pour noter la qualité des réponses face à un jeu fixe de questions-réponses avant qu'un changement ne parte en production.

07

Séquence — Lecture de la vue patient à 360°

Un chargement de tableau de bord, cinq services, un seul appel gRPC chacun.

Cette séquence justifie à elle seule l'association GraphQL/gRPC. Un praticien ouvre un tableau de bord qui nécessite démographie, consultations et résultats de laboratoire — trois services, potentiellement des dizaines de patients affichés en même temps. L'étape 10, « DataLoader regroupe tous les identifiants en un seul cycle de la boucle d'événements », est le moment décisif : plutôt que le schéma naïf N+1 (un appel par patient et par champ), chaque demande d'identifiant d'un résolveur est mise en file pour un seul cycle, puis vidée en un seul lot.

Le bloc par qui suit déclenche trois appels groupés en parallèle — gRPC batchGet vers patient-service, gRPC batchGetEncounters vers clinical-service, et un appel REST vers lab-service — et patient-service applique lui-même la même rigueur en interne : un MGET sur Redis d'abord, une requête PostgreSQL seulement pour ce qui n'était pas en cache. La note en bas de séquence énonce le gain sans détour : un appel gRPC par service, pas un par ligne de patient, quel que soit le nombre de patients affichés.

08

Séquence — Réservation de rendez-vous → rappel push

Un double-réservation impossible, et un rappel qui ne peut pas se perdre.

Deux modes de défaillance sont éliminés par conception, pas rattrapés après coup. Le SET slot-hold NX EX 120 sur Redis empêche deux personnes de se disputer le même créneau dans les secondes que prend le remplissage d'un formulaire, et l'INSERT PostgreSQL en dessous est protégé par une contrainte d'exclusion en base sur (practitioner_id, time_range) — même si deux requêtes franchissent toutes deux la vérification Redis, la base de données refuse elle-même la seconde écriture. La correction de la concurrence se trouve au seul endroit qui puisse réellement la garantir : la base de données, pas du code applicatif qui espère avoir vérifié à temps.

La seconde moitié met en œuvre le patron outbox : l'insertion du rendez-vous et celle de l'outbox se font dans la même transaction de base de données, si bien que « la réservation a réussi mais personne ne l'a su » n'est pas un état atteignable — un sondeur fonctionnant indépendamment de la requête récupère la ligne d'outbox et publie appointment.booked dès qu'il est prêt. Le rappel emprunte ensuite délibérément la voie lente : le mécanisme TTL plus échange de lettres mortes de RabbitMQ met en œuvre le délai de 24 heures, et si le fournisseur de push échoue, le message est renvoyé avec remise en file et un recul exponentiel avant d'atterrir dans une file de lettres mortes plutôt que de disparaître.

09

Séquence — Valeur de laboratoire critique → alerte temps réel

D'un message HL7 sur le fil à un téléphone qui vibre, en un seul saut de Kafka Streams.

C'est le flux qui résume vraiment tout le projet — c'est celui décrit dans la première phrase de présentation. Un automate de laboratoire parle HL7 v2, un format de 1989 que lab-service analyse avec HAPI HL7v2 et valide contre LOINC avant de persister quoi que ce soit. La décision intéressante intervient juste après : plutôt que de laisser lab-service lui-même juger de ce qui est critique, il publie le fait — lab.result.finalized — et une topologie Kafka Streams séparée, partitionnée par identifiant patient avec une déduplication par fenêtre glissante de 15 minutes, se charge du jugement. La détection est délibérément découplée de l'enregistrement, pour que la logique d'alerte puisse évoluer sans toucher au service propriétaire des données.

Quand une valeur bascule dans le critique, une seule publication sur lab.critical-value se diffuse vers trois consommateurs en parallèle : notification-service la route vers une file prioritaire qui contourne entièrement les heures de silence, chat-service pousse une trame STOMP directement vers toute session ouverte du médecin, et l'abonnement GraphQL met à jour son tableau de bord en direct. Trois canaux de livraison différents, un seul événement, pour que, quelle que soit la façon dont le médecin regarde le système à cet instant, il le voie.

10

Séquence — Génération de PDF → ingestion RAG

Un PDF de prescription signé devient quelque chose que l'assistant IA peut citer.

La vérification d'idempotence tout en haut est la première chose qui se produit, avant tout traitement réel : document-service recherche l'idempotencyKey reçue dans PostgreSQL et, si elle a déjà été traitée, acquitte le message sans rien faire d'autre. Cette seule vérification est ce qui permet à RabbitMQ de redistribuer la même commande de génération sans jamais produire deux PDF signés pour la même prescription.

Passé cette garde, le pipeline est en réalité deux pipelines mis bout à bout. Le premier génère, applique un profil d'archivage PDF/A, signe avec l'implémentation PAdES de BouncyCastle, et stocke l'objet dans MinIO sous son sha256. Le second ne démarre qu'une fois le premier pleinement validé : PDFBox extrait le texte du PDF qui vient d'être écrit, publie document.text-extracted, et c'est cet unique événement qui rend le document consultable par l'assistant de chat — découpé, vectorisé via Voyage AI, et inséré dans pgvector avec l'identifiant du patient en métadonnée, ce qui permet plus tard à une requête RAG de se limiter aux documents d'un seul patient et d'aucun autre.

11

Séquence — Conversation avec l'assistant IA

Le consentement vérifié avant qu'un seul document ne soit récupéré, puis les tokens diffusés à mesure qu'ils arrivent.

L'ordre des opérations ici concrétise tout l'argument de confidentialité : ai-service appelle hasConsent(patientId, AI_ASSIST) auprès d'identity-service avant de vectoriser la question, avant d'interroger pgvector, avant de lire le moindre mot du dossier du patient. Un consentement révoqué renvoie PERMISSION_DENIED à ce stade et la conversation s'arrête là — l'assistant n'avance jamais assez loin pour avoir quoi que ce soit à expurger.

Une fois le consentement validé, la récupération se limite à ce seul patient (topK 8, filtré par patientId) et le contexte récupéré est expurgé des données de santé identifiantes avant d'être intégré au prompt — le modèle ne voit jamais plus de détails identifiants que la réponse n'en exige. La note cache_control sur l'étape de construction du prompt compte autant pour le coût que pour la latence : le prompt système et le corpus de directives restent stables à travers toutes les questions posées par l'équipe soignante de ce patient, si bien que le cache de prompt d'Anthropic transforme la majeure partie de ce contexte en lecture de cache plutôt qu'en nouvelle facturation, et cacheReadTokens est journalisé précisément pour vérifier que c'est réellement le cas plutôt que de le supposer. À partir de là, la boucle est un relais direct — Claude diffuse des événements content_block_delta, ai-service en transmet chacun via un flux serveur gRPC, chat-service le transforme en trame STOMP, et le navigateur l'affiche progressivement, si bien qu'une réponse apparaît comme une personne qui tape plutôt que d'arriver d'un bloc après une attente.

12

Séquence — Prescription avec vérification d'interaction médicamenteuse

Un cache à deux niveaux devant la seule vérification qu'on ne peut pas se permettre de sauter.

La cascade de cache Caffeine puis Redis existe parce que validateCode est appelée à presque chaque écriture clinique dans tout le système, et une recherche terminologique qui atteindrait PostgreSQL à chaque fois ralentirait chacune de ces écritures sans raison — les référentiels de codes changent à peine, un cache L1 local au processus adossé à un cache L2 partagé est donc la solution évidente, et chaque défaut de cache alimente les deux niveaux au passage, si bien que la prochaine requête, n'importe où dans le cluster, en profite.

La vérification d'interaction elle-même est ce qui justifie ce diagramme : un résultat contre-indiqué ne fait pas échouer la requête, il renvoie un 422 avec le conflit précis, et le praticien peut passer outre — mais seulement en fournissant une justification qui est enregistrée avec la prescription et un indicateur explicite de dérogation. Rien n'est bloqué en silence et rien n'est autorisé en silence ; chaque prescription contestée laisse une trace de qui a dérogé, à quoi, et pourquoi. Les deux dernières publications en bas de séquence — prescription.issued vers Kafka, une commande de génération vers RabbitMQ — sont exactement le point où reprend le diagramme 4d.

Approfondissement technique

Le logiciel hospitalier est un problème de systèmes distribués authentiquement difficile, déguisé en sujet ennuyeux. Il impose des exigences de cohérence strictes (on ne peut pas doublement réserver un bloc opératoire), des exigences de latence strictes (une valeur de potassium critique ne vaut rien une heure trop tard), des exigences de conformité strictes (chaque lecture d'un dossier patient doit pouvoir être auditée par la loi), une surface d'intégration extrêmement hétérogène (du HL7 v2 datant de 1989 côtoyant du FHIR en REST), et des données de santé qui rendent chaque décision de flux de données lourde de conséquences.

Je l'ai choisi délibérément. Mon travail précédent — un portail Drupal/Next.js chez Atos, une plateforme de livraison Spring Boot chez OpenTecc — était solide mais architecturalement singulier. « Microservices » et « Kafka » étaient des concepts que je comprenais, pas des artefacts que je pouvais montrer. Ce projet existe pour rendre cette affirmation démontrable, et pour construire quelque chose qui pourrait réellement aider un hôpital à fonctionner plus sûrement, pas seulement pour étoffer une liste de technologies.

Pourquoi un monolithe n'aurait pas fonctionné

Quatre propriétés du domaine imposent cette scission :

1

Une charge radicalement asymétrique

Les recherches terminologiques s'exécutent à presque chaque écriture clinique. La facturation s'exécute une fois par consultation. Dans un monolithe, elles partagent un pool de threads et un tas mémoire ; une boucle terminologique intensive dégrade la génération de factures sans raison.

2

Des budgets de latence radicalement asymétriques

Une alerte de valeur critique a un engagement de service de l'ordre de la seconde. La génération d'un PDF peut prendre trente secondes sans que cela pose problème. Les placer sur le même chemin de requête fait gagner partout le budget le plus strict.

3

Des domaines de défaillance indépendants

Une panne de Stripe ne doit pas empêcher un médecin d'enregistrer une consultation. Dans un monolithe, la portée de transaction et les pools de connexions partagés rendent cette isolation très difficile à obtenir réellement.

4

Un rayon d'exposition réglementaire

Les services touchant aux données de santé ont besoin de chiffrement, de contrôles de consentement et d'audit. Ceux qui n'y touchent pas ne devraient pas en payer le coût. Un monolithe place par défaut chaque composant sous ce même périmètre réglementaire.

Stratégie de protocoles

Trois protocoles de transport, chacun justifiable en une phrase.

RESTToute API externe ou traversant une frontière de service. Cachable, débogable au curl, natif du navigateur, et le bon choix par défaut. FHIR R4 est lui-même structuré en REST, si bien que l'interopérabilité hospitalière vient sans effort supplémentaire.

GraphQLExactement un service, le BFF. La vue patient à 360° du frontend a besoin de données de cinq services, avec une forme dictée par le client. Faire cela en REST, c'est soit cinq allers-retours, soit un endpoint d'agrégation sur mesure par écran — Spring for GraphQL, schéma en amont, avec autorisation au niveau des champs et limites de profondeur et de complexité des requêtes.

gRPCExclusivement entre services internes, concentré sur terminology-service. Des charges utiles inférieures au kilo-octet où l'enrobage JSON représente une part importante du message, appelé à presque chaque écriture clinique, avec un schéma qui ne change réellement jamais de forme. L'association DataLoader/gRPC dans le BFF est la conception dont je suis le plus fier : les résolveurs semblent aller chercher les données champ par champ, mais DataLoader regroupe chaque identifiant en un seul cycle de la boucle d'événements et répartit l'appel en un seul gRPC par service — voir le diagramme de la vue patient à 360° ci-dessus pour le détail exact de ce mécanisme.