---
{
  "title": "Design von $unmerge: Rückgängigmachen des FHIR-Patient-Merge",
  "description": "FHIR verfügt über keine $unmerge-Operation. Wir haben eine entwickelt, die einen Merge mithilfe der Provenance- und History-API rückgängig macht und dem Client die Entscheidung überlässt, was mit Daten geschehen soll, die zwischen Merge und Unmerge hinzugefügt wurden.",
  "date": "2026-05-22",
  "author": "Ivan Shukshin",
  "reading-time": "15 min read",
  "tags": ["FHIR Standard", "Data Quality", "Analytics", "Master Data Management", "Integrations", "MDMbox"],
  "tldr": "$unmerge spiegelt die Schnittstelle von $merge: Der Server übernimmt die Invarianten (Atomarität, Audit, Lebenszyklus), der Client übernimmt die Policy. Der einfache Teil ist die Wiederherstellung des Zustands vor dem Merge: Task, Provenance und die History API liefern alles, was wir zur Konstruktion eines umgekehrten Transaktions-Bundles benötigen. Der schwierige Teil ist die zeitliche Lücke: Daten, die zwischen Merge und Unmerge hinzugefügt wurden.",
  "utm-campaign": "fhir_expert",
  "utm-content": "unmerge-design"
}
---

Dies ist der dritte und letzte Beitrag der MDMbox-Merge-Serie. Der [erste](/blog/beyond-patient-merge) stellte unser client-gesteuertes Design für die `$merge`-Operation vor: Der Client sendet ein Transaktions-Bundle, das jedes PUT, POST und DELETE beschreibt; der Server umhüllt es mit Task + Provenance und führt es atomar aus. Der [zweite](/blog/mpi-vendors-patient-merge) erläuterte, warum Merge kein einheitlicher Algorithmus sein kann: Es gibt mindestens vier unabhängige Policy-Achsen, jede mit mehreren ausgelieferten Varianten bei MPI-Anbietern, MDM-Plattformen, EHRs und nationalen Registern. Und hier werden wir abschließend über die umgekehrte Operation sprechen, `$unmerge`.

Die FHIR-Spezifikation stellt uns `Patient/$merge` auf Reifegrad 0 zur Verfügung und kein `$unmerge` überhaupt, dennoch muss jeder MPI und jedes MDM im Produktionsbetrieb eine Umkehrung unterstützen, da jeder Master Patient Index irgendwann zwei Datensätze zusammenführt, die nicht hätten zusammengeführt werden sollen. Die Frage ist nicht, ob eine MDM-Plattform Unmerge benötigt, sondern wie Unmerge sich verhalten soll.

## Warum $unmerge nicht einfach $merge in umgekehrter Richtung ist

Die Versuchung besteht darin, Unmerge als „Rückwärtsabspielen der Merge-Transaktion" zu behandeln. Die Provenance lesen, die Versionen vor dem Merge aus dem Verlauf abrufen, sie zurückschreiben. Fertig.

Das funktioniert für die mechanische Hälfte, aber nicht für die Policy-Hälfte des Problems. Sehen wir uns einige Beispiele an.

**Zeitliche Lücke.** Anna Schmidt ist zweifach als `Patient/anna-old` und `Patient/anna-new` registriert. Am Montag wird `Patient/anna-old` in `Patient/anna-new` gemergt, aufgezeichnet als `Task/merge-anna` mit einer zugehörigen Provenance. Am Dienstag werden drei neue Encounters zu `Patient/anna-new` hinzugefügt (`Encounter/visit-tue-1..3`), Besuche, die die Klinik nach der Zusammenführung der Identität erfasst hat. Am Mittwoch wird ein Unmerge angefordert, weil der ursprüngliche Merge falsch war. Diese drei Encounters haben keine Version vor dem Merge. Sie waren nie auf `Patient/anna-old`. Wohin gehen sie jetzt: zurück zu einer wiederhergestellten Quelle, behalten auf dem Ziel, nach einer klinischen Regel verteilt, oder in eine Steward-Warteschlange geleitet?

![The temporal gap: data added between merge and unmerge has no pre-merge version to restore to](temporal-gap.svg)

Es gibt keinen einzigen definitiven Algorithmus dafür. Jeder Encounter-Datensatz könnte zu einem der beiden Patienten gehören, je nachdem, welcher tatsächlich den Besuch hatte, und der Server weiß das nicht.

