Skip to main content

Architecture

Component overview

┌──────────────────────────────────────────────────────────────┐
│ Referral Module :8083 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ REST API /api/referrals │ │
│ │ │ │
│ │ POST /api/referrals (create draft) │ │
│ │ POST /api/referrals/{id}/send (consent gate) │ │
│ │ PATCH /api/referrals/{id}/tasks/{taskId} │ │
│ │ POST /api/referrals/{id}/revoke │ │
│ │ GET /api/referrals/inbox (receiving queue) │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────▼──────────────┐ │
│ │ ReferralWorkflowEngine │ │
│ │ (state machine) │ │
│ └────┬──────────┬───────────┘ │
│ │ │ │
│ ┌────────────▼──┐ ┌───▼────────────────┐ │
│ │ ConsentGate │ │ ReferralFhirSync │ │
│ │ Service │ │ Service │ │
│ │ │ │ @Async │ │
│ │ Calls Consent │ │ │ │
│ │ Manager │ │ ServiceRequest │ │
│ │ /evaluate │ │ + Task → HAPI │ │
│ └───────────────┘ └─────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ JPA (PostgreSQL) │ │
│ │ referral_record · referral_task │ │
│ │ referral_audit_event │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │ │
Consent Manager HAPI FHIR Auth Server
:8082 :8080 :9000
/api/consent ServiceRequest JWKS
/evaluate Task JWT validation
AuditEvent

State machine

ReferralWorkflowEngine enforces all valid transitions. Invalid transitions throw ReferralWorkflowException which the API returns as HTTP 422.

                    ┌──────────────────────┐
┌──────────┤ DRAFT │
│ └──────────────────────┘
│ send() │
│ (consent │ (consent gate fails
│ gate) │ → stays DRAFT)
│ │
▼ ▼
┌──────────────┐ ┌──────────┐
│ ACTIVE │ │ DRAFT │ (unchanged)
└──────┬───────┘ └──────────┘

┌────┴──────────────────────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ ONHOLD │ │ REVOKED │
└────┬─────┘ └──────────────┘
│ resume()

ACTIVE (again)

Task transitions:
REQUESTED → RECEIVED → ACCEPTED → INPROGRESS → COMPLETED
↘ REJECTED

When a Task reaches COMPLETED, the parent ReferralRecord automatically transitions to COMPLETED.

The consent gate is the core integration point with the Consent Manager. It runs inside the send() transition — the referral cannot move from DRAFT to ACTIVE without passing the gate.

// For each resource type in sharedResourceTypes:
POST /api/consent/evaluate
?patientId=Patient/ePatient-8675309
&actorReference=Device/{receivingFacilityClientId}
&resourceType=Observation
&httpMethod=GET

Fail-closed: Any error from the Consent Manager — HTTP 4xx, 5xx, timeout, connection refused — is treated as a denial. The referral stays in DRAFT.

All-or-nothing: Every resource type in sharedResourceTypes must be permitted. If Observation is permitted but Condition is denied, the entire referral is blocked. The error message tells the clinician exactly which resource types need patient consent.

Error response (422):

{
"code": "CONSENT_GATE_FAILED",
"message": "Cannot send referral — patient has not consented to share Condition with akhester-smart-client. Ask the patient to grant consent via the patient portal."
}

The message includes the receiving facility's client_id so the clinician can tell the patient which application needs consent.

Data model

referral_record
├── id, version (optimistic lock)
├── fhir_service_request_id (HAPI logical ID)
├── patient_id, requester_id, performer_id
├── receiving_facility_client_id (for consent gate)
├── referral_code (SNOMED CT)
├── referral_display
├── priority: ROUTINE/URGENT/ASAP/STAT
├── clinical_note
├── status: DRAFT/ACTIVE/ONHOLD/COMPLETED/REVOKED
├── occurrence_date
├── authored_on (set when ACTIVE)
├── consent_verified (true after gate passes)
├── sending_organisation_id
└── receiving_organisation_id

referral_reason_code (element collection)
└── reason_code: ICD-10 or SNOMED CT

referral_shared_resource (element collection)
└── resource_type: e.g. "Observation"

referral_supporting_info (element collection)
└── fhir_reference: e.g. "DocumentReference/doc-001"

referral_task
├── id, version
├── fhir_task_id (HAPI logical ID)
├── referral_record_id (FK)
├── owner_id (receiving facility practitioner/org)
├── status: REQUESTED/RECEIVED/ACCEPTED/REJECTED/INPROGRESS/COMPLETED
├── description, note, rejection_reason
└── last_modified

referral_audit_event (append-only)
├── referral_record_id (FK, nullable on delete)
├── event_type, action, outcome
├── patient_id, agent_id
├── source_facility, target_facility
├── fhir_resource_type, fhir_resource_id
├── consent_checked (boolean)
├── consent_result: permit/deny/not_required
└── fhir_audit_event_id (cross-reference to HAPI)

FHIR resource mapping

ServiceRequest

{
"resourceType": "ServiceRequest",
"status": "active",
"intent": "order",
"priority": "routine",
"subject": { "reference": "Patient/ePatient-8675309" },
"requester": { "reference": "Practitioner/dr-smith" },
"performer": [{ "reference": "Organization/royal-london-cardiology" }],
"code": {
"coding": [{ "system": "http://snomed.info/sct", "code": "103696004",
"display": "Patient referral to cardiologist" }]
},
"reasonCode": [{ "coding": [{ "system": "http://hl7.org/fhir/sid/icd-10", "code": "I20.9" }] }],
"note": [{ "text": "Chest pain and elevated troponin." }],
"authoredOn": "2025-06-01T14:32:00Z",
"extension": [{
"url": "https://ajfhir.org/fhir/StructureDefinition/receiving-facility-client-id",
"valueString": "akhester-smart-client"
}]
}

Task

{
"resourceType": "Task",
"status": "accepted",
"intent": "order",
"focus": { "reference": "ServiceRequest/sr-P8675309-cardio-001" },
"owner": { "reference": "Organization/royal-london-cardiology" },
"description": "Cardiology referral — appointment scheduling in progress",
"lastModified": "2025-06-02T09:15:00Z"
}

Next: Workflow →