Projekt · 2020 · Autor

meinrezept.online Rezept-Plattform

Online-Rezeptplattform in Hamburg. Als Backend-Lead PHP/Node.js-Microservices und RabbitMQ-Apotheken-Routing unter Pandemie-Last 2020 gebaut.

Screenshot of meinrezept.online - the Hamburg online prescription platform

Im April 2020 wurden plötzlich alle deutschen Healthcare-Startups wichtig.

Apotheken waren überwältigt. Ärzte machten zum ersten Mal Video-Konsultationen. Patienten schickten Papierrezepte per Post ein. Das ganze Land wachte auf und bemerkte, dass die Rezept-Pipeline - der Weg von einem Arzt, der ein Rezept ausstellt, bis zum Patienten, der sein Medikament bekommt - fast vollständig analog war. In Hamburg stieß ich als einer der Backend-Leads zu meinrezept.online, um dabei zu helfen, das System in der Mitte davon zu bauen.

Das hier ist kein “wir haben während einer Pandemie Leben gerettet”-Post. Es ist ein “so sah das Engineering wirklich aus”-Post - weil das Engineering genuinely interessant war und das meiste davon von außen unsichtbar ist.

Das Rezeptformat-Problem

Bevor man ein Rezept an eine Apotheke routen kann, muss man es lesen. In Deutschland 2020 bedeutete das, mit mehreren Formaten gleichzeitig umzugehen: dem Muster-16-Papierformular, frühen digitalen Rezeptformaten, die noch nicht standardisiert waren, Fax-Images und dem gelegentlichen PDF, das eine Arztpraxis aus welcher Praxisverwaltungssoftware auch immer generiert hatte.

Jedes Format hat einen anderen Extraktionspfad. Das Papierformular hat ein bekanntes Layout, kommt aber als Scan an. Die digitalen Formate haben strukturierte Daten, aber nicht alle Apotheken konnten alle von ihnen konsumieren. Das PDF aus der Praxisverwaltungssoftware ist der schlimmste Fall - strukturierte Daten in einem Dokumentenformat, das nie dafür gedacht war, programmatisch geparst zu werden, in einem Format, das sich aktiv weiterentwickelte, während die Bundesregierung das Elektronische Rezept (eRezept) in Echtzeit einführte.

Die Aufgabe des Backends war, all das in eine einzelne interne Repräsentation zu normieren, die der Rest des Systems - die Apotheken-Routing-Layer, der Order-Tracking-Service, der Patienten-Benachrichtigungs-Service - konsumieren konnte, ohne sich um das Eingangsformat zu kümmern. Das ist ein schwieriger Problem als es klingt, wenn die Eingangsformate sich unter einem bewegen.

Apotheken-Integration ist Legacy-schwer by Design

So sieht der Apotheken-Integrations-Bereich vom Backend aus: Jede große Apothekenkette hat ihr eigenes Bestellsystem; viele unabhängige Apotheken betreiben WINAPO, ProFit oder ADG - Praxisverwaltungssysteme, die Jahrzehnte alt sind und Bestellschnittstellen über Protokolle exponieren, von denen man in einem modernen Backend-Kontext noch nie gehört hat. HTTP/JSON ist nicht das dominante Wire-Format. EDIFACT und HL7-Varianten tauchen auf. Manche Apotheken haben ein Webportal und eine inoffizielle API, die jemand reverse-engineered hat. Manche haben nichts, und man ist zurück bei strukturierten E-Mails.

Das ist keine Beschwerde. So sieht Healthcare-Integration an der Consumer-Seite aus. Die EHR-Arbeit im Krankenhausmaßstab, die ich vorher gemacht hatte (AFAQ, HAKEEM), operiert auf einer anderen Abstraktionsebene - dedizierte Integration-Engines, HL7-FHIR-Contracts, ein technischer Counterpart am anderen System. Im Consumer-Apotheken-Maßstab arbeitet man mit dem, was die Apotheke tatsächlich hat, was häufig deutlich weniger ist als man sich wünschen würde.

RabbitMQ war der Message-Broker, was genau richtig war für diesen Maßstab. Kein Kafka-Level-Volumen - zuverlässige Zustellung zwischen einer handhabbaren Anzahl von Services mit Retry-Semantik, Dead-Letter-Handling und die Entkopplung von eingehendem Order-Empfang von ausgehendem Apotheken-Dispatch. RabbitMQ handhabt das sauber. Die Exchange-Topologie nutzte Topic-Exchanges, die auf Apotheken-Region, Rezept-Typ und Fulfillment-Pfad routen - was uns ermöglichte, Routing-Logik hinzuzufügen, ohne jeden Consumer anzufassen.

Microservices mit drei Sprachen

Der Stack war PHP 7, Python und Node.js. Das ist die echte Welt. Man erbt die Sprache, in der der erste Service geschrieben wurde. Man wählt das beste Tool für das nächste Problem - Python für die Extraktionsarbeit, Node.js für die Echtzeit-Status-API. Man endet mit einer polyglotten Codebase, die durch Message-Contracts zusammengehalten wird statt durch Sprachkonsistenz.

Die Message-Contract-Disziplin zählt mehr als die Sprachkonsistenz, ehrlich. Wenn jeder Service sich darüber einigt, wie ein order.received-Event aussieht - das Schema, die Versionierung, die erforderlichen Felder - dann ist die Tatsache, dass der Producer PHP ist und der Consumer Node.js, ein Implementierungsdetail. Der gefährliche Failure-Mode ist, jeden Service sein Verständnis der Domain unabhängig weiterentwickeln zu lassen, sodass order.received dem Apotheken-Dispatch-Service etwas leicht anderes bedeutet als dem Patienten-Benachrichtigungs-Service. Man findet das zum schlechtestmöglichen Zeitpunkt heraus: wenn ein Patient den Support anruft und sagt, er hat eine “Rezept abgeschickt”-Benachrichtigung erhalten, aber die Apotheke hat keine Aufzeichnung der Bestellung.

ReactJS auf dem Frontend - nicht primär meine Verantwortung - war mit demselben Domain-Modell durch die Status-API verbunden. Konsistenz zwischen dem, was der Patient in der UI sieht, und dem, was das Event-Log des Backends sagt, ist ein unterschätztes Integration-Concern, das sichtbar wurde, als Pandemie-Bestellvolumen Timing-Gaps aufdeckte, die Staging-Umgebungen nicht antizipiert hatten.

Der Pandemie-Zeitplan amplizierte alles

Im April 2020 einzusteigen bedeutete, einem Team beizutreten, wo das Produkt von echter Nutzernachfrage load-getestet wurde auf eine Weise, die keine Staging-Umgebung modelliert hatte. Healthcare-Angst ist ein echter Traffic-Treiber. Eingehende Bestellvolumen spiken bei jedem Nachrichtenereignis - einer Lockdown-Ankündigung, einer Änderung der Apothekenöffnungszeiten, einer Neuigkeit über Medikamentenengpässe. Man lernt sehr schnell, welche Teile des eigenen Services stateful sind und welche nicht, weil die stateful Teile unter Load-Spikes nicht-graceful scheitern.

Die Extraktions-Pipeline von dem Order-Acceptance-Pfad zu entkoppeln war die kritische Architekturentscheidung. Eine langsame Apotheken-Integration sollte nicht verhindern, dass das Rezept eines Patienten empfangen und bestätigt wird. Die Bestätigung und die Fulfillment sind verschiedene Concerns und sollten unabhängig scheitern. Diese Änderung wurde unter Last gemacht, was nicht ideal ist - aber es ist ein echtes Engineering-Memory, und die Begründung ist rückblickend und im Whiteboard-Session solide.

Was die Bandbreite über Healthcare-Maßstäbe lehrt

Ich habe Healthcare-Software auf zwei sehr verschiedenen Maßstäben geliefert: große Enterprise-EHR und klinische Workflow-Systeme (Krankenhausmaßstab, Hunderttausende von Patientenakten, komplexe klinische Entscheidungsunterstützung) und Consumer-facing Healthcare-Apps (Rezeptbestellung, Patienten-Benachrichtigungen, Apotheken-Routing). Sie teilen regulatorische Anforderungen - Patientendaten-Handling, Rezeptvalidität, Audit-Trails - aber fast nichts sonst.

Die Consumer-seitige hat schnellere Iterations-Zyklen, weniger formale Integration-Partner-Beziehungen und mehr Druck, den langen Schwanz von Edge-Cases in den Daten zu handhaben. Die Enterprise-Seite hat strukturierte Integration, formale HL7- und FHIR-Contracts und langsamere Änderungszyklen. Beides ist schwer. Die Bandbreite ist nützlich.

meinrezept.online operiert noch. Die Apotheken-Routing- und Rezeptverarbeitungs-Infrastruktur aus dieser Zeit - unter genuinely ungewöhnlichem Druck gebaut, gegen eine sich entwickelnde regulatorische Landschaft, mit einem Team, das sich an dieselben Lockdowns anpasste, die das System bediente - ist Teil dieser Operation. Die Rohre laufen noch.