← Back
Home › Systems › Healthcare

e-prescription

Moves a prescriber's order to dispense a drug to a pharmacy without paper and without losing, altering or double-filling it.

Surescripts

Open the e-prescription pagee-prescription Hdr histogramdata-structures: Log-linear bucketed histograms with bounded relative error for latency percentilesDeterministic simulationtestingdistributed: Run the whole system in one thread under a seeded scheduler with simulated network, disk and clock, so every failure replays from its seed.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Idempotency keysfinance: Client-chosen unique key per logical operation, stored with a request fingerprint and the final response, so retries replay the first result instead of acting twice.›Process supervisionhardware-os: A parent process starts services, reaps their exits and restarts them under a delay and burst-limit policy, then stops them with SIGTERM before SIGKILL.›Fhir resource modelnetworking: FHIR resources, references, search parameters and REST interactions for health data exchangeTerminology code systemsnetworking: Code systems, value sets and concept maps (SNOMED CT, LOINC, ICD) with subsumption queriesFuzzy string matchingsearch-sort: Edit distance, Jaro-Winkler and trigram indexes for approximate name and address matchingRecord linkagesearch-sort: Probabilistic record matching (Fellegi-Sunter) with blocking for patient or entity indexesAuthorization models (RBAC,ABAC, ReBAC)security: How to decide who may do what to which object; roles for coarse rules, relationships for sharing, and checks on every object.›Coverage-guided fuzzingsecurity: Mutate inputs, run an instrumented target, and keep any input that reaches a new code edge; feedback turns blind guessing into incremental search.›Digital signatures (Ed25519)security: Small, fast, deterministic public-key signatures that prove who produced a message; the default for tokens, releases and protocols.›Hardware security module (HSM)security: A tamper-resistant device or service that generates keys, signs, decrypts and wraps with them internally, and releases a key only as ciphertext.›Mfasecurity: Multi-factor authentication flows: TOTP, push, WebAuthn second factor, recovery codes, step-upMutual TLSsecurity: TLS where the client also proves a certificate key, so every connection carries a verified, rotatable machine identity.›Tamper-evident audit logsecurity: An append-only log whose Merkle root, signed per batch, lets anyone verify an entry is present and history was never rewritten, with log-size proofs.›TLS 1.3security: The standard encrypted, authenticated transport; one round trip, only forward-secret AEAD suites, and little left to misconfigure.›X509 certificatessecurity: Certificate chains, path validation, SANs, revocation and short-lived issuancePoint-in-time recoverystorage: Periodic base backups plus a continuously archived change log let you rebuild the state as of any instant, then branch from there.›Write-ahead logstorage: Append each change to a checksummed sequential log and fsync it before touching data pages; the basis of crash durability in nearly every database.›Event condition action rulessystems: Event-condition-action rule engines for automations, edit checks and alertsExponential backoff with jittersystems: Retry after delays that grow geometrically and are randomized, so failed clients spread out instead of hammering a recovering service in synchronized waves.›Graceful drainingsystems: On a termination signal, stop taking new work, finish in-flight work until a deadline, tell clients to reconnect elsewhere, then exit; clients resume from a token.›Hierarchical state machinesystems: Nest states so a substate inherits its parent's reactions and only overrides what differs, with ordered entry and exit actions along the path.›Cooperative schedulingconcurrency: Green threads/coroutines yielding at await points on a user-space schedulerFault injectiondistributed: Inject latency, errors, kills and partitions via proxies, netem or hooksRecord replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuildHealth checkingsystems: Decide per backend whether to route to it using probes and real-traffic errors, with consecutive-count hysteresis, capped ejection and a panic fallback.›Leasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Transitive closure indexgraphs: Segment/interval labelling of DAGs (commit graphs) for fast reachability queriesClosure tablestorage: Storing trees and hierarchies in relational tables (closure table, materialized path) for fast subtree queriesInverted indexstorage: Map each term to a sorted, compressed list of the documents containing it; the core of full-text search and other fast set queries.›Union-findgraphs: Disjoint-set forest with union by size and path compression; merges sets and answers same-set queries in effectively constant time.›Linear and logistic regressionmachine-learning: Fit a weighted sum of features to a number (least squares) or a probability (logistic); the baseline every model must beat.›Near-duplicate detection withMinHash LSHmachine-learning: Estimate set Jaccard similarity from k min-hashes and find similar pairs with banded LSH buckets; the standard dedup for web crawls, training corpora and sequence collections.›Cryptographic hashes (SHA-2,BLAKE3)security: Collision-resistant fingerprints of data; SHA-256 for interoperability, BLAKE3 for speed, HMAC/HKDF for keyed uses.›+ 26 moreTransparent interceptionnetworking: Redirect traffic to a local proxy (iptables TPROXY/REDIRECT, eBPF) without app changesSyscall interceptionsecurity: Intercept syscalls via ptrace, seccomp user notify or a user-space kernelFencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›Interval treedata-structures: Index of ranges that answers "which intervals overlap this point or range?" in O(log n + k); static sorted-array form is the compact, cache-friendly choice.›BFS and DFSgraphs: The two basic graph traversals; BFS gives hop distances and levels, DFS gives discovery/finish order for cycles, components and topological order.›Topological sortgraphs: Order the vertices of a DAG so every edge points forward; the core of build systems, schedulers and dependency resolution, with cycle detection for free.›Bit packing and PFORcompression: Store blocks of integers with exactly b bits each and patch rare outliers separately (PFOR); the fastest integer codec family, billions of values per second.›Delta encodingcompression: Store differences between consecutive values instead of the values, turning sorted IDs, timestamps and counters into small numbers that pack into few bits.›Varint (LEB128)compression: Variable-length integers in 7-bit groups with a continuation bit; small numbers take 1 byte, the wire format of Protobuf, WebAssembly and DWARF.›BM25 rankingsearch-sort: The standard lexical relevance score: IDF-weighted term frequency with saturation (k1) and length normalization (b); default in Lucene, Elasticsearch and SQLite FTS5.›Sorted-list intersectionsearch-sort: Intersect sorted id lists by merging, galloping from the short list into the long one, and skipping whole compressed blocks, shortest list first.›Loss functionsmachine-learning: The scalar a model minimizes: cross-entropy for classes and tokens, MSE or Huber for values, each with a stable form and simple gradient.›+ 11 moreEbpfhardware-os: Verified in-kernel programs on hooks (kprobes, XDP, tc) with mapsPtrace process controlhardware-os: Ptrace: attach, read and write registers and memory, single-step, intercept signalsSeccomp bpfsecurity: Syscall filtering with seccomp BPF programsSparse matrices (CSR)numerics: Compressed sparse row stores only nonzeros as row pointers, column indices and values; SpMV streams them once, so it is memory-bandwidth bound.›Top-k selectionsearch-sort: Keep the k best of n items without sorting all n - bounded min-heap for streams, quickselect for arrays, threshold pruning to skip most items.›Unicode text tokenization forsearchsearch-sort: Turn multilingual text into index terms: segment words (UAX 29 style, CJK bigrams), normalize, case-fold and accent-fold, identically at index and query time.›+ 3 moreText normalizationmachine-learning: Rewrite messy text into one canonical form for matchers and models, or into spoken words for speech output (and back), keeping a map to the original offsets.›

