---
{
  "title": "Au-delà de Patient/$merge : une fusion générique et pilotée par le client pour FHIR",
  "description": "Le Patient/$merge de FHIR R5 est un début, mais la MDM en production exige davantage. Nous avons conçu un $merge générique avec des plans pilotés par le client, des pistes d'audit atomiques et une opération $referencing universelle.",
  "date": "2026-04-09",
  "author": "Ivan Shukshin",
  "reading-time": "8 min read",
  "tags": ["FHIR Standard", "Aidbox", "System Design", "Integrations", "MDMbox"],
  "tldr": "Nous avons étendu le $merge FHIR pour accepter un Bundle de transaction fourni par le client comme paramètre plan. Le client décide de ce qu'il faut mettre à jour, créer ou supprimer — le serveur garantit l'atomicité et crée des ressources d'audit Task + Provenance dans la même transaction. Combiné à une nouvelle opération $referencing pour l'inclusion inverse générique, cela vous offre une fusion entièrement auditable et indépendante du type de ressource, qui fonctionne pour Patient, Organization, Practitioner ou n'importe quelle autre ressource."
}
---
Nous faisons tourner la gestion des données de référence (Master Data Management) dans [MDMbox](https://www.health-samurai.io/mdmbox) au sein de déploiements en production depuis deux ans — dédoublonnage de patients, fusion d'organisations, réconciliation de professionnels de santé entre établissements. Au fil du temps, nous avons constaté que chaque organisation fusionne différemment, et qu'aucun algorithme côté serveur ne peut couvrir la variété des politiques de fusion du monde réel.

[FHIR R5](/articles/fhir-r4-vs-fhir-r5-choosing-the-right-version-for-your-implementation) a introduit `Patient/$merge` — une opération très attendue qui permet de fusionner des dossiers patients en double. Elle définit des paramètres d'entrée (`source-patient`, `target-patient`, `result-patient`), attend du serveur qu'il gère les mises à jour de références, et retourne le résultat fusionné. L'opération est actuellement au niveau de maturité 0 et n'a pas connu de développement supplémentaire depuis son introduction.

C'est un bon début. Mais après avoir mis en œuvre la fusion en production, nous avons constaté que la spécification ne va pas assez loin.

## Le problème avec la fusion pilotée par le serveur

La spécification FHIR `$merge` suppose que le serveur sait *comment* fusionner. Le serveur désactive la source, copie les identifiants, met à jour les références. Mais la logique de fusion varie énormément selon les organisations :

- **L'hôpital A** supprime entièrement le patient source et réécrit toutes les références
- **L'hôpital B** conserve la source inactive et crée une ressource Linkage pour la traçabilité
- **L'hôpital C** fusionne les Encounters et les Observations dans la cible, mais conserve des enregistrements AllergyIntolerance séparés pour révision manuelle
- **Le réseau d'échange d'informations de santé D** doit fusionner des Organizations et des Practitioners, pas seulement des Patients

La spécification ne couvre rien de tout cela — elle ne s'applique qu'aux Patients et l'étape « mettre à jour toutes les références » est traitée de façon expéditive.

![Merge flow](merge-flow.svg)

Ce dont nous avions besoin, c'était une opération de fusion où :
- Le **client** décide de ce qui se passe (quelles ressources mettre à jour, créer ou supprimer)
- Le **serveur** garantit l'atomicité, crée une piste d'audit et applique les vérifications de sécurité
- Elle fonctionne pour **tout type de ressource**, pas seulement Patient

## Notre approche : le paramètre plan

Nous avons conçu un `$merge` compatible FHIR qui accepte un Bundle de transaction standard comme paramètre `plan` supplémentaire :

```json
POST $merge

{
  "resourceType": "Parameters",
  "parameter": [
    {"name": "source", "valueReference": {"reference": "Patient/123"}},
    {"name": "target", "valueReference": {"reference": "Patient/456"}},
    {"name": "preview", "valueBoolean": false},
    {
      "name": "plan",
      "resource": {
        "resourceType": "Bundle",
        "type": "transaction",
        "entry": [
          {
            "resource": {"resourceType": "Patient", "id": "456", "...": "merged state"},
            "request": {"method": "PUT", "url": "Patient/456", "ifMatch": "W/\"3\""}
          },
          {
            "resource": {"resourceType": "Encounter", "id": "789",
                         "subject": {"reference": "Patient/456"}},
            "request": {"method": "PUT", "url": "Encounter/789", "ifMatch": "W/\"1\""}
          },
          {
            "request": {"method": "DELETE", "url": "Patient/123", "ifMatch": "W/\"2\""}
          }
        ]
      }
    }
  ]
}
```

Le client envoie explicitement chaque PUT, POST et DELETE. Le serveur enveloppe le plan avec des ressources d'audit (Task + Provenance) et exécute l'ensemble comme une seule transaction FHIR. Soit tout réussit — y compris la piste d'audit — soit rien ne se produit.

Remarquez les en-têtes `ifMatch` sur chaque entrée — c'est du verrouillage optimiste. Si une ressource a été modifiée entre le moment où le client a construit le plan et celui où il est exécuté, la transaction échoue. Aucun écrasement silencieux, aucune mise à jour perdue.

## Un sur-ensemble, pas un dérivé

Vous remarquerez que nous utilisons `source`, `target` et `result` plutôt que les paramètres `source-patient`, `target-patient` et `result-patient` de la spécification. C'est intentionnel — l'opération est indépendante du type de ressource, et une dénomination propre aux Patients serait trompeuse. Fusion d'Organizations en double ? De Practitioners ? Même opération, même piste d'audit.

Omettez le paramètre `plan` et passez `result` à la place — vous obtenez le comportement standard de FHIR `$merge`. La recherche par identifiant pour la source et la cible est prise en charge. Le paramètre `plan` est ce qui le rend strictement plus puissant : lorsqu'il est présent, le client prend le contrôle total de la logique de fusion.

## La pièce manquante : $referencing

Pour construire un bon plan de fusion, le client doit répondre à une question : *« Quelles ressources référencent Patient/123 ? »*

FHIR dispose de `_revinclude` pour les résultats de recherche et de `$everything` pour Patient, mais il n'existe pas d'opération générique qui dise « donne-moi toutes les ressources de la base de données qui pointent vers cette référence ». Nous en avons conçu une.

![Before and after merge](before-after.svg)

Sous le capot, il s'agit d'une requête PostgreSQL utilisant JSONPath pour effectuer une recherche dans les tables de ressources :

```sql
SELECT id, resource_type, resource
FROM <resource_table>
WHERE jsonb_path_query_array(resource, '$.** ? (@.reference == "Patient/123")')
      != '[]'::jsonb
```

La requête ci-dessus est simplifiée pour la clarté, mais l'idée centrale demeure : `$.**` parcourt récursivement l'ensemble de la structure de la ressource, trouvant tout objet imbriqué où `reference` est égal à la cible — indépendamment de l'endroit où il apparaît dans l'arbre de la ressource. Aucun chemin codé en dur, aucune configuration par type de ressource. Une seule requête trouve les références dans `Encounter.subject`, `Observation.performer`, `Claim.provider`, ou tout autre champ de référence.

**Sur les performances :** cette requête utilise un index GIN sur la colonne JSONB, de sorte que le parcours récursif s'effectue sur l'index — et non par balayage complet de la table. Pour une fusion typique, l'opération se termine en quelques millisecondes. Pour les types de ressources comportant des millions de lignes, nous parallélisons la recherche entre les tables et transmettons les résultats au client en flux continu.

Nous avons exposé cela comme une opération `$referencing` autonome dans [MDMbox](https://www.health-samurai.io/mdmbox) — un bloc de construction que tout flux de fusion (ou d'analyse d'impact) peut utiliser.

## Task + Provenance : bien plus que de l'audit

Chaque fusion crée une Task et une Provenance dans la même transaction. C'est bien plus qu'une exigence de conformité :

![Audit trail](audit-trail.svg)

**Abonnements.** Les systèmes externes peuvent s'abonner à la création de Task de fusion et réagir immédiatement — mettre à jour leurs caches, déclencher des flux de travail en aval, notifier les utilisateurs. Des abonnements FHIR standards, sans plomberie personnalisée.

**Instantanés versionnés.** La Provenance enregistre une référence versionnée (p. ex. `Patient/456/_history/3`) pour chaque ressource modifiée ou supprimée par la fusion. Combiné à l'API History de FHIR, cela signifie que l'état complet avant la fusion est toujours récupérable.

**Suivi du cycle de vie.** La Task enregistre le statut de la fusion (`requested` → `in-progress` → `completed` ou `failed`), qui l'a initiée, quand elle s'est produite, et les liens vers la source et la cible. La requête « montrez-moi toutes les fusions qui ont touché ce patient » se résume à une seule recherche FHIR sur Task.

## La prochaine étape : $unmerge

FHIR ne définit pas encore d'opération de défusion. Mais considérez ce scénario : une fusion est exécutée un lundi, et le mercredi quelqu'un réalise que les deux patients étaient en réalité des personnes différentes. Entre-temps, 50 ressources ont été modifiées — des Encounters redirigés, des Observations réassignées, le Patient source supprimé.

Avec la Task qui suit le cycle de vie de la fusion, la Provenance qui capture chaque ressource affectée avec sa version d'avant la fusion, et l'API History qui préserve tous les états antérieurs — nous disposons de tout ce qu'il faut pour inverser une fusion de façon fiable. L'opération `$unmerge` lit la Provenance, récupère les versions d'avant la fusion depuis l'historique, et construit un Bundle de transaction inverse qui restaure chaque ressource à son état antérieur.

Nous couvrirons `$unmerge` dans le prochain article — maintenant publié : [Concevoir $unmerge : inverser la fusion de patients FHIR](/articles/designing-unmerge-fhir-patient). Le deuxième article de la série, [Les fournisseurs MPI n'ont pas pu s'entendre. Nous avons résolu la fusion de patients](/articles/mpi-vendors-patient-merge), explique pourquoi un seul `$merge` piloté par le serveur ne peut pas répondre à toutes les politiques de production. Pour les bases du Master Patient Index sous-jacent, consultez [Master Patient Index (MPI) : fonctionnement et exemples](/articles/master-patient-index-and-record-linkage).

---

*Vous souhaitez essayer la fusion pilotée par le client dans votre projet ? [MDMbox](https://www.health-samurai.io/mdmbox) est disponible dès aujourd'hui.*