**Hartes Löschen.** Wenn der Quell-Patient zum Zeitpunkt des Merge hart gelöscht wurde (das VistA-Muster aus unserem [vorherigen Beitrag](/blog/mpi-vendors-patient-merge)), ist der Quelldatensatz selbst aus den aktiven Tabellen verschwunden. Die History API verfügt noch über seine früheren Versionen, aber das Wiederherstellen einer aktiven Ressource aus einem Tombstone ist eine Policy-Entscheidung: Erstellen Sie eine neue ID und bewahren Sie die Zuordnung im Unmerge-Audit-Trail auf, stellen Sie die ursprüngliche ID wieder her und akzeptieren Sie die Lücke, oder verweigern Sie den Unmerge vollständig? Verschiedene MDM-Implementierungen beantworten dies aus Gründen unterschiedlich, die nichts mit FHIR zu tun haben: Aufbewahrungsregeln, nachgelagerte Abonnenten, jurisdiktionale Audit-Anforderungen.

Diese Grenze bestimmt die Schnittstelle: $unmerge ist ein vom Client bereitgestellter Umkehrplan, kein serverseitiger Rückgängig-Befehl.

Der Client sendet den ursprünglichen Merge-Task und ein Transaktions-Bundle, das den gewünschten wiederhergestellten Zustand beschreibt. Der Server validiert diesen Plan, prüft optimistische Sperren, wendet ihn atomar an und schreibt den Task/Provenance-Audit-Trail.

Die `$unmerge`-Operation ist damit symmetrisch zu `$merge` aus unserem [ersten Beitrag](/blog/beyond-patient-merge).

```http
POST $unmerge
Content-Type: application/fhir+json
```

```json
{
  "resourceType": "Parameters",
  "parameter": [
    {"name": "task", "valueReference": {"reference": "Task/merge-anna"}},
    {"name": "preview", "valueBoolean": false},
    {
      "name": "plan",
      "resource": {
        "resourceType": "Bundle",
        "type": "transaction",
        "entry": [
          {
            "resource": {"resourceType": "Patient", "id": "anna-old", "...": "restored from history"},
            "request": {"method": "PUT", "url": "Patient/anna-old", "ifMatch": "W/\"3\""}
          },
          {
            "resource": {"resourceType": "Patient", "id": "anna-new", "...": "rolled back to pre-merge state"},
            "request": {"method": "PUT", "url": "Patient/anna-new", "ifMatch": "W/\"7\""}
          },
          {
            "resource": {"resourceType": "Encounter", "id": "visit-jan",
                         "subject": {"reference": "Patient/anna-old"}},
            "request": {"method": "PUT", "url": "Encounter/visit-jan", "ifMatch": "W/\"4\""}
          },
          {
            "resource": {"resourceType": "Encounter", "id": "visit-tue-1",
                         "subject": {"reference": "Patient/anna-new"}},
            "request": {"method": "PUT", "url": "Encounter/visit-tue-1", "ifMatch": "W/\"1\""}
          }
        ]
      }
    }
  ]
}
```

Die einzige erforderliche Eingabe ist ein Verweis auf den ursprünglichen Merge-Task. Alles andere – welche Version welcher Ressource wo wiederhergestellt wird, was mit nach dem Merge hinzugefügten Daten geschieht, was mit der Quelle zu tun ist, wenn sie gelöscht wurde – befindet sich im `plan`-Bundle.

`preview: true` führt einen Trockenlauf der gleichen Validierungs-Pipeline durch. Der Server gibt das OperationOutcome zurück, das er zurückgegeben hätte, sowie die resultierende Provenance, sodass der Client einem Steward genau zeigen kann, was ein Unmerge berühren wird, bevor er ihn autorisiert.

`ifMatch`-Header tragen optimistische Sperren, genau wie beim Merge. Wenn sich eine Ressource zwischen dem Zeitpunkt, an dem der Client den Unmerge-Plan erstellte, und dem Zeitpunkt der Ausführung durch den Server geändert hat, schlägt die Transaktion fehl. Wir überschreiben stillschweigend keine Arbeit, die der Steward nicht gesehen hat.

## Den Plan aus der Provenance aufbauen

Der Client muss den Plan nicht von Grund auf neu konstruieren. Die Merge-Provenance enthält bereits die Daten, die der Client benötigt.

Für jede Ressource, die der Merge berührt hat, hat die Merge-Provenance einen versionierten Verweis unter `entity[].what` aufgezeichnet, zum Beispiel `Patient/anna-old/_history/2`. Das ist der Zustand vor dem Merge. Die History API löst ihn direkt auf:

```http
GET Patient/anna-old/_history/2
```

Ein naiver Standard-Plan ist dann eine Einpassen-Transformation:

