Recipe 5.7 Architecture and Implementation: Longitudinal Patient Matching Across Name Changes
Companion to Recipe 5.7: Longitudinal Patient Matching Across Name Changes. This page covers the AWS architecture, services, prerequisites, and pseudocode. For the problem framing and the conceptual approach, start with the main recipe.
The AWS Implementation
Why These Services
Amazon DynamoDB for the temporal-identity store. The longitudinal-name-change recipe is fundamentally a record-keeping problem on top of an entity-resolution problem. The identity record (with its current name, prior names, aliases, sensitivity classifications, supporting-document references, and audit-event history) is read on every chart-rendering, every cross-facility match, every release-of-information request, and every analytics pipeline that needs to deduplicate across name variants. DynamoDB's low-latency item reads support those operational paths, and the table's keying scheme (identity_id as partition key, with name-event versions as sort-key items under the identity) supports point-in-time queries for "what was this patient's name as of date X." A second table holds the active-search index for matcher input (current-name and prior-name lookups for incoming records); the search index is rebuilt from the canonical identity store on every change.
Amazon S3 for the supporting-document store and the audit archive. Court orders, marriage certificates, divorce decrees, driver's-license scans, and patient-signed attestations all land in S3 with SSE-KMS encryption and Object Lock in Compliance mode for the audit-archive bucket. The retention floor is typically the longer of the institution's general medical-records retention, the document-specific retention floor where applicable, and the regulatory retention floor for identity-related records. The identity record in DynamoDB points at the S3 object; the document itself is not duplicated.
AWS Glue and Apache Spark for the historical backfill and periodic reconciliation. Standing up the longitudinal-matching pipeline involves a one-time backfill that re-evaluates years of historical records against whatever name-change evidence is available, and the periodic reconciliation re-runs the matcher across the population to catch name changes that the front-line registration workflow missed. Typical reconciliation cadence is monthly for the cross-organizational reconciliation (checking the institution's population against incoming cross-facility refresh signals) and quarterly for the full retrospective sweep (re-evaluating the entire population for indirect name-change candidates that the per-event flow missed). Both are bulk-batch operations over hundreds of thousands to tens of millions of records, and Spark on Glue is the right substrate for the workload pattern.
AWS Lambda for the per-event detection and resolution path. When a registration update arrives with a different name from the existing identity, when a payer-eligibility refresh detects a name discrepancy, when a vital-records feed delivers a name-change event, when a document upload provides supporting evidence, the per-event Lambda invokes the detector, the resolver, and the persistence step on a single identity. Each invocation is in VPC with VPC endpoints for downstream services. The bulk Glue jobs handle the population-scale work; the Lambdas handle the operational stream.
Amazon SageMaker for the matcher calibration. The name-change-specific Fellegi-Sunter weights, the per-feature thresholds, the name-pair-plausibility scoring (which pairs of names are plausibly a legal-change variant of one another), and the temporal-tolerance values are calibrated against an institutional gold set. The calibration runs as a SageMaker training job over the historical name-change records, produces a candidate configuration set, and emits the metrics the institutional review committee uses to decide on promotion. SageMaker Processing jobs run the cohort-stratified accuracy reports.
Amazon SQS for the review queue and the propagation queue. Two queues: a name-change review queue (for self-asserted-without-document changes that fall in the medium-confidence band, and for indirect-detection cases below the auto-resolve threshold) and a propagation queue (for fanning out resolved name-change events to downstream consumers that maintain derived state). Separating the queues lets the operational pipeline absorb bursts (a bulk vital-records feed delivery) without delaying the higher-priority review flow.
AWS Step Functions for orchestration. Three workflows: a per-event resolution workflow (detect, evaluate, persist, propagate), a periodic reconciliation workflow (sweep the population looking for name-change candidates that the per-event flow missed), and an invalidation workflow (subscribe to superseding events and selectively re-resolve). The state machine handles retries, error routing to DLQs, and parallel execution where the dependency graph allows.
Amazon EventBridge for the cross-recipe events. When a name change is resolved (identity_name_change_resolved), when a name change is invalidated (identity_name_change_invalidated), when a sensitivity classification is updated (identity_sensitivity_updated), an event flows out to the local MPI (recipe 5.1), the cross-reference table (recipe 5.4), the cross-facility matcher (recipe 5.5), the claims-clinical linkage (recipe 5.6), the chart-rendering layer, the release-of-information workflow, and the patient-portal services. EventBridge rules route events to the right consumer, with DLQs for failed deliveries.
Event-schema contract. Every event on the name-change-resolved and name-change-invalidated buses carries a standardized envelope: source (producing Lambda or Glue job ARN), detail_type (e.g. identity_name_change_resolved), detail.identity_id, detail.event_id, detail.previous_state, detail.new_state, detail.detected_at, detail.matcher_config_version, detail.evidence_summary, detail.sensitivity_class, detail.access_control_envelope_id, and detail.access_control_envelope_version. Routing uses two channels: a standard channel for non-restricted sensitivity classes (full consumer fan-out), and a restricted channel for sensitivity-classified events (reduced consumer fan-out limited to consumers whose IAM roles carry the sensitivity-aware policy). Downstream consumers acknowledge processing via a CloudWatch metric (NameChangeEventProcessed with dimensions ConsumerId and EventType); missing acknowledgments within the SLA trigger a re-delivery alarm. Same chapter pattern as 5.5, 5.6.
Amazon HealthLake for the FHIR-native rendering view. Where the institution stores clinical resources in HealthLake, the longitudinal-name-change recipe writes the patient's name history into the FHIR Patient resource's name list (with use codes of official, usual, old, maiden, etc., and period to capture the effective span). The chart-rendering layer reads the FHIR Patient resource and applies the institution's display rules (current-name primary, prior-name suppression for restricted classes, prior-name display for treatment use cases).
AWS Lake Formation for column-level and row-level access control on the identity store's analytics surface. Different audiences see different views. Treatment-context users see the current name with the prior-name display governed by the sensitivity classification. Operations users see the linkage but a constrained view of the prior names. Research users (for de-identified analytics) see the linked identity with no name detail at all. Lake Formation grants enforce the row-and-column distinctions; Athena query paths use the same grants. Same chapter pattern as 5.2, 5.3, 5.4, 5.5, 5.6.
AWS KMS, CloudTrail, CloudWatch. Customer-managed keys for the identity store, the supporting-document bucket, the audit archive, and the Lambda log groups. CloudTrail data events on the identity table and the audit-archive bucket. CloudWatch alarms on review-queue depth and aging, on detection-rate drops (sudden drops are usually a data-source outage), on cohort-stratified disparities, and on invalidation backlog depth. Same chapter pattern as 5.1, 5.4, 5.5, 5.6.
Amazon QuickSight for operational and quality dashboards. Per-cohort name-change-detection rate, per-cohort review-queue depth and aging, per-source name-change rate (registration-asserted, payer-asserted, vital-records-asserted, document-asserted), time-to-resolve distribution (how long does a self-asserted change sit in pending state before a supporting document arrives), audit-volume trends, and per-jurisdiction policy-overlay activity.
Architecture Diagram
flowchart LR
subgraph Trigger_Sources
T1[Registration update<br/>front-desk workflow]
T2[Payer eligibility refresh<br/>recipe 5.4 feed]
T3[Vital-records feed<br/>where available]
T4[Cross-facility match<br/>recipe 5.5 callback]
T5[Document upload<br/>court order, marriage cert,<br/>license scan]
T6[Patient-portal app<br/>connection]
T7[Bulk reconciliation<br/>periodic Glue sweep]
end
T1 --> EB0[EventBridge<br/>name-change-trigger bus]
T2 --> EB0
T3 --> EB0
T4 --> EB0
T5 --> EB0
T6 --> EB0
EB0 --> L1[Lambda<br/>detect-name-change-candidate]
L1 --> L2[Lambda<br/>resolve-name-change]
L2 --> X1[(DynamoDB<br/>identity-temporal-name-store)]
L2 --> X2[(DynamoDB<br/>active-search-index)]
L2 --> Q1[SQS<br/>name-change-review-queue]
L2 --> S1[(S3 audit archive<br/>Object Lock Compliance)]
T7 --> SF1[Step Functions<br/>periodic-reconciliation workflow]
SF1 --> G1[Glue Job<br/>population-sweep]
G1 --> X1
G1 --> X2
G1 --> Q1
G1 --> S1
T5 --> S2[(S3 supporting-document-store<br/>SSE-KMS, BAA-eligible)]
S2 -.referenced_by.-> X1
Q1 --> RV1[Review tooling<br/>HIM and patient-experience staff]
RV1 --> X1
RV1 --> SM1[SageMaker<br/>calibration training]
SM1 --> CFG1[(Versioned matcher<br/>configuration store)]
CFG1 --> L1
CFG1 --> L2
CFG1 --> G1
X1 --> EB1[EventBridge<br/>name-change-resolved bus]
EB1 -->|FanOut| C5[Local MPI<br/>recipe 5.1]
EB1 -->|FanOut| C6[Cross-Reference Table<br/>recipe 5.4]
EB1 -->|FanOut| C7[Cross-Facility Matcher<br/>recipe 5.5]
EB1 -->|FanOut| C8[Claims-Clinical Linkage<br/>recipe 5.6]
EB1 -->|FanOut| C9[Chart-Rendering Layer]
EB1 -->|FanOut| C10[Release-of-Information<br/>Workflow]
EB1 -->|FanOut| C11[Patient-Portal Services]
EB1 -->|FanOut| C12[Quality and Risk-Adjustment<br/>Pipelines]
EB1 -->|FanOut| H1[(Amazon HealthLake<br/>FHIR Patient.name list)]
subgraph Invalidation
L3[Lambda<br/>invalidate-on-event]
EB1 -->|corrections, reversals,<br/>identity merges,<br/>sensitivity updates,<br/>document upgrades| L3
L3 --> Q2[SQS<br/>invalidation-queue]
Q2 --> L2
end
X1 --> SS1[(S3 derived<br/>identity-history snapshots<br/>for analytics)]
SS1 --> GC1[Glue Catalog]
GC1 --> AT1[Athena]
AT1 --> LF1[Lake Formation<br/>column/row access]
AT1 --> QS1[QuickSight<br/>operational and quality<br/>dashboards]
X1 --> AUD1[(S3 audit archive<br/>Object Lock Compliance)]
style X1 fill:#9ff,stroke:#333
style X2 fill:#9ff,stroke:#333
style S1 fill:#cfc,stroke:#333
style S2 fill:#cfc,stroke:#333
style SS1 fill:#cfc,stroke:#333
style AUD1 fill:#cfc,stroke:#333
style EB0 fill:#f9f,stroke:#333
style EB1 fill:#f9f,stroke:#333
style H1 fill:#fc9,stroke:#333
style CFG1 fill:#ffc,stroke:#333
Prerequisites
| Requirement | Details |
|---|---|
| AWS Services | Amazon DynamoDB, Amazon S3, AWS Glue, Apache Spark on Glue, AWS Lambda, AWS Step Functions, Amazon EventBridge, Amazon SQS, Amazon SageMaker, Amazon Athena, AWS Lake Formation, Amazon HealthLake (where used for FHIR-native rendering), Amazon QuickSight, AWS KMS, Amazon CloudWatch, AWS CloudTrail. |
| External Inputs | Trigger sources: registration system updates, payer eligibility refresh feeds (output of recipe 5.4), state vital-records feeds where contractually available, cross-facility match callbacks (output of recipe 5.5), document uploads (court orders, marriage certificates, divorce decrees, driver's-license scans), patient-portal app connections, bulk historical reconciliation runs. Cross-recipe dependencies: the local MPI from recipe 5.1, the address standardization from recipe 5.3, the eligibility cross-reference from recipe 5.4. Reference data: name-pair-plausibility models (which pairs of names are plausible legal-change variants of one another), nickname dictionaries, transliteration mappings, jurisdiction-specific naming-convention rules. |
| IAM Permissions | Per-Lambda least-privilege: scoped dynamodb:GetItem / PutItem / UpdateItem / Query on the identity-temporal-name and active-search-index tables, s3:GetObject / PutObject on specific bucket prefixes for the supporting-document and audit-archive buckets, events:PutEvents on the name-change-resolved bus, kms:Decrypt on relevant CMKs, sqs:SendMessage on the review and propagation queues. The persistence Lambda has append-only permissions on the identity-event history (no delete) enforced through IAM condition keys plus DynamoDB resource-based policy. SageMaker training jobs have read access to the curated identity-store snapshots and write access to a model-artifacts bucket. Per-Glue-job execution-role binding so Step Functions invokes only the role appropriate for the current pipeline stage. Never use * actions or * resources in production. |
| BAA and Trust Framework | AWS BAA signed. Vital-records feeds (where available) operate under state-level data-use agreements that constrain how the data may be used and retained. Patient-self-asserted name changes carry per-event consent metadata captured at intake: patient-consent-for-change scope (the patient consented to the change being recorded) and patient-consent-for-prior-name-display scope (the patient's preferences for who may see the prior name and under what circumstances). Jurisdictional overlays include state law on prior-name disclosure (particularly post-Dobbs reproductive-health-care state laws and post-Bostock employment-context implications where relevant to the sensitivity-classification rules). Sensitivity classification (gender-affirming, protective-custody, intimate-partner-violence) carries explicit patient preferences enforced at the access-control layer. |
| Encryption | Identity-temporal-name DynamoDB: customer-managed KMS at rest. Active-search-index DynamoDB: customer-managed KMS at rest. Supporting-document S3 bucket: SSE-KMS with bucket-level keys, restricted access policy, Object Lock optional based on document type. Audit-archive S3 bucket: SSE-KMS with bucket-level keys, Object Lock in Compliance mode. Glue temp storage: KMS-encrypted. Lambda log groups: KMS-encrypted. SageMaker: KMS-encrypted volumes and outputs. EventBridge and SQS: server-side encryption. TLS 1.2 or higher for all in-transit traffic. |
| VPC | Production: Lambdas in VPC. Glue jobs in VPC connections. SageMaker training in VPC. VPC endpoints for DynamoDB, S3, KMS, Secrets Manager, CloudWatch Logs, EventBridge, SQS, Step Functions, Glue, Athena, STS, SageMaker. NAT Gateway for outbound HTTPS to vital-records partner endpoints with an outbound proxy and an allow-list; PrivateLink where the partner offers it. Vital-records feeds and document-source partner connections use distinct outbound proxy rules with non-overlapping allow-lists scoped to compute roles; per-role rate limits set below the partner's published rate limits; egress connections are CloudWatch-logged for forensic auditing. The patient-portal-app trigger source is exempt from the partner-allow-list pattern (it authenticates inbound rather than outbound) but has its own per-patient rate-limit enforcement at the API Gateway layer. Same chapter pattern as 5.3, 5.4, 5.5, 5.6. |
| CloudTrail | Enabled with data events on the identity-temporal-name table, the active-search-index table, the supporting-document S3 bucket, and the audit-archive S3 bucket. Glue job runs and SageMaker training runs logged. CloudTrail logs encrypted with KMS and retained at the longest of: HIPAA records-retention 7-year minimum, state medical-records-retention (varies by state, often 7-10 years from last encounter), identity-document retention floor (typically 10+ years for court orders, marriage certificates, and divorce decrees), and research IRB retention where the linkage feeds an IRB-approved research substrate. For sensitivity-classified events (GENDER_AFFIRMING, PROTECTIVE_CUSTODY, IPV_RELOCATION, WITNESS_PROTECTION, PATIENT_REQUESTED_RESTRICTED), audit logs reside in a separately access-controlled S3 bucket with an additional 5-year retention floor beyond the general floor, forwarded to a dedicated audit AWS account. Audit-archive bucket uses Object Lock in Compliance mode with lifecycle to S3 Glacier Deep Archive after 90 days. CloudTrail data events forwarded to the dedicated audit AWS account for separation of duties. Same chapter pattern as 5.1, 5.4, 5.5, 5.6. |
| Reference Data and Name-Plausibility Models | A versioned reference-data store with: nickname-and-diminutive dictionaries; transliteration mappings for non-Latin scripts and diacritic handling; surname-change-pattern models (hyphenation, suffix drop, maiden-to-married, transliteration variants); naming-tradition rules (Spanish double surnames, East Asian family-name-first conventions, Arabic patronymics); per-jurisdiction policy overlays. The reference data refreshes on a regular cadence and is versioned so each name-change resolution references the model versions active at the time. |
| Sample Data | Use synthetic patient populations that include modeled name changes with diverse name-tradition representation. Synthea generates synthetic patient populations; extending Synthea to inject name-change events with timestamped supporting-document references is feasible. Never use real PHI in development environments. |
| Cost Estimate | At an institution with one million active patients and a name-change rate of approximately 2-4 percent per year (the rate varies considerably by population): DynamoDB for the identity-temporal-name and active-search-index tables typically $200-1,000 per month; S3 for supporting-document storage and audit archive typically $200-800 per month at multi-year retention; Glue compute for periodic reconciliation typically $500-2,500 per month; Lambda compute for per-event processing typically $100-500 per month; SageMaker calibration jobs typically $100-400 per month; Athena, QuickSight, EventBridge, SQS, Step Functions, KMS in aggregate typically $200-700 per month. Total AWS infrastructure typically $1,300-5,900 per month, dominated by Glue periodic reconciliation and DynamoDB. |
Ingredients
| AWS Service | Role |
|---|---|
| Amazon DynamoDB | Identity-temporal-name store (current name, prior names, aliases, sensitivity classifications, audit-event history); active-search-index for matcher input lookups |
| Amazon S3 | Supporting-document store (court orders, marriage certificates, license scans), audit-archive bucket (Object Lock in Compliance mode), derived-zone snapshots for analytics |
| AWS Glue and Apache Spark | Periodic-reconciliation Glue jobs that sweep the population for name-change candidates, historical-backfill jobs that retroactively reconcile pre-existing records |
| AWS Lambda | Per-event detection (detect-name-change-candidate), per-event resolution (resolve-name-change), invalidation handling (invalidate-on-event), read API for downstream consumers |
| AWS Step Functions | Orchestrates per-event resolution, periodic reconciliation, and invalidation workflows |
| Amazon EventBridge | Trigger-input bus for raw name-change-candidate events; resolved-output bus for fanning out resolutions to local MPI (5.1), cross-reference table (5.4), cross-facility matcher (5.5), claims-clinical linkage (5.6), chart-rendering, release-of-information, patient-portal, and analytics consumers |
| Amazon SQS | Buffers name-change review queue (medium-confidence cases) and propagation queue (downstream consumer fan-out) on separate queues |
| Amazon SageMaker | Matcher calibration over historical name-change records, name-pair-plausibility model training, cohort-stratified accuracy reports |
| Amazon HealthLake | FHIR-native rendering target where the institution stores clinical resources in HealthLake; the FHIR Patient resource's name list with use and period fields holds the time-varying name |
| Amazon Athena and AWS Glue Data Catalog | SQL access to the identity-history derived snapshots for analytics, quality measurement, equity monitoring, audit reporting |
| AWS Lake Formation | Column-level and row-level access controls for the differentiated audiences (treatment, operations, research, audit) |
| Amazon QuickSight | Operational and quality dashboards (per-cohort detection rate, per-cohort review-queue depth and aging, per-source name-change rate, time-to-resolve distribution, audit-volume trends) |
| AWS KMS | Customer-managed encryption keys for all identity data stores and supporting documents |
| Amazon CloudWatch | Operational metrics and alarms (review-queue depth and aging, detection-rate drops, cohort-stratified disparities, invalidation backlog) |
| AWS CloudTrail | Audit logging for all API calls on the identity table, the active-search-index, the supporting-document bucket, and the audit-archive bucket |
Code
Reference implementations: Useful libraries and patterns for this recipe:
- Splink: an open-source probabilistic record linkage library with strong support for time-aware comparisons; the underlying Fellegi-Sunter math powers most of what the matcher does.
recordlinkage: a Python toolkit for record linkage; useful for the candidate-generation and similarity-scoring steps.jellyfish: a Python library for approximate string matching and phonetic encoding; the workhorse for name-similarity scoring.- Anc.NicknameAndDiminutiveNamesLookup: a community-maintained nickname-and-diminutive lookup; a starting point for nickname-aware name comparison.
- The HL7 FHIR Patient resource and the HumanName datatype: the standards-based representation of the time-varying-name model.
Walkthrough
Step 1: Detect a name-change candidate from the trigger event. Every operational path that produces a name discrepancy goes through detection first. A registration update with a new name, a payer eligibility refresh with an updated name, a vital-records feed event, a cross-facility match callback that surfaced a different name on the responding side. The detector classifies the trigger as direct (an explicit name-change assertion) or indirect (a high-demographic-match arriving with a different name from the matched identity), pulls the candidate identity record, and produces a detection envelope. Skip the detection step and you treat every name discrepancy as a potential new identity, which over-creates duplicate records and corrupts the longitudinal continuity the recipe is supposed to maintain.
FUNCTION detect_name_change_candidate(trigger_event):
// Extract the asserted name and any change metadata
// from the trigger. Different sources carry different
// structure; normalize to a canonical assertion.
asserted_name = extract_asserted_name(trigger_event)
asserted_change_date = extract_change_date(trigger_event)
// May be NULL for indirect cases.
asserted_prior_name = extract_prior_name(trigger_event)
// May be NULL.
supporting_document_ref = extract_document_ref(trigger_event)
// May be NULL.
source_strength = classify_source_strength(trigger_event.source_type)
// Court order: STRONG. Marriage cert: STRONG.
// Vital records: STRONG. Payer update: MEDIUM.
// Patient self-assertion at registration: MEDIUM-WEAK.
// Cross-facility match callback: MEDIUM-WEAK.
// Indirect detection alone: WEAK.
// Step 1A: identify the candidate identity. Different
// trigger sources reference the patient differently; the
// candidate-identity lookup walks through the available
// identifiers in priority order.
candidate_identity = NULL
IF trigger_event.local_patient_id IS NOT NULL:
// Direct reference to an existing identity.
candidate_identity = identity_store.get_by_local_patient_id(
trigger_event.local_patient_id)
IF candidate_identity IS NULL
AND trigger_event.member_id IS NOT NULL
AND trigger_event.payer_id IS NOT NULL:
// Lookup via the cross-reference table from recipe 5.4.
cross_ref = cross_reference_table.lookup_by(
payer_id=trigger_event.payer_id,
member_id=trigger_event.member_id,
as_of=trigger_event.event_date)
IF cross_ref IS NOT NULL:
candidate_identity = identity_store.get_by_local_patient_id(
cross_ref.local_patient_id)
IF candidate_identity IS NULL
AND asserted_prior_name IS NOT NULL:
// The trigger asserted an explicit prior name; look
// up the identity by the prior name plus other
// demographic features.
candidate_identity = identity_store.search_by_name_and_demographics(
name=asserted_prior_name,
demographics=trigger_event.demographics,
as_of=trigger_event.event_date)
IF candidate_identity IS NULL:
// No candidate found via direct references. Run a
// demographic-based search using the asserted name
// plus other features. This is the indirect-detection
// path; the search is broader and the threshold for
// accepting a candidate is higher.
candidate_identity = identity_store.search_by_name_and_demographics(
name=asserted_name,
demographics=trigger_event.demographics,
as_of=trigger_event.event_date,
tolerance="indirect_search")
IF candidate_identity IS NULL:
// Genuinely no candidate; this is a new identity, not
// a name change. Hand off to recipe 5.1's new-record
// path.
RETURN {
classification: "NO_EXISTING_IDENTITY",
handoff_to: "new_record_path_recipe_5_1"
}
// Step 1B: classify direct vs indirect.
IF asserted_prior_name IS NOT NULL OR
asserted_change_date IS NOT NULL OR
supporting_document_ref IS NOT NULL OR
trigger_event.event_type IN ["explicit_name_change",
"vital_records_update",
"court_order_recorded"]:
change_type = "DIRECT"
ELSE:
change_type = "INDIRECT"
// Step 1C: build the detection envelope.
name_pair_plausibility = compute_name_pair_plausibility(
asserted_name=asserted_name,
candidate_current_name=candidate_identity.current_name,
candidate_prior_names=candidate_identity.prior_names,
candidate_aliases=candidate_identity.aliases,
reference_data_versions=current_reference_data_versions(),
scoring_components={
"shared_first_or_middle": shared_given_name_score,
"surname_change_pattern": surname_change_pattern_score,
// hyphenation, suffix drop, maiden-to-married,
// transliteration variant
"nickname_or_diminutive": nickname_score,
"family_name_distance": family_name_distance_score,
// family-member-confounder dampener
})
demographic_match_strength =
compute_non_name_demographic_match_strength(
trigger_event.demographics,
candidate_identity.demographics_as_of(
trigger_event.event_date))
temporal_plausibility = compute_temporal_plausibility(
asserted_change_date=asserted_change_date,
candidate_identity_creation_date=
candidate_identity.creation_date,
candidate_existing_name_history=
candidate_identity.name_history)
detection_score = combine_detection_signals({
name_pair_plausibility: name_pair_plausibility,
demographic_match_strength: demographic_match_strength,
temporal_plausibility: temporal_plausibility,
source_strength: source_strength,
change_type: change_type
})
RETURN {
classification: change_type,
candidate_identity_id: candidate_identity.identity_id,
asserted_name: asserted_name,
asserted_prior_name: asserted_prior_name,
asserted_change_date: asserted_change_date,
supporting_document_ref: supporting_document_ref,
source_strength: source_strength,
detection_score: detection_score,
evidence_summary: {
name_pair_plausibility: name_pair_plausibility,
demographic_match_strength: demographic_match_strength,
temporal_plausibility: temporal_plausibility
},
reference_data_versions: current_reference_data_versions(),
matcher_config_version: matcher_config.version
}
Step 2: Resolve the candidate against the existing identity record. The detector produced a candidate; the resolver decides whether to accept it as a name change, hold it for review, or reject it. The resolver consults the identity's existing name history, applies the name-change-specific thresholds, and produces a resolution decision with the evidence preserved for audit. Skip the explicit resolution step and you have a detector that flags candidates without a clear handoff to the persistence layer; the result is name-change events that get partially recorded and then drift out of sync with the rest of the patient record.
FUNCTION resolve_name_change(detection_envelope, matcher_config):
candidate = detection_envelope
// Step 2A: re-fetch the identity record at evaluation
// time. Between detection and resolution, the identity
// may have been updated by a concurrent path (a separate
// registration update, a separate payer refresh); the
// resolver re-reads under the latest version.
identity = identity_store.get_by_id(
candidate.candidate_identity_id,
consistent_read=TRUE)
// Step 2B: apply the name-change-specific threshold.
// The thresholds for name-change events are calibrated
// separately from the demographic-match thresholds in
// recipe 5.1, because the cost-benefit profile is
// different (false acceptances of name changes corrupt
// the longitudinal record; false rejections fragment it).
IF candidate.classification == "DIRECT":
IF candidate.detection_score >= matcher_config
.DIRECT_NAME_CHANGE_HIGH:
resolution = "AUTO_RESOLVE_HIGH"
ELIF candidate.detection_score >= matcher_config
.DIRECT_NAME_CHANGE_MED:
// Direct assertion with medium-confidence
// demographic alignment. Hold for review unless
// a strong-source document is on file.
IF candidate.source_strength == "STRONG":
resolution = "AUTO_RESOLVE_MED_DOCUMENTED"
ELSE:
resolution = "REVIEW_PENDING_DIRECT"
ELSE:
resolution = "REJECT_INSUFFICIENT_EVIDENCE"
ELIF candidate.classification == "INDIRECT":
IF candidate.detection_score >= matcher_config
.INDIRECT_NAME_CHANGE_HIGH:
// Indirect detection at high confidence is rare
// and usually requires very strong demographic
// alignment plus a plausible name-change pattern.
resolution = "AUTO_RESOLVE_INDIRECT_HIGH"
ELIF candidate.detection_score >= matcher_config
.INDIRECT_NAME_CHANGE_MED:
// Indirect medium-confidence routes to review
// by default; the cost of an auto-acceptance is
// higher when there was no explicit assertion.
resolution = "REVIEW_PENDING_INDIRECT"
ELSE:
resolution = "REJECT_LIKELY_DIFFERENT_PERSON"
// Step 2C: build the resolution record.
IF resolution IN ["AUTO_RESOLVE_HIGH",
"AUTO_RESOLVE_MED_DOCUMENTED",
"AUTO_RESOLVE_INDIRECT_HIGH"]:
// Build the new name-event record. The current name
// becomes a prior name; the asserted name becomes
// the current name.
new_event = {
event_id: generate_event_id(),
event_type: "NAME_CHANGE",
previous_current_name: identity.current_name.name_string,
previous_current_name_effective_from:
identity.current_name.effective_from,
new_current_name: candidate.asserted_name,
change_effective_date:
candidate.asserted_change_date OR
infer_effective_date(candidate, identity),
source: candidate.source_strength,
source_record_id: candidate.source_record_id,
supporting_document_ref:
candidate.supporting_document_ref,
detection_score: candidate.detection_score,
evidence_summary: candidate.evidence_summary,
matcher_config_version: matcher_config.version,
reference_data_versions:
candidate.reference_data_versions,
sensitivity_class: classify_sensitivity(
candidate, identity, jurisdictional_overlays),
resolved_at: current UTC timestamp,
resolved_by: candidate.resolver_role
// "automated" for auto-resolved cases;
// a reviewer identity for human-confirmed cases.
}
RETURN {
resolution: resolution,
new_event: new_event,
updated_identity_state: compute_updated_identity_state(
identity, new_event)
}
ELIF resolution IN ["REVIEW_PENDING_DIRECT",
"REVIEW_PENDING_INDIRECT"]:
// Hold the assertion in pending state. The identity
// record is not yet modified; a pending-review item
// is recorded and surfaced to the review queue.
pending_item = {
pending_id: generate_pending_id(),
candidate_identity_id: identity.identity_id,
candidate: candidate,
held_at: current UTC timestamp,
requires: ["human_review",
IF candidate.source_strength != "STRONG"
THEN "supporting_document"
ELSE NULL]
}
SQS.SendMessage("name-change-review-queue", pending_item)
RETURN {
resolution: resolution,
pending_item: pending_item,
updated_identity_state: identity
// Identity state unchanged for pending cases.
}
ELSE:
// REJECT_INSUFFICIENT_EVIDENCE or
// REJECT_LIKELY_DIFFERENT_PERSON.
RETURN {
resolution: resolution,
rejected_candidate: candidate,
updated_identity_state: identity,
rejection_reason: resolution
}
Step 3: Apply the sensitivity and consent envelope. A resolved name change carries a sensitivity classification and an access-control envelope. The classification reflects the type of change (general, gender-affirming, protective-custody, intimate-partner-violence, witness-protection) and the patient's expressed preferences for prior-name display. The envelope encodes the access rules that downstream consumers (chart-rendering, release-of-information, patient-portal) must honor. Skip the sensitivity envelope and the prior name surfaces in places the patient did not consent to, which is a dignity violation and, in some jurisdictions, a regulatory violation.
FUNCTION apply_sensitivity_and_consent_envelope(resolution_envelope,
identity,
patient_preferences,
jurisdictional_overlays):
// The sensitivity classification is one of:
// GENERAL: no special handling beyond audit
// GENDER_AFFIRMING: the change is part of gender
// transition; specific patient-preference rules apply
// PROTECTIVE_CUSTODY: legal protective measures are in
// effect; the prior name should not surface outside
// specific authorized contexts
// IPV_RELOCATION: intimate-partner-violence safety;
// prior-name visibility is restricted
// WITNESS_PROTECTION: the strictest class; prior name
// is suppressed except for narrowly-authorized
// contexts
// PATIENT_REQUESTED_RESTRICTED: the patient has
// specifically requested restricted visibility for a
// reason that does not fit the named classes
sensitivity_class = resolution_envelope.new_event
.sensitivity_class
// Patient preferences override the default rules where
// they have been expressed. Preferences are captured at
// the time of the change (or updated later through a
// patient-portal flow).
patient_pref = patient_preferences.get_for_change_event(
resolution_envelope.new_event.event_id)
// Examples:
// { display_scope: "treatment_only" }
// { display_scope: "default" }
// { display_scope: "masked" }
// { display_scope: "archive_only",
// patient_consented_for_audit: TRUE }
// Build the access-control envelope. The envelope is a
// structured object that downstream consumers consult
// when deciding what to show, what to log, and what to
// include in releases.
envelope = {
prior_name_event_id: resolution_envelope.new_event
.event_id,
sensitivity_class: sensitivity_class,
patient_preference: patient_pref,
jurisdictional_overlay:
jurisdictional_overlays.applicable_overlays(
identity, resolution_envelope.new_event),
permitted_display_contexts: derive_display_contexts(
sensitivity_class,
patient_pref,
jurisdictional_overlays),
// Examples:
// ["treatment", "operations"] for GENERAL
// with default preference
// ["treatment_with_clinical_relevance"] for
// GENDER_AFFIRMING with masked preference
// ["audit_only"] for WITNESS_PROTECTION
permitted_release_scopes: derive_release_scopes(
sensitivity_class,
patient_pref,
jurisdictional_overlays),
// Examples:
// ["patient_access_api", "treatment_disclosure",
// "operations_disclosure"]
// ["patient_access_api"] only for restricted
// classes
audit_rules: derive_audit_rules(
sensitivity_class,
patient_pref,
jurisdictional_overlays)
// Higher-sensitivity classes may require more
// detailed audit logging for every prior-name
// disclosure.
}
RETURN envelope
Step 4: Persist the resolved name change atomically with the audit log. The persistence step writes the new event to the identity-temporal-name table, updates the active-search-index, archives the resolution to the audit S3 bucket, and emits the cross-recipe event. Use a transactional write so partial failures do not leave the system in an inconsistent state. Skip the transactional discipline and you produce identity records whose name history disagrees with the search index, which causes the matcher to make decisions on stale data and the analytics layer to deduplicate incorrectly.
FUNCTION persist_resolved_name_change(resolution_envelope,
access_control_envelope):
new_event = resolution_envelope.new_event
identity = resolution_envelope.updated_identity_state
// Step 4A: build the canonical identity-event record.
// The record is an append-only event in the identity's
// history; the current state is computed from the event
// log. This pattern lets the resolver re-derive the
// current state under any historical configuration
// (which is what the audit layer needs for
// forensic-reconstruction queries).
identity_event_record = {
identity_id: identity.identity_id,
event_version: next_event_version_for(identity.identity_id),
event_id: new_event.event_id,
event_type: "NAME_CHANGE",
event_payload: new_event,
access_control_envelope: access_control_envelope,
emitted_to_eventbus_at: NULL,
// Set by the outbox-drainer flow.
archived_to_s3_at: NULL,
// Set by the outbox-drainer flow.
created_at: current UTC timestamp
}
// Step 4B: update the active-search-index. The matcher
// reads the active-search-index for incoming-record
// lookups (current name, prior names, aliases). The
// index is a denormalized projection of the identity's
// current state; the persistence step rebuilds the
// affected entries.
new_search_index_entries = build_search_index_entries(
identity, new_event)
// Step 4C: write the event, the index update, and the
// outbox row in one transaction so partial failures do
// not leave the system inconsistent.
DynamoDB.TransactWriteItems([
PutItem("identity-temporal-name",
identity_event_record,
condition: "attribute_not_exists(event_id) AND " +
"expected_version = " +
str(identity.current_event_version)),
PutItem("active-search-index", new_search_index_entries),
PutItem("identity-event-outbox", {
outbox_id: generate_uuid(),
event_type: derive_event_type_for_emission(
access_control_envelope),
// "identity_name_change_resolved" for
// standard cases.
// "identity_name_change_resolved_restricted"
// for high-sensitivity cases that require
// restricted downstream propagation.
payload: identity_event_record,
access_control_envelope: access_control_envelope,
emitted_at: NULL,
archived_at: NULL
})
])
// Step 4D: drain the outbox. A separate Lambda or
// DynamoDB Streams consumer reads the outbox, archives
// the event to S3, emits to EventBridge, and marks the
// outbox row COMPLETED. This pattern keeps the operational
// store, the audit archive, and the event stream
// consistent even on partial failure.
// (Implementation detail; see Recipe 5.5 expert review
// A1 reference. Idempotent at outbox_id.)
RETURN identity_event_record
Step 5: Propagate the resolution to dependent stores. The downstream consumers maintain their own derived state that depends on the identity's name. The local MPI from recipe 5.1 needs the new name in its master record. The cross-reference table from recipe 5.4 may need an update if the eligibility cross-reference was keyed on the prior name in any way. The cross-facility matcher from recipe 5.5 needs to refresh its prior-name handling for query responses. The chart-rendering layer needs the updated name-history view. Skip the propagation step and the downstream consumers continue to operate on stale data, producing decisions and displays that disagree with the canonical identity store.
FUNCTION propagate_to_dependents(identity_event_record,
access_control_envelope):
// Each dependent consumer subscribes to the
// identity_name_change_resolved event via EventBridge
// rules that route based on the access_control_envelope
// (some consumers receive only events for non-restricted
// sensitivity classes; some receive all events but
// honor the envelope when consuming).
// Step 5A: emit the canonical event. Done by the
// outbox-drainer flow described in step 4D.
// Step 5B: each consumer's handler is its own function;
// the persistence layer's job here is to make the event
// visible. The consumers handle their own idempotency
// (each consumes the event_id once) and their own
// failure-and-retry behavior.
// Examples of consumer behavior:
// Local MPI (recipe 5.1):
// UPDATE master_patient_record
// SET current_name = new_event.new_current_name,
// prior_names = append(prior_names,
// new_event.previous_current_name),
// updated_at = new_event.resolved_at
// WHERE local_patient_id = identity.linked_local_mrns
// .primary;
// Cross-Reference Table (recipe 5.4):
// // The cross-reference may carry a name snapshot
// // for the eligibility-verification step; refresh
// // it under the new name.
// UPDATE eligibility_cross_reference
// SET demographic_snapshot = new_demographics,
// demographic_version = new_event.event_id,
// valid_from = new_event.change_effective_date
// WHERE local_patient_id = identity.linked_local_mrns
// .primary;
// Cross-Facility Matcher (recipe 5.5):
// // The HIE's MPI projection of this patient
// // refreshes; queries against the prior name still
// // resolve to the same identity, with the response
// // governed by access_control_envelope.
// SUBMIT mpi_refresh_event_to_hie(
// identity.identity_id,
// new_event,
// access_control_envelope);
// Claims-Clinical Linkage (recipe 5.6):
// // Re-evaluate encounter clusters whose patient
// // resolution depended on the prior name; the
// // change-effective-date constraint determines
// // which clusters are affected.
// ENQUEUE_RELINK_FOR_PATIENT(
// identity.linked_local_mrns.primary,
// change_effective_date:
// new_event.change_effective_date);
// Chart-Rendering Layer:
// // Refresh the cached chart-render templates for
// // active sessions that have this patient open.
// INVALIDATE_RENDER_CACHE(
// identity.linked_local_mrns.primary,
// honor: access_control_envelope);
// Release-of-Information Workflow:
// // Update the open-request rendering for any
// // requests in flight that involve this patient.
// INVALIDATE_ROI_RENDER_CACHE(
// identity.linked_local_mrns.primary,
// honor: access_control_envelope);
// Patient-Portal Services:
// // The patient sees her own name change reflected
// // in her portal view; the portal also updates
// // the patient-preference UI to surface
// // sensitivity-class options if applicable.
// UPDATE_PATIENT_PORTAL_VIEW(
// identity.identity_id,
// new_event,
// patient_preference_ui_state);
// Quality and Risk-Adjustment Pipelines:
// // Deduplicate the records under the unified
// // identity in the next pipeline run; mark any
// // already-emitted measure values as superseded
// // by the new identity-aware computation.
// ENQUEUE_QUALITY_REFRESH(
// identity.identity_id,
// affecting_measures:
// measures_with_lookback_covering(
// new_event.change_effective_date));
// Amazon HealthLake (FHIR-native):
// // Update the FHIR Patient resource's name list
// // with the new HumanName entry (use=official,
// // period.start=change_effective_date) and
// // demote the prior HumanName entry to use=old
// // with period.end=change_effective_date.
// UPDATE_FHIR_PATIENT_NAME_LIST(
// identity.healthlake_patient_id,
// new_event,
// honor: access_control_envelope);
RETURN propagation_status
Step 6: React to invalidation events that supersede prior resolutions. A name-change resolution is not permanent. It can be corrected (a wrong assertion was recorded), reversed (the patient changed back), superseded by an identity merge from recipe 5.1, updated by a sensitivity-classification change (the patient has expressed new preferences), upgraded by a document-strength promotion (a previously self-reported change is now backed by a court order). The invalidation pipeline subscribes to these events and selectively re-resolves the affected identities. Skip the invalidation pipeline and the resolved-name-change records drift out of sync with the rest of the institution's identity infrastructure; the drift compounds over time.
FUNCTION invalidate_on_event(invalidation_event):
// Identify which identities and which name-change events
// are affected.
IF invalidation_event.source == "correction":
// A reviewer has corrected a previously-resolved
// name-change event. Mark the prior event as
// superseded; resolve the correction as a new event
// that overrides the prior one.
affected_identity = identity_store.get_by_id(
invalidation_event.identity_id)
prior_event = affected_identity.find_event_by_id(
invalidation_event.superseded_event_id)
resolve_correction(affected_identity, prior_event,
invalidation_event.correction_payload)
ELIF invalidation_event.source == "reversal":
// The patient changed back. The prior name becomes
// the current name again; the briefly-current name
// becomes a prior name for the period of its
// currency.
affected_identity = identity_store.get_by_id(
invalidation_event.identity_id)
resolve_reversal(affected_identity,
invalidation_event.reversal_payload)
ELIF invalidation_event.source == "identity_merge":
// Recipe 5.1 merged two identities. Both name
// histories fold into the surviving identity; events
// are dated under the surviving identity but retain
// their original effective dates.
merged_identities = [
identity_store.get_by_id(
invalidation_event.merged_from_identity_id),
identity_store.get_by_id(
invalidation_event.merged_into_identity_id)]
resolve_identity_merge(merged_identities,
invalidation_event.merge_payload)
ELIF invalidation_event.source == "identity_unmerge":
// A prior merge is being reversed. Name histories
// split back to their respective pre-merge
// identities. The original effective dates are
// preserved; the merge audit-trail is preserved.
resolve_identity_unmerge(invalidation_event)
ELIF invalidation_event.source == "sensitivity_update":
// The patient (or an authorized representative) has
// updated the sensitivity classification or the
// prior-name display preference for an existing
// name-change event. Update the access_control_
// envelope on the affected event; do not modify the
// event itself.
affected_identity = identity_store.get_by_id(
invalidation_event.identity_id)
update_access_control_envelope_for_event(
affected_identity,
invalidation_event.event_id,
invalidation_event.new_envelope_payload)
ELIF invalidation_event.source == "document_upgrade":
// A previously self-reported change now has a
// supporting document (a court order arrived,
// a marriage certificate was scanned). The event's
// source_strength is upgraded; the resolution may
// move from REVIEW_PENDING to AUTO_RESOLVE if it
// was held for review.
affected_identity = identity_store.get_by_id(
invalidation_event.identity_id)
upgrade_event_with_document(
affected_identity,
invalidation_event.event_id,
invalidation_event.document_ref,
invalidation_event.document_metadata)
ELIF invalidation_event.source ==
"cross_facility_match_invalidated":
// Recipe 5.5 has retracted a cross-facility linkage
// that affected this identity's prior-name handling
// for query responses. Re-evaluate any prior-name-
// dependent search-index entries.
re_evaluate_search_index_for_identity(
invalidation_event.identity_id,
invalidation_event.affected_prior_name_event_ids)
// Emit the aggregate invalidation event for downstream
// consumers to refresh their derived state.
EventBridge.PutEvents([{
source: "longitudinal-name-change",
detail_type: "identity_name_change_invalidated",
detail: {
identity_id: invalidation_event.identity_id,
invalidation_source: invalidation_event.source,
invalidation_event_id: invalidation_event.event_id,
superseded_event_ids: list_of_superseded_event_ids,
new_state_summary: new_state_summary,
invalidated_at: current UTC timestamp
}
}])
Curious how this looks in Python? The pseudocode above covers the concepts. If you'd like to see sample Python code that demonstrates these patterns using boto3, check out the Python Example. It walks through each step with inline comments and notes on what you'd need to change for a real deployment.
Expected Results
Sample direct, high-confidence name-change resolution:
{ "identity_id": "id-internal-00874", "event_id": "evt-name-2026-04-22-12-08-44", "event_type": "NAME_CHANGE", "resolution": "AUTO_RESOLVE_HIGH", "previous_current_name": { "given": "Catherine", "middle": "Marie", "family": "Wilson", "effective_from": "2018-09-14" }, "new_current_name": { "given": "Catherine", "middle": "Marie", "family": "Hernandez", "effective_from": "2026-04-22" }, "change_effective_date": "2026-04-22", "source": "STRONG", "source_record_id": "doc-court-order-2026-04-22-001", "supporting_document_ref": "s3://supporting-documents/id-internal-00874/court-order-2026-04-22-001.pdf", "detection_score": 0.96, "evidence_summary": { "name_pair_plausibility": 0.92, "demographic_match_strength": 0.99, "temporal_plausibility": 1.00 }, "sensitivity_class": "GENERAL", "patient_preference": { "display_scope": "default" }, "permitted_display_contexts": ["treatment", "operations"], "permitted_release_scopes": [ "patient_access_api", "treatment_disclosure", "operations_disclosure" ], "matcher_config_version": "lncm-v1.7.2", "reference_data_versions": { "nickname_dictionary": "ndict-2026-q1", "surname_change_patterns": "scp-2026-q1", "transliteration_maps": "tmap-2026-q1" }, "resolved_at": "2026-04-22T12:08:44Z", "resolved_by": "automated" }
Sample direct, medium-confidence resolution that requires review:
{ "identity_id": "id-internal-02199", "event_id": "evt-name-2026-04-23-09-15-22-pending", "event_type": "NAME_CHANGE_PENDING", "resolution": "REVIEW_PENDING_DIRECT", "asserted_name": { "given": "Maria", "family": "Garcia-Lopez" }, "asserted_prior_name": { "given": "Maria", "family": "Garcia" }, "asserted_change_date": "2026-04-15", "source": "MEDIUM-WEAK", "source_record_id": "registration-update-2026-04-23-front-desk", "supporting_document_ref": null, "detection_score": 0.78, "evidence_summary": { "name_pair_plausibility": 0.86, "demographic_match_strength": 0.82, "temporal_plausibility": 0.75 }, "review_reason": "self_asserted_no_supporting_document_and_demographic_match_below_auto_threshold", "queued_for_review": true }
Sample indirect, high-confidence resolution (rare):
{ "identity_id": "id-internal-04412", "event_id": "evt-name-2026-04-23-14-44-09", "event_type": "NAME_CHANGE", "resolution": "AUTO_RESOLVE_INDIRECT_HIGH", "previous_current_name": { "given": "Margaret", "family": "Chen" }, "new_current_name": { "given": "Margaret", "family": "Chen-Patel" }, "change_effective_date": "2026-04-23", "source": "WEAK", "source_record_id": "payer-eligibility-refresh-2026-04-23-001", "detection_score": 0.91, "evidence_summary": { "name_pair_plausibility": 0.94, "demographic_match_strength": 0.97, "temporal_plausibility": 0.85 }, "interpretation": "payer_eligibility_refresh_carried_new_name_with_high_confidence_demographic_alignment_and_plausible_hyphenation_pattern", "sensitivity_class": "GENERAL", "permitted_display_contexts": ["treatment", "operations"], "permitted_release_scopes": [ "patient_access_api", "treatment_disclosure", "operations_disclosure" ] }
Sample resolution with restricted sensitivity classification:
{ "identity_id": "id-internal-07331", "event_id": "evt-name-2026-04-24-10-22-08", "event_type": "NAME_CHANGE", "resolution": "AUTO_RESOLVE_HIGH", "previous_current_name": { "given": "[suppressed]", "family": "[suppressed]", "effective_from": "[suppressed]" }, "new_current_name": { "given": "Avery", "family": "Mitchell", "effective_from": "2026-04-24" }, "change_effective_date": "2026-04-24", "source": "STRONG", "supporting_document_ref": "s3://supporting-documents/id-internal-07331/court-order-2026-04-24-002.pdf", "sensitivity_class": "GENDER_AFFIRMING", "patient_preference": { "display_scope": "masked", "patient_consented_for_audit": true, "treatment_clinical_relevance_disclosure": "permitted_when_clinically_indicated" }, "permitted_display_contexts": [ "treatment_with_clinical_relevance", "audit_only" ], "permitted_release_scopes": [ "patient_access_api" ], "audit_rules": { "every_prior_name_disclosure_logged": true, "every_prior_name_query_logged": true, "monthly_summary_to_patient_portal": true } }
Performance benchmarks (illustrative, your mileage varies):
| Metric | Status quo (no longitudinal handling) | Recipe pipeline |
|---|---|---|
| Detection of name-change events from registration updates | <30% (most updates land as overwrites with no history) | 85-95% with explicit detection |
| Detection of name-change events from indirect signals | <5% | 60-80% with periodic reconciliation |
| False-acceptance rate (name change accepted when records are different patients) | 1-3% (varies by name density) | <0.5% with conservative thresholds |
| False-rejection rate (legitimate name change missed) | 30-60% | <10% with documented sources, 15-25% with self-reported only |
| Time-to-resolve median (trigger arrival to resolution) | n/a | minutes for direct documented changes; days for self-reported pending review |
| Time-to-resolve p99 | n/a | <30 days (covers review-queue aging for low-priority cases) |
| Per-cohort linkage rate disparity (best vs worst cohort) | 0.15-0.30 (if measured at all) | <0.07 with monitoring and per-cohort tuning |
| Audit-event volume per resolved change | 1-2 (no audit beyond the update itself) | 8-15 (full event chain with sensitivity-aware logging) |
Where it struggles:
-
Self-reported, undocumented name changes with weak demographic alignment. A patient who walks in and says "I changed my name" without supporting documentation, where the demographic features only weakly support the change (an address that has not been refreshed since the prior name's currency, a phone number that has changed, missing SSN). The matcher routes to review; the review queue ages while the patient is mid-care; the patient experiences the friction of "every visit someone has to fix something." The mitigation is patient-portal flows that let the patient upload supporting documents asynchronously, plus a default to provisional acceptance for low-stakes contexts (with downgrade if the documents do not arrive within a defined window).
-
Family-member confounders during name changes. A daughter who marries and takes her husband's name now matches her father's name on the family side. A son who drops the Jr suffix when his father dies now matches his deceased father's records. The matcher has to maintain enough specificity to avoid merging across these events, which requires family-aware features (shared address-as-of-date, shared phone, age-sex-relationship constraints) layered on top of the demographic features. The mitigation is explicit family-disambiguation rules in the matcher's name-pair-plausibility scoring, calibrated against a gold set that includes intentionally-tricky family-confounder cases.
-
Names from naming traditions the matcher's reference data does not handle well. Spanish double surnames (the reference data may treat the maternal-paternal compound as two surnames or as one), East Asian family-name-first conventions (the reference data may invert the components), Arabic patronymics (the reference data may treat the binyam-ibn-sequence as a flat string), names with diacritics that the EHR strips on input. The mitigation is per-tradition reference data, cohort-stratified accuracy monitoring with alarms when the per-tradition false-rejection rate exceeds the threshold, and a feedback loop that surfaces under-handled traditions to the reference-data maintenance team.
-
Reversibility cases that touch downstream propagation. A name-change event was resolved and propagated; a reviewer determined the resolution was wrong; the invalidation pipeline retracts it. Downstream consumers that have already consumed the event need to refresh; some consumers (analytic pipelines that produced static reports) cannot retroactively un-emit their outputs. The mitigation is the invalidation event with a clear superseded-event-id reference, plus institutional discipline around acknowledging that some derived outputs (already-published quality reports, already-submitted regulatory filings) carry the prior-state assumption and require explicit annotation rather than silent revision.
-
Cross-organizational name-change propagation. A name change resolved at organization A does not automatically propagate to organization B, even when both organizations are connected through an HIE. The patient may continue to be known by her prior name at organization B until she presents there in person and updates her record, or until the cross-facility refresh path (recipe 5.5) detects the discrepancy. The mitigation is explicit cross-organization refresh signals where the trust framework allows them, plus the patient-mediated path (the patient connects her records to a personal-health-record app that propagates the new name to authorized organizations).
-
Historical-record retrofit gaps. The backfill that retroactively reconciles pre-existing records cannot recover information that was never recorded. A patient who was registered in 2008 under her current-at-the-time name, moved to a different organization for several years, and returned in 2024 under a different name, may have a 2008 record at the institution that the backfill cannot confidently link to the 2024 record without supporting evidence. The mitigation is the patient-portal flow that surfaces unresolved historical records to the patient for confirmation, plus periodic reconciliation runs as new evidence accumulates.
-
Sensitivity-classification updates that conflict with prior disclosures. A patient who initially classified a name change as GENERAL and later upgrades the classification to GENDER_AFFIRMING (with masked-display preference) creates a state in which prior-name disclosures that were appropriate at the time may no longer be appropriate going forward. The architecture cannot retract disclosures that have already left the institution, but it can update the access-control envelope for future disclosures and surface the change to the audit-monitoring layer. The mitigation is explicit handling of sensitivity-class upgrades as forward-looking constraints, with patient-facing communication about what can and cannot be retracted.
-
Pending-state aging. A self-reported change that goes to review-pending may sit in the queue for days or weeks if the supporting document does not arrive and the reviewer's prioritization defers it. The patient continues to interact with the system in the meantime, sometimes under the new name (where the front-desk staff manually applied the update at the visit) and sometimes under the prior name (in the still-canonical identity record). The mitigation is a clear default-state policy (typically: the prior name remains canonical for the matcher and the active-search-index until the pending review resolves, but the chart-rendering layer surfaces "the patient has reported a name change pending review" to the staff seeing the patient in person), plus aging alarms that escalate review-queue items above a threshold.
-
Document-strength promotion edge cases. A previously self-reported change is upgraded with a supporting document that arrives weeks later. The upgrade should promote the resolution from REVIEW_PENDING to AUTO_RESOLVE, but the resolution's effective date may need adjustment (the document indicates the legal change happened on a date earlier than the self-report date). The document-extraction confidence threshold determines auto-promotion versus review-routing. Per-document-type authority rules: a court order is authoritative for the legal-change date; a marriage certificate is authoritative for the marriage date with conditional authority for the legal-change date (subject to per-jurisdiction recording rules); a divorce decree is authoritative for the divorce date; a driver's-license scan is authoritative for the license-issuance date with no authority for the underlying legal-change date. When a promotion adjusts the effective date retroactively, consumers receive an
identity_name_change_effective_date_adjustedevent (distinct fromidentity_name_change_resolved); consumers either re-evaluate affected decisions or apply an explicit-no-retroactive-revision policy depending on consumer-specific rules. -
Multi-step name changes within the lookback window. A patient who marries, takes her husband's name, divorces, reverts to her maiden name, and then remarries and takes a different married name, all within a five-year window. The identity record carries multiple name events; the matcher and the chart-rendering layer have to navigate the history correctly. The multi-step-pattern reasoning layer in the name-pair-plausibility model evaluates whether the new name is a plausible legal-change variant of the current name, of any prior name, or of any composition of plausible transitions across prior names (calibrated against gold-set records that include multi-step histories). The chart-rendering layer's as-of-date name lookup queries the identity-temporal-name store with the historical record's date and receives the as-of-date name; the access-control-envelope evaluation considers the as-of-date name's sensitivity classification. Every disclosure of a prior name is logged in the audit log with the as-of-date the disclosure rendered for (the date context in which the name was current). The mitigation for the multi-event case is explicit support for multi-event histories in the data model, with the canonical event log preserving every transition and the rendering layer surfacing the appropriate name for each historical record's date.
-
Cohort-specific operational disparities. Patients whose name-handling involves any of the above edge cases experience more friction, and the friction is not uniform across populations. Patients with names from non-dominant-culture traditions experience the reference-data-gap edge case more often. Transgender patients experience the sensitivity-classification-update edge case at higher rates. Women in jurisdictions with high marriage-and-divorce rates experience the multi-step-history edge case at higher rates. The cohort-stratified monitoring catches the disparities; per-cohort threshold tuning and per-cohort review-queue prioritization are the operational responses.
Why This Isn't Production-Ready
The pseudocode and architecture above demonstrate the pattern. A production deployment needs to close several gaps that are intentionally out of scope for a recipe.
Patient-preference UI and consent capture. The sensitivity classification and patient-preference fields in the identity record assume that the patient has been asked. The mechanism for asking is not the matcher's job; it is the registration workflow's, the patient-portal app's, and (for clinical-care contexts) the gender-affirming-care intake workflow's. Build the patient-preference capture as a deliberate workflow with appropriate framing, training for the staff who solicit the information, and mechanisms for the patient to update preferences over time. Skip this and the access-control envelope is operating on default values that may not match the patient's actual preferences, with predictable dignity-and-trust failures.
Reference-data sourcing and maintenance. The name-pair-plausibility scoring depends on nickname dictionaries, surname-change-pattern models, transliteration maps, and per-tradition naming-convention rules. These are not free, are not static, and are not usually built well from scratch. Most institutions either license a commercial reference-data product or invest in maintaining their own internal references with a clinical-informatics-and-patient-experience team. The reference-data version drives the detection accuracy; reference-data gaps propagate into the cohort disparities. Plan the reference-data maintenance as an ongoing program with versioning, change governance, and regression-testing against gold-set name changes.
Threshold calibration and approval governance. The DIRECT_NAME_CHANGE thresholds, the INDIRECT_NAME_CHANGE thresholds, the source-strength weights, the per-feature weights, the sensitivity-classification rules, and the per-jurisdiction overlay rules live in a versioned configuration table (DynamoDB, partition key config_version). A SageMaker calibration job produces the candidate threshold set by evaluating against the institutional gold set. Before promotion, each candidate is evaluated via a per-cohort impact analysis: the candidate configuration is scored against the cohort registry (the population stratified by the cohort axes from the monitoring discipline), and the resulting per-cohort detection-rate and false-acceptance-rate are compared to the current production configuration. The patient-experience-and-dignity committee participates in the review committee with explicit dignity-stakes assessment authority: they evaluate whether the proposed configuration moves the dignity-relevant cohorts (transgender-or-gender-diverse, protective-custody, IPV-relocation) toward or away from operational friction. Each name-change event references the matcher_config_version active at decision time. Same chapter pattern as 5.1, 5.4, 5.5, 5.6.
Three review queues with sensitivity-aware tooling. The name-change review queue surfaces pending-direct and pending-indirect cases for human review; reviewers see the asserted name, the candidate identity, the demographic comparison, the name-pair plausibility breakdown, and any available supporting documents. The supporting-document review queue surfaces uploaded documents for verification and metadata extraction; reviewers verify the document type, extract the legal-change date, and link the document to the pending name-change event. The sensitivity-classification review queue surfaces patient-preference updates for verification (especially when the update is delivered through a non-standard channel like a phone call to medical records). Each review action is audited: reviewer identity (with appropriate authentication tied to the reviewer's institutional credential), decision, stated reason, configuration version active at the time, threshold version active at the time, reference-data version active at the time, and any reviewer-supplied additional context. Pre-assignment conflict-of-interest check runs against an institutional registry before a case is assigned; conflict-of-interest screening extends to known relationships beyond the standard categories for sensitivity-classification reviews. The patient-experience-and-dignity committee has oversight on sensitivity-classification reviews, with a dual-control requirement for downgrades: two reviewer signatures from the committee for staff-initiated downgrades, patient re-authentication for patient-initiated downgrades. Each review tool emits the reviewer's decision back into the matcher's training signal. Build the tools with the same care as the analytics pipeline; the matcher's accuracy depends on it.
Access-control envelope as a versioned, queryable artifact. The access-control envelope is not a bag of policy fields inline on each name event; it is a separate, versioned resource with its own persistence and query paths. The envelope-versioning store is a separate DynamoDB table keyed on (envelope_id, version) with a GSI on envelope_id pointing to the current-version item. Two write paths: (1) per-event envelope assignment, where the sensitivity step produces a reference to a versioned envelope at the time of name-change resolution; (2) envelope updates, which produce a new envelope version with a forward-only-disclosure-update framing (prior disclosures under the old envelope version remain valid as-of-their-time; future disclosures use the new version). Three read paths: (1) chart-rendering reads the envelope-as-of-now for the requesting context; (2) release-of-information reads the envelope-at-disclosure-time for the audit log (what envelope version governed this specific disclosure); (3) patient-portal-audit-summary-delivery reads the envelope-history for the time-window display. Envelope-update events flow through the same outbox-and-EventBridge-fan-out pattern as name-change resolutions, with the same access-control-aware routing (standard channel for non-restricted, restricted channel for sensitivity-classified). Consumers query by envelope_id and consume the current version or a specified historical version depending on their purpose.
Information-blocking compliance posture. The 21st Century Cures Act information-blocking provisions require that the institution release a patient's records on request, regardless of which name the records were created under. The release pipeline has to recognize the linkage (the matcher's job) and apply the patient's explicit preferences for prior-name display in the released documents (the access-control envelope's job). Build the patient-access-API release path as a deliberate workflow that consults both the linkage and the envelope, with audit logging on every release. Skip this and the institution either over-releases (exposing prior names that the patient preferred to suppress) or under-releases (failing to include records under prior names, which is increasingly characterized as information blocking).
Patient-access and provider-access read paths. Two read paths deliver the longitudinal identity to authorized consumers. Both use API Gateway with WAF and mTLS for system clients. The patient-access path: the institution's patient-portal authentication (Cognito-federated or equivalent) authenticates the patient; a Lambda authorizer binds the requesting principal to the identity_id (one patient cannot read another patient's identity record); the Lambda handler retrieves the linked records, applies the access-control envelope (honoring the patient's own prior-name display preferences), and returns the response. The provider-access path: the institution's provider-directory authentication identifies the requesting clinician; the Lambda authorizer binds the requesting principal to a treatment-relationship (the clinician has an active or recent treatment relationship with the patient); the Lambda handler retrieves the linked records, applies the access-control envelope at the dignity-compliance layer (determining which prior names are released and which are suppressed for the treatment context), and returns the response with explicit handling of prior-name disclosures. The dual-obligation enforcement is the read-path Lambda's defining responsibility: release records under prior names (information-blocking compliance) AND honor patient preferences for prior-name suppression in disclosures (patient-dignity compliance), simultaneously, per-patient, per-context, per-disclosure. The audit log records every patient-access and provider-access read with the envelope version at the time of the read. Same pattern as recipe 5.5, recipe 5.6 with recipe-specific dual-obligation elevation.
Cross-organizational propagation policy. Name-change events resolved at the institution may need to propagate to other organizations the patient has authorized for cross-facility data exchange. The propagation policy is not technical; it is governed by HIE participation agreements, patient consent, and the trust frameworks the institution operates under. Some HIE frameworks support push notifications for identity updates; others rely on pull-time refresh during query response. The architecture has to fit the institution's specific cross-organizational posture; the recipe's propagation-queue is the institution-internal portion of the broader cross-org flow.
Historical backfill plan and execution. The one-time backfill that retroactively reconciles pre-existing records is a substantial project, with cohort-stratified accuracy monitoring during the backfill (it is the one-time opportunity to surface cohort issues at scale), suppression of routine event emission during the backfill (downstream consumers refresh from a single backfill_complete marker rather than millions of individual events), governance approval at each stage, and patient-facing communication for the historical records that surface as candidates for confirmation. Plan the backfill as a project with its own timeline, its own review staffing, and its own communication strategy.
Vital-records integration where available. Where the state's vital-records agency provides feeds for legal name changes, the integration is its own subproject: the partner-agreement, the network connectivity, the data-format normalization, the privacy-and-purpose-of-use constraints, the audit requirements. The typical integration pattern is a FHIR endpoint or a flat-file feed delivered through a state HIE, with per-state authentication (mTLS or signed envelope). The data-use-agreement scope is typically constrained to identity-resolution and patient-matching purposes; the data is not generally redistributable. Tag vital-records-derived events with data_use_scope: "identity_resolution_and_patient_matching" enforced via Lake Formation grants on the analytics surface (preventing research or secondary-use queries from accessing vital-records-derived attributes directly). Per-event audit is logged with the state agency's reference identifier; per-state retention floors are incorporated into the audit-log retention floor per the retention posture above. The recipe accommodates the integration where it exists; building the integration is institutional-scale work that varies by state.
Idempotency and retry semantics. The pipeline must handle duplicate-event delivery, partner-side retries, and Glue job re-runs without producing duplicate name-change events or scrambled audit logs. Per-stage idempotency keys: detect-name-change-candidate uses (source_record_id, source_type); resolve-name-change uses (candidate_identity_id, asserted_change_date, asserted_name); apply-sensitivity-envelope uses (candidate_identity_id, event_id); persist uses (identity_id, event_id); propagate uses (event_id, consumer_id); invalidate-on-event uses (invalidation_event_source, invalidation_event_id); bulk-historical-reconciliation Glue uses (identity_id, reconciliation_run_id). Each stage has a dedicated DLQ. CloudWatch alarms fire on DLQ depth (typically > 0 records for critical paths, or > 15 minutes age for any item stuck in the DLQ). Step Functions Catch states route terminal failures to the per-stage DLQ so stuck workflows are visible. Same chapter pattern as 5.3, 5.4, 5.5, 5.6.
Cohort-stratified accuracy monitoring discipline. The CloudWatch metrics with cohort-bucket dimensions, the QuickSight dashboard, the institutional review cadence, and the disparity-alarm thresholds are architecture-level commitments, not bolt-ons. Cohort axes: name-tradition cohort (English-traditional, Spanish-double-surname, East-Asian-traditional, Arabic-patronymic, other), transgender-or-gender-diverse cohort (with patient-consented self-identification only), name-change-frequency cohort (first change, second-or-more changes), and age-of-name-change cohort (under-25, 25-45, 45+). Per-cohort metrics collected weekly: name-change detection rate, name-change false-acceptance rate, review-queue aging. Sampled error rate collected monthly via human audit of a stratified sample. Disparity calculation: absolute difference between best-rate cohort and worst-rate cohort per metric per cycle. Alarm thresholds: detection-rate disparity > 0.05 = MEDIUM alarm; false-acceptance-rate disparity > 0.01 = HIGH alarm (clinical-safety implications). Routing: alarms route to the analytics governance committee, the equity-monitoring committee, and the patient-experience-and-dignity committee with a 5-business-day SLA for triage and response plan. Remediation pathway includes explicit dignity-stakes-disparity translation: cohort-disparity metrics in this recipe measure how disparately one cohort's patients experience friction in registration, clinical-record-rendering, and release-of-information workflows. Same chapter pattern as 5.1, 5.4, 5.5.
Compliance and operational ownership. Longitudinal name-change handling sits at the intersection of registration, clinical informatics, compliance, patient experience, equity monitoring, and IT. Establish clear operational ownership: who tunes the thresholds, who reviews the cohort-disparity reports, who owns the reference-data maintenance, who handles the patient-preference UI, who responds to invalidation backlogs. The pipeline works only when the operational ownership is clear and funded.
Variations and Extensions
FHIR-native temporal-name representation. For institutions standardized on FHIR resources (with HealthLake or an equivalent), the time-varying name lives directly in the FHIR Patient resource's name list. Each HumanName entry carries a use code (official, usual, old, maiden, nickname, anonymous, temp) and a period covering its effective span. The matcher reads and writes the FHIR Patient resource directly; the access-control envelope lives in a custom extension on the Patient resource or in a parallel structured-data store.
Patient-mediated name-change propagation. Rather than relying on institutional cross-org refresh, the patient connects her records to a personal-health-record app, the app holds the current canonical view of her identity (with her explicit consent), and the app propagates name changes to authorized institutions through the FHIR endpoints each institution exposes. This pattern is becoming more practical as the Patient Access API ecosystem matures; the institution-side architecture extends to accept patient-mediated updates as a trigger source, with appropriate authentication and verification. The patient is the connecting tissue rather than the institution's MPI.
Vital-records integration where available. Where the state's vital-records agency provides a feed for legal name changes (currently limited to a small number of states, but expanding), the integration provides authoritative source-strength evidence with minimal patient effort. The architecture extension is a Lambda that consumes the vital-records feed, matches each event to the institution's identity records (using the state's identifier or via demographic matching where the state-issued identifier is not available), and triggers a high-confidence resolution path. The integration is constrained by the state's data-use agreement; typically the data is permitted only for identity-resolution and patient-matching purposes.
Tokenization-based privacy-preserving name-change reconciliation. For research datasets that combine claims-and-clinical data across organizations under privacy-preserving constraints (recipe 5.8), name changes complicate the tokenization. The tokens are derived from demographic fields including name; a name change produces a different token after the change than before. The architecture extension is a token-pair-history layer that records the relationship between the pre-change and post-change tokens for the same identity, allowing the research linkage to maintain longitudinal continuity without exposing the underlying name change. Ties to recipe 5.8 directly.
Gender-affirming-care-specific workflow integration. Where the institution operates a gender-affirming-care service line, the name-change handling integrates with the service line's intake workflow. The intake workflow captures the patient's preferences for prior-name display (default-display, treatment-only, masked-display, archive-only) at the start of care, the matcher receives the preferences as part of the trigger event, and the access-control envelope is configured at resolution time. The architecture extension is a tighter coupling between the gender-affirming-care intake and the longitudinal-name-change resolution, with patient-experience considerations driving the workflow design.
Public-health-registry-specific reconciliation. State immunization registries, cancer registries, and other public-health-reporting infrastructures accumulate records over decades and routinely encounter name-change-driven duplicates. A specialized variant of the recipe focuses on the registry context: bulk reconciliation of registry submissions with the patient's longitudinal identity, with awareness of the registry's specific consent and disclosure rules. The output deduplicates the registry's records under the unified identity while preserving the per-submission audit trail.
Cross-organizational name-change push notification. For institutions in mature HIE ecosystems where the trust framework supports it, the recipe extends to push notifications: when a name change is resolved at organization A, the HIE delivers an identity-update event to all participating organizations that have a record for the same patient and have authorized push notifications. The receiving organizations consume the event and refresh their local identity records. This pattern is uncommon but becoming more viable; it requires explicit consent and trust-framework support.
Active-learning-driven configuration tuning. As the name-change review queue resolves cases, the labels feed a periodic re-training of the matcher's thresholds and the per-feature weights. Active learning concentrates the review effort on the cases that most improve the downstream accuracy and the cohort fairness; over time, the review queue depth decreases as the matcher absorbs the labeled cases. Same chapter pattern as recipe 5.5 and 5.6.
Sensitivity-class-specific audit channels. For high-sensitivity classifications (gender-affirming, witness-protection, intimate-partner-violence relocation), the audit log goes to a dedicated, more-tightly-controlled channel with restricted access. The architecture extension is a per-sensitivity-class audit routing rule that emits to the standard audit archive for general-classified events and to a dedicated archive for restricted classes, with separate access-control posture and separate retention rules.
Audit-summary delivery to the patient. As part of the patient-experience layer, the patient may opt to receive periodic summaries of how her prior name has been disclosed, queried, or referenced. The architecture extension is a patient-portal summary-delivery service that aggregates the per-disclosure audit entries (filtered to the patient's own data) and delivers them on the patient's chosen cadence. This is particularly relevant for sensitivity-classified patients but is generally available; ties to the broader patient-access-and-consent architecture under the 21st Century Cures Act.
Multi-step name-change history with effective-date intervals. A patient with multiple name changes over a multi-decade horizon (marriage, divorce, remarriage, hyphenation drop, suffix change) accumulates a rich event history. The matcher's name-pair-plausibility scoring extends to multi-step paths: the new name is a plausible legal-change variant of the current name, of any prior name, or of any composition of plausible transitions across prior names. The architecture extension is the multi-step-pattern reasoning layer in the name-pair-plausibility model, calibrated against gold-set records that include multi-step histories.
Family-disambiguation rules. A specialized layer in the matcher that maintains tighter control over family-member confounders during name-change events. The layer carries explicit shared-attribute rules (shared address-as-of-date, shared phone, age-sex-relationship constraints) that downweight or override the name-pair-plausibility score when the candidate identity has a family member whose attributes match the trigger. The architecture extension is a per-identity family-graph snapshot that the matcher consults at evaluation time, with the family-graph maintained as part of recipe 5.3's household linkage.
Additional Resources
AWS Documentation:
- Amazon DynamoDB Developer Guide
- Amazon S3 User Guide
- Amazon S3 Object Lock
- AWS Glue Developer Guide
- AWS Lambda Developer Guide
- AWS Step Functions Developer Guide
- Amazon EventBridge User Guide
- Amazon SQS Developer Guide
- Amazon SageMaker Developer Guide
- Amazon HealthLake Developer Guide
- Amazon Athena User Guide
- AWS Lake Formation Developer Guide
- Amazon QuickSight User Guide
- AWS HIPAA Eligible Services
AWS Sample Repos:
aws-samples/serverless-patterns: API Gateway + Lambda + DynamoDB patterns applicable to the per-event detection and resolution Lambdasaws-samples/aws-glue-samples: Glue ETL patterns applicable to the periodic-reconciliation pipelineaws-samples/amazon-healthlake-samples: HealthLake patterns including FHIR Patient resource manipulation
AWS Solutions and Blogs:
- AWS Solutions Library (filter Healthcare and Life Sciences): browse for healthcare data lake and patient-data-management reference architectures
- AWS for Industries: Healthcare and Life Sciences Blog: search "patient matching," "MPI," "EMPI," "FHIR," "interoperability" for relevant deep-dives
External References (Standards):
- HL7 FHIR Patient Resource: the FHIR resource for patient identity, with the HumanName list supporting the time-varying-name model
- HL7 FHIR HumanName Datatype: the structured datatype for names, with
useandperiodfields - HL7 FHIR Patient $match Operation: the standard query operation that consumes the time-varying-name model on the responding side
- IHE PIX/PDQ Profiles: the patient-identifier-cross-reference and patient-demographics-query profiles that legacy HIE infrastructure uses
- HL7 FHIR US Core Implementation Guide: the U.S.-specific FHIR profile set including patient identity recommendations
External References (Methodology and Open Source):
- Splink: an open-source probabilistic record linkage library
recordlinkage: a Python toolkit for record linkagejellyfish: a Python library for approximate string matching and phonetic encoding- Synthea: synthetic patient population generator, useful for development and testing of patient-matching pipelines
- Anc.NicknameAndDiminutiveNamesLookup: a community-maintained nickname-and-diminutive lookup; one starting point for nickname-aware name comparison
External References (Regulatory and Industry):
- 21st Century Cures Act Information Blocking Rule: the regulatory framework that governs patient-record release including release of records under prior names
- HIPAA Privacy Rule: the foundational regulatory framework
- CMS Interoperability and Patient Access Final Rule: the rule mandating Patient Access APIs that release patient records on demand
- ONC Patient Matching: ONC's published research and guidance on patient matching
- Sequoia Project Patient Matching Framework: industry-developed framework for patient-matching practices and benchmarking
- Pew Charitable Trusts Patient Matching Research: published research on patient-matching accuracy, equity, and disparate impact
External References (Equity and Patient Experience):
- AHIMA Patient Identification Resources: AHIMA publishes practice briefs and educational materials on MPI maturity and identity resolution
- Fenway Institute Resources on Sexual Orientation and Gender Identity Data Collection: published guidance on capturing and managing identity data for LGBTQ+ patients in healthcare settings
- National LGBTQIA+ Health Education Center: published guidance on inclusive practices in healthcare data systems
Estimated Implementation Time
| Tier | Scope | Time |
|---|---|---|
| Basic | DynamoDB temporal-name store + per-event detection Lambda + per-event resolution Lambda + manual review queue + S3 audit archive + simple chart-rendering integration with current-name-only display | 4-6 months |
| Production-ready | Full pipeline with direct and indirect detection, sensitivity-classification handling, access-control envelope, periodic reconciliation backfill, integration with local MPI (5.1), eligibility cross-reference (5.4), cross-facility matcher (5.5), claims-clinical linkage (5.6), patient-portal flow for document upload, three-queue review tooling, cohort-stratified accuracy monitoring, threshold-calibration governance, complete CloudTrail and audit-retention posture, FHIR Patient resource integration where used | 9-15 months |
| With variations | Add patient-mediated propagation, vital-records integration where available, tokenization-based privacy-preserving extension, gender-affirming-care workflow integration, public-health-registry-specific reconciliation, cross-organizational push notification where supported by HIE, active-learning configuration tuning, sensitivity-class-specific audit channels, audit-summary delivery to patient, multi-step history reasoning, family-disambiguation rules | 6-12 months beyond production-ready |
โ Main Recipe 5.7 ยท Python Example ยท Chapter Preface