How it is built

Architecture overview

An e-prescription system moves one legally meaningful order, "dispense this drug to this patient", from a prescriber's software to a pharmacy's software without paper and without losing, altering or double-filling it. Prescribers (a health-record or practice system), pharmacies (a dispensing system) and a network operator between them are involved. The operator is either a hub that routes messages (the US model, Surescripts being the best-known operator) or a central repository that holds each prescription as a record the parties act on (the German gematik E-Rezept). Either way four jobs appear: represent the order in a shared model with coded drugs, prove who signed it, drive it through a small life cycle exactly once, and keep an audit trail that survives a dispute. The one idea that makes it work: the prescription is a signed, immutable document plus a state machine that lives on the operator. The document is never edited after signing; only the state moves (ready, in progress, completed), guarded by a capability the patient hands to one pharmacy.

Parts:

  prescriber app (EHR)                  operator / prescription service                pharmacy app
 ┌──────────────────────┐   mTLS/TLS   ┌─────────────────────────────────┐  mTLS/TLS  ┌─────────────────┐
 │ order entry          │─────────────►│ API (FHIR REST)                 │◄───────────│ fetch / accept  │
 │  rules (ECA)         │  signed      │  authn (certs) · authz (ABAC)   │  dispense  │ dispense / close│
 │  terminology lookup  │  order       │  idempotency · state machine    │  results   │ drug catalogue  │
 │  patient match       │              │        │            │           │            │  (ConceptMap)   │
 │  2-factor + sign     │              │   WAL ─► primary ─► standby     │            └─────────────────┘
 └──────────────────────┘              │        │            │ archive   │
         ▲                             │   audit log (Merkle, signed)    │──► point-in-time recovery
         │  access code / 2D code      └─────────────────────────────────┘
         └──────── patient (phone app or paper token) ──────────────────────► hands it to a pharmacy

How the parts fit together

Prescription model and vocabulary

The order is not free text. FHIR resources (fhir-resource-model, no page yet) give it a shape: a MedicationRequest carries the status, the medication, the dosage and a dispenseRequest block with the validity period and the number of refills allowed, plus a substitution block that says whether the pharmacy may swap a generic (source: HL7 FHIR R5 MedicationRequest page). A MedicationDispense is the pharmacy's answer. Why FHIR and not a pipe-delimited message such as HL7 v2 (hl7v2-message-parsing, no page yet)? Resources are typed, versioned (ETag/versionId), searchable by named parameters, and one shape serves the prescriber API, the patient app and the pharmacy API. The US network has long spoken NCPDP SCRIPT messages; a new national scheme such as the German one is built directly on FHIR profiles.

The drug is a code, not a string. terminology-code-systems (no page yet) holds the code systems and value sets the profile binds to: RxNorm for US clinical drugs, SNOMED CT for clinical concepts (both listed on the HL7 code-system page). The prescriber UI uses $expand on a value set to offer only valid choices; the pharmacy uses a ConceptMap ($translate) to turn the code into the product in its own catalogue; $subsumes answers "is this product a kind of the ingredient the rule names?". The contract is a coded drug plus the original display text, so a code the pharmacy cannot map degrades to a readable line, not a rejection.

Prescriber-side checks

At order entry the prescriber software runs rules: interaction, duplicate therapy, allergy, maximum dose, "controlled substance needs two-factor signing". event-condition-action-rules (no page yet) fits: the event is "order drafted" or "changed", the conditions read the order and the medication list, the actions are warnings the prescriber must acknowledge. A table of a few dozen rules needs nothing more; Rete rules engine pays off only when thousands of rules share conditions. Rules run on the prescriber's side because only there can the prescriber react before signing; the operator validates the shape of the order (the profile), not the clinical content.

Identity matching

Patient, prescriber and pharmacy directories must be matched; the patient case is the dangerous one. record-linkage (no page yet) scores candidate pairs on name, birth date, address and identifiers with Fellegi-Sunter weights after a blocking step; fuzzy-string-matching (no page yet) supplies the similarity for misspelt names. FHIR exposes the operation as Patient/$match (source: HL7 FHIR Patient-operation-match). The contract: ranked candidates with a score, never a silent merge, with two thresholds giving match, clerical review and no match, because attaching a prescription to the wrong patient is a safety event while a missed match costs one phone call. Where the scheme carries a patient identifier on every order (Germany's insurance number) matching shrinks to a lookup.

Trust and signing

Who may speak and who signed are two different questions.

  • Who is calling: each organisation holds an X.509 certificate (x509-certificates, no page yet), checked with Mutual TLS, so the service knows the caller is a registered system, not only that a token is valid. The channel is TLS 1.3. The German scheme adds an application-layer channel on top of TLS (the "VAU transport") so requests stay encrypted into the service's trusted execution environment instead of ending at a load balancer (source: gematik api-erp, authentisieren.adoc).
  • Who signed: in Germany a qualified electronic signature is made with the physician's health-professional card through the connector; the service validates it and adds its own server-side signature to protect integrity (source: gematik api-erp, erp_bereitstellen.adoc). In the US, for controlled substances, 21 CFR 1311 requires two of three factors (known, owned, being), with a hard token separate from the computer if a token is used; completing that step is the signing, the application then digitally signs and archives the record, and an unsigned prescription must not be transmitted (source: eCFR 1311.115, 1311.140). This is the multi-factor pattern (mfa, no page yet) applied as a step-up at the moment of signing, not once per session.
  • Which algorithm: both regimes fix cryptography by regulation (the US text cites FIPS 186 and FIPS 140-2 Level 1 modules). Where the design is free, such as batch signatures on the audit log and signed receipts, Ed25519 is the default: small and deterministic, with no per-signature randomness to get wrong.
  • Where keys live: service signing keys and TLS private keys sit in a hardware security module with dual control for key ceremonies, so an administrator who can log in still cannot export the key that makes receipts valid.

Prescription service and life cycle

The service is small and strict. Its core is a hierarchical state machine per prescription. The German implementation shows the shape (source: gematik api-erp): POST /Task/$create reserves a prescription ID and returns an access code; after signing, $activate moves draft to ready; a pharmacy's $accept with the access code moves it to in-progress and returns a secret; $dispense and $close with the secret complete it; $reject returns it to ready and deletes the secret; $abort deletes the task. A second pharmacy calling $accept gets 409 Conflict. Nesting helps: expiry and cancellation by the prescriber are written once on the parent state, not per substate.

Two contracts keep this safe under retries:

  • Capability, not just identity: the access code is something the patient holds (a 2D code or an app) and shows to the pharmacy of their choice; the service combines that capability with the caller's certificate and role in an attribute-style check (Authorization models): "this role, this pharmacy, this task, this state". The secret issued at accept then binds the later dispense and close calls to the pharmacy that accepted.
  • Retries replay, they do not repeat: a pharmacy whose response was lost must be able to retry. The German docs allow the same pharmacy to fetch the accepted task again to recover a lost secret, and refuse other pharmacies. The general technique is Idempotency keys: the client sends a key per logical operation; the service stores the key with a request fingerprint and the first response and replays it, so "dispense" cannot happen twice. The prescription ID reserved at $create already is such a key for creation; operations on it need their own.

The gematik docs also warn that $create consumes an ID from a finite number space even if never activated, so clients must not pre-create blanks: an ID allocator with per-client quotas is part of the design.

Durability

A lost prescription is a patient without medicine; a duplicated one is a drug dispensed twice. Every transition is appended to a Write-ahead log and synced before it is acknowledged. The store is primary-backup: one primary orders all writes, a standby receives the log, and the acknowledgement rule decides what a crash can lose. Here the standby acknowledges before the call returns, because losing an acknowledged "dispensed" causes exactly the double fill the system exists to prevent. Quorum replication and Raft tolerate more failures but cost more machines and latency than a regional service needs. Point-in-time recovery covers what replication does not: a bad deployment or operator error that was faithfully replicated.

Delivery reliability

Pharmacies and prescribers are on uneven networks; calls fail. Clients retry with exponential backoff with jitter, so a recovering service sees spread-out retries, not a synchronized wave; safety of the retry comes from the idempotency keys above. A hub that forwards messages needs the same on its outbound side: store, forward, retry, and a dead-letter state that a human sees. US rules add a duty: if delivery of a controlled-substance prescription fails, any replacement must say the original was sent electronically and failed (source: eCFR 21 CFR 1311.102(c)).

Audit

Disputes happen ("I never signed that", "the quantity was changed"). Every state change, signature verification and access goes to a Tamper-evident audit log: an append-only log whose Merkle root is signed per batch, so an auditor can check inclusion and that history was not rewritten. The US rules ask for the same in plainer words: the pharmacy audit trail is analysed at least every calendar day, incidents are reported within one business day (source: eCFR 21 CFR 1311.215), and prescription content must not be altered in transit (1311.170). The log lives apart from the state store, so compromising the database does not let an attacker rewrite the evidence.

Operations

Run the service under Process supervision and Graceful draining: on deploy a node stops accepting calls, finishes in-flight transitions, and clients retry elsewhere, which idempotency keys make safe. hdr-histogram (no page yet) records latency percentiles for the pharmacy-counter path, where a slow $accept is a customer standing at a desk. Deterministic simulation crashes nodes between "accepted" and "dispensed" under a seeded clock and checks "at most one dispense per prescription". Parsers for messages from many vendors run under coverage-guided fuzzing.

Data and control flow

One prescription from order to dispense (central-repository model, German shape):

  1. The prescriber drafts the order; the ECA rules run on the draft and the prescriber acknowledges warnings. Drug choice comes from a value-set $expand; the patient is selected by identifier or by $match.
  2. The prescriber app authenticates to the service (certificate, token) and calls $create; the service reserves an ID and returns the task with an access code.
  3. The app builds the order bundle with the task ID inside, the physician signs it (card plus PIN, via the connector), and the app calls $activate. The service validates profile, signature and dates, adds its own signature, appends the transition to the write-ahead log, waits for the standby, and answers.
  4. The patient receives the access code (app or printed 2D code) and presents it at a pharmacy.
  5. The pharmacy calls $accept with the code; the service checks certificate, role, state and code, flips ready to in-progress, issues a secret, returns the signed bundle. A second pharmacy gets 409.
  6. The pharmacy maps the coded drug to a product (ConceptMap), dispenses, calls $dispense and $close with the secret; the service stores the dispense record and a receipt signed by the service, and the state becomes completed. Each step is audit-logged.

One message in a hub model (US shape, controlled substance): the prescriber completes two-factor authentication and the application signs and archives the order; it goes to the network over an authenticated channel, which validates structure and routes by pharmacy identifier; the pharmacy acknowledges (without an acknowledgement the sender retries with backoff, then reports failure); the pharmacy verifies the signature, dispenses and the fill status returns through the network.

Scaling

The workload is small next to a web service: a country issues prescriptions in numbers of the order of millions per day at most, each with a handful of calls. Levers in the order to pull them:

  1. Correctness under concurrency (two pharmacies accepting the same task) comes before throughput: one primary serialises transitions per prescription; shard by prescription ID only when its log write rate is the limit.
  2. Signature checks and TLS handshakes at peaks: cache validated certificate chains, keep connections open, add stateless API nodes in front of the primary.
  3. Terminology and search (typeahead, $expand) are read-mostly and versioned by code-system release: precompute expansions, serve from replicas.
  4. Record linkage is quadratic without blocking; with blocking it is a batch per directory update plus an online lookup.
  5. Audit log growth: seal and archive old segments, keep the signed roots online.

Failure modes

FailureSymptomCauseFix
Double dispenseone prescription filled at two pharmaciesaccept not exclusive, or a retried dispense replayed as newone serialised state check; idempotency keys; 409 for the second caller
Lost secretpharmacy cannot close an accepted prescriptionresponse lost after acceptlet the same pharmacy re-fetch, never another
Wrong patientorder attached to another personmatch merged automaticallythree-outcome thresholds, review band, identifier match first
Unmapped drugpharmacy cannot find the productno ConceptMap entry or retired codekeep original text in the order, versioned maps, alert on unmapped codes
Replicated mistakebad data on primary and standbydeploy bug or operator errorpoint-in-time recovery to before the change
Clock skewsignature dates rejected, audit order wrongnode clocks driftclock monitoring; US rules require the application clock within five minutes of NIST (eCFR 1311.120)
Retry stormoverload after a short outageclients retry in lockstepbackoff with jitter, per-caller rate limits
Evidence rewrittenaudit trail disputedlog in the same store, no external anchorsigned Merkle roots held elsewhere
ID exhaustion$create fails or IDs run lowclients pre-create blanksallocate on intent, per-client quotas

Real implementations

namewhat it isrepolicencenotes
gematik api-erpdocumentation and FHIR profiles of the German national e-prescription servicegematik/api-erpApache-2.0central-repository model; docs only, gematik operates the service
HAPI FHIRJava API for FHIR clients and servershapifhir/hapi-fhirApache-2.0base for a prescriber or repository prototype
Medplumhealthcare platform for building compliant applications on FHIRmedplum/medplumApache-2.0FHIR-native app platform
SnowstormSNOMED CT terminology server on ElasticsearchIHTSDO/snowstormsee repothe terminology service
Splinkprobabilistic record linkage over SQL backendsmoj-analytical-services/splinkMITpatient index, duplicate cleanup
OpenEMRopen-source EHR and practice managementopenemr/openemrGPL-3.0a prescriber-side application

The hub alternative. In the US the operator forwards messages and keeps a directory of prescribers and pharmacies (Surescripts, proprietary). The parts are the same, but the operator does not own the record, so the state machine lives in sender and receiver and the network adds routing, directory and delivery acknowledgement. Pick a central repository when one authority exists and patients choose the pharmacy at the counter; pick a hub when many independent record owners already exist.

Sources

Similar systems

Share the most building blocks with this one.

In-house systems

Named systems of this kind at large companies, with their paper or write-up.