1. Den Merge-Task lesen, seinen `Provenance.target`- und `Provenance.entity`-Listen folgen.
2. Für jede betroffene Ressource die Version vor dem Merge aus dem Verlauf abrufen.
3. Einen `PUT`-Eintrag ausgeben, der diese Version wiederherstellt, mit `ifMatch`, das zur *aktuellen* Version der Ressource passt (damit gleichzeitige Bearbeitungen erkannt werden).
4. Für Ressourcen, die während des Merge erstellt wurden und keinen Zustand vor dem Merge hatten (typischerweise: ein Linkage-Datensatz oder eine Matcher-Assertion), einen `DELETE` ausgeben. Werte, die in eine bestehende Ressource kopiert wurden (z. B. Identifier, die dem Ziel-Patient hinzugefügt wurden), verschwinden durch den Wiederherstellungs-`PUT` für diese Ressource.

Der größte Teil der Steward-UI von MDMbox verwendet genau diesen Standard. Der Steward sieht das vorgeschlagene umgekehrte Bundle, bearbeitet die Einträge, die Policy-Entscheidungen erfordern, und sendet es ab.

Was der Standard *nicht* behandelt, ist die zeitliche Lücke. Ressourcen, die *nach* dem Merge-Zeitstempel erstellt oder geändert wurden, sind nicht in der Merge-Provenance enthalten. Sie sind neuer Zustand, und die unten aufgeführten Policy-Slots sind die Möglichkeit des Stewards, anzugeben, was damit geschehen soll.

## Die Audit-Kette

Unmerge löscht den Merge nicht – er wird angehängt. Nachdem `$unmerge` committet hat, koexistieren drei Artefakte für `Task/merge-anna`:

1. **Der ursprüngliche Merge-Task**, mit `businessStatus` innerhalb derselben Transaktion von `merged` auf `unmerged` umgestellt. Das `ifMatch` auf diesem Flip erkennt gleichzeitige Unmerge-Versuche: Bei zwei parallelen `$unmerge`-Aufrufen für denselben Merge-Task gelingt einer, der andere erhält einen Versionskonflikt.
2. **Die Merge-Provenance**, unverändert. Sie nennt weiterhin die Versionen vor dem Merge in `entity[].what`. Der Unmerge macht den Audit-Trail des Merge nicht ungültig; er baut darauf auf.
3. **Ein neuer Unmerge-Task mit `basedOn → Task/merge-anna`**, kombiniert mit einer Unmerge-Provenance, die auf die *Versionen vor dem Unmerge* von allem zeigt, was der Umkehrplan berührt hat.

Die Kette lässt sich als Vorwärts-Abfrage lesen:

```http
GET /Task/merge-anna                                # the merge, businessStatus=unmerged
GET /Task?code=unmerge&based-on=Task/merge-anna     # the unmerge that reversed it
GET /Provenance?target=Task/unmerge-anna            # the unmerge's own audit trail
GET /Patient/anna-old/_history                      # full timeline of the source
```

Dieser letzte Aufruf ist derjenige, den Prüfer und Stewards tatsächlich ausführen, wenn die Quelle unter ihrer ursprünglichen ID wiederhergestellt wird. `Patient/anna-old/_history` liest sich als kontinuierliche Erzählung: erstellt, gelebt, beim Merge gelöscht, beim Unmerge wiederhergestellt, weiterhin gelebt. Jeder Übergang hat eine Provenance daran angeheftet, jede Provenance verweist auf einen Task, jeder Unmerge-Task verweist auf den Merge-Task, den er rückgängig gemacht hat. Wenn eine Implementierung stattdessen eine neue ID erstellt, ist die Kontinuität explizit in der Unmerge-Provenance und nicht in einem einzigen Patient-Verlauf.

Dieselbe Struktur gilt rekursiv. Wenn `Patient/anna-old` im nächsten Monat erneut gemergt und erneut ungemergt wird, wächst die Zeitleiste um einen weiteren Merge-Unmerge-Zyklus, der an seinem eigenen Task-Paar verankert ist. Nichts kollabiert, nichts wird neu geschrieben – die History API ist das entrollte Audit-Log.

## Vier Policy-Slots: die Merge-Achsen, umgekehrt

Ein Policy-Slot ist die Stelle im Unmerge-Plan, an der der Client eine explizite Entscheidung treffen muss. Einige Slots ändern das Transaktions-Bundle selbst. Andere erstellen Folgeaufgaben für Stewards, Matcher oder nachgelagerte Systeme. Zusammen machen sie den wiederhergestellten Zustand intentional, anstatt ihn lediglich historisch umkehrbar zu sein.

![The four merge axes inverted into unmerge questions](policy-slots.svg)

**Survivorship → Feldzuweisung.** Merge fragte, welcher Wert gewinnt, wenn zwei Datensätze in Konflikt geraten. Unmerge fragt: Für Felder, die *nach* dem Merge geschrieben wurden, welcher wiederhergestellte Datensatz erhält sie? Eine neue Telefonnummer, die am Dienstag erfasst wurde: Geht sie zur Quelle, zum Ziel oder zu beiden? MDMbox behandelt dies als eine Entscheidung pro Feld durch den Client, mit einem sicheren Standard von „bleibt beim Ziel" (dem Datensatz, der aktiv war, als das Feld geschrieben wurde), sowie einem Flag in der Provenance, damit ein Steward es überprüfen kann.

**Referenzbehandlung → Referenzzuweisung.** Merge fragte, was mit Zeigern auf die Quelle zu tun ist. Unmerge fragt: Wohin zeigen *neue* Referenzen jetzt – Referenzen, die nach dem Merge erstellt wurden und immer auf das Ziel gezeigt haben? Ein am Dienstag eingereichter Anspruch, der auf `Patient/anna-new` verweist, hat jetzt zwei Patienten im Geltungsbereich, und die Spezifikation gibt keine Anleitung dazu. Wir machen dies zu einer expliziten `plan`-Entscheidung pro Referenztyp. Der Standard ist, neue Referenzen beim Ziel zu belassen, es sei denn, der Client überschreibt dies.

**Quelldisposition → Quellenwiederherstellung.** Merge fragte, was aus der Quelle wird. Unmerge fragt: Was kommt zurück? Wenn die Quelle mit `replaced-by` inaktiv gehalten wurde, ist die Wiederherstellung mechanisch: `active` wieder auf true setzen, den Link entfernen. Wenn die Quelle hart gelöscht wurde, muss der Client wählen: die ursprüngliche ID aus dem Verlauf wiederherstellen (und die Versionslücke akzeptieren), eine neue ID mit Unmerge-Provenance erstellen, die die alte versionierte Quelle auf die neue Patient-ID abbildet, oder den Unmerge verweigern.

**Nachgelagerte Effekte → Neubewertungs-Warteschlange.** Merge besagte, dass kumulative Berechnungen und Link-Graphen neu bewertet werden müssen. Unmerge sagt dasselbe, mit einer weiteren Besonderheit: Alles, was *während* der zusammengeführten Periode aufgebaut wurde (neu abgeleitete Strahlendosis, neu berechnete Kohortenzugehörigkeit, Caches von Drittanbietern, der eigene probabilistische Link-Graph des MPI), kann ohne Überprüfung für keinen der wiederhergestellten Patienten mehr als vertrauenswürdig gelten. Der Unmerge-Task und seine Provenance sind der Subscriptions-Ankerpunkt: Nachgelagerte Verbraucher (Steward-UI, Neuberechnungen der kumulativen Dosis, der Matcher) beobachten `Task?code=unmerge` und werten die in `Provenance.target` aufgeführten Ressourcen neu aus. Der Server veröffentlicht das Audit-Ereignis; was neu berechnet werden soll, ist eine nachgelagerte Entscheidung.

Das Muster ist dasselbe wie bei `$merge`. Der Server hat nicht den klinischen Kontext, um diese Entscheidungen zu treffen. Die Client-Seite sollte sie treffen.

## Nach dem Unmerge: Re-Merge und Matcher-Feedback

Sobald `$unmerge` committet, liest sich der `businessStatus` des Merge-Tasks als `unmerged`. Die Pre-Merge-Prüfung von MDMbox (die Abfrage, die das Mergen einer bereits gemergten Quelle verhindert) sucht nach `Task?code=merge&business-status=merged&subject={ref}` und findet den alten Task nicht mehr. `Patient/anna-old` ist für einen neuen Merge berechtigt. Das ist beabsichtigt: Unmerge macht den Merge-Lebenszyklus vollständig rückgängig, einschließlich der Blockierung eines erneuten Merge.

Wenn ein Mensch gerade entschieden hat, dass zwei Patienten unterschiedlich sind, sollte der Matcher sie beim nächsten Batch-Lauf nicht stillschweigend wieder zusammenführen. Das Argument dagegen, es automatisch zu machen, ist stichhaltig: Ein Unmerge könnte einen *technischen* Fehler rückgängig machen (falsche Identifier im Merge-Plan, eine Quelle, die auf den falschen Datensatz zeigt) und nicht einen *klinischen* Fehler behaupten (diese Patienten sind wirklich verschiedene Personen). Der Server kann das nicht unterscheiden.

MDMbox hält die beiden Signale getrennt. Der Unmerge-Task und die Provenance sind die öffentliche Aufzeichnung der Umkehrung, abfragbar durch den Matcher, die Steward-UI oder jeden nachgelagerten Abonnenten. Eine Nicht-Merge-Assertion – wenn eine MDM-Implementierung eine solche möchte – liegt in der Verantwortung des Clients zu schreiben, und da der Unmerge-Plan ein Standard-Transaktions-Bundle ist, fügt der Client ein `POST` für diese Assertion neben den Einträgen des Umkehrplans ein.

Der natürliche FHIR-Träger ist eine `Linkage`-Ressource, die auf beide wiederhergestellten Patienten zeigt, mit dem „do-not-merge"-Code der Implementierung in einer Extension (die eingebauten `Linkage.item.type`-Codes wie `source`/`alternate`/`historical` decken keine Matcher-Assertions ab):

```json
{
  "resource": {
    "resourceType": "Linkage",
    "active": true,
    "item": [
      {"type": "alternate", "resource": {"reference": "Patient/anna-old"}},
      {"type": "alternate", "resource": {"reference": "Patient/anna-new"}}
    ],
    "extension": [{
      "url": "http://mdmbox.dev/fhir/StructureDefinition/linkage-assertion",
      "valueCode": "do-not-merge"
    }]
  },
  "request": {"method": "POST", "url": "Linkage"}
}
```

Eine In-Place-Alternative ist `Patient.link` mit `type=seealso` zuzüglich derselben Extension am Link: weniger Ressourcen, aber der Matcher-Zustand landet dann zusammen mit den demografischen Daten in der Patient-Versionskette. Die meisten Implementierungen werden die eigenständige `Linkage`-Ressource bevorzugen, damit die „Diese nicht fusionieren"-Regeln des Matchers in ihrer eigenen Ressource leben und abgefragt, abgelaufen oder widerrufen werden können, ohne Patient-Versionen zu berühren.

Sie hat dieselbe Atomarität wie alles andere: Der Unmerge und die Assertion werden zusammen committet, oder keines von beiden wird es. Der Vertrag des Servers endet an der Bundle-Grenze; welche Form die Assertion annimmt und welche Datensätze sie abdeckt, liegt im Ermessen des Clients.

Dasselbe Prinzip gilt für Merge-Ketten. Ein Ziel kann mehr als einmal gemergt worden sein (`A → B`, dann `C → B`); der Server verhindert das nicht. Wenn der Client später einen davon unmergt, muss der Umkehrplan berücksichtigen, dass `B`'s aktueller Zustand *beide* Merges widerspiegelt, nicht nur den, der rückgängig gemacht wird. Jeder Merge hat seine eigene Provenance, sodass die Daten vorhanden sind, aber die korrekte Anwendung über überlappende Merges hinweg ist eine Client-Entscheidung, keine Server-Entscheidung.

Einige Unmerges benötigen auch ein Policy-Gate, bevor der Umkehrplan committen darf. Der kanonische Fall ist kumulativer klinischer Zustand: lebenslange Strahlendosis, kumulative Opioid-Gesamtmengen oder jede signierte Berechnung, die durch die Umverteilung von Ereignissen auf zwei Patienten ungültig werden würde. Der portable Vertrag lautet nicht „immer ablehnen" oder „immer fortfahren". Der Merge-Task ist der Ankerpunkt für diese Sperre; Preview kann sie als OperationOutcome anzeigen; die Implementierung entscheidet, ob der Unmerge inline blockiert, in eine menschliche Überprüfungs-Warteschlange weitergeleitet oder zugelassen wird, während abgeleitete Gesamtwerte unter Quarantäne verbleiben.

## Abschluss der Serie

Dieser Beitrag vervollständigt die Merge-Trilogie. Der [erste](/blog/beyond-patient-merge) definierte ein client-gesteuertes `$merge` und plädierte dafür, Policy aus dem Server herauszuverlagern. Der [zweite](/blog/mpi-vendors-patient-merge) zeigte, dass Policy nicht eine einzelne Entscheidung ist, sondern vier, und dass jeder MPI-Anbieter im Produktionsbetrieb eine andere Kombination ausliefert. Dieser spiegelt den Vertrag für die umgekehrte Richtung: dieselbe Schnittstelle, dieselben vier Achsen als umgekehrte Unmerge-Fragen, dieselbe Aufgabenteilung zwischen Server-Invarianten und Client-Policy.

---

*Die vollständige $merge / $unmerge-Implementierung ist heute in [MDMbox](https://www.health-samurai.io/mdmbox) verfügbar – einer FHIR-nativen Master Patient Index- und MDM-Plattform.*