Skip to main content

FHIR Queries

ImmunizationFhirService owns all HAPI FHIR queries. A shared FhirContext bean (R4, configured at startup) builds clients per request — no connection pooling complexity, HAPI manages timeouts internally (connect: 10 s, socket: 30 s).

Every method is @Cacheable by patientId. A cache miss triggers a real HAPI call; a hit returns from Caffeine in microseconds.

Vaccination history

Bundle bundle = client.search()
.forResource(Immunization.class)
.where(Immunization.PATIENT.hasId(ctx.getPatientId()))
.where(Immunization.STATUS.exactly().code("completed"))
.sort().descending(Immunization.DATE)
.returnBundle(Bundle.class)
.execute();

FHIR request:

GET /fhir/Immunization?patient=ePatient-8675309&status=completed&_sort=-date
Authorization: Bearer {access_token}

Only status=completed records are returned. entered-in-error and not-done records are excluded — they must not appear in the vaccination history or certificate. Results are sorted newest-first (_sort=-date).

The toVaccinationRecord() mapper extracts protocolApplied[0] for dose number and series name, performer[0].actor for the administering facility, and occurrenceDateTimeType for the date (converted to LocalDate via ZoneId.systemDefault()).

ImmunizationRecommendation

Bundle bundle = client.search()
.forResource(ImmunizationRecommendation.class)
.where(ImmunizationRecommendation.PATIENT.hasId(ctx.getPatientId()))
.returnBundle(Bundle.class)
.execute();

FHIR request:

GET /fhir/ImmunizationRecommendation?patient=ePatient-8675309
Authorization: Bearer {access_token}

Each ImmunizationRecommendation resource can contain multiple recommendation entries. The mapper iterates rec.getRecommendation() and produces one VaccineRecommendation per entry.

Date criteria are extracted by code:

CodeMeaning
30100 or Earliest-antigens-dose-datedateCriterionDue
30101 or Latest-antigens-dose-datedateCriterionLatest

Results are sorted overdue-first, then by dateCriterionDue ascending (soonest due at the top).

Patient demographics

Patient patient = client.read()
.resource(Patient.class)
.withId(ctx.getPatientId())
.execute();

FHIR request:

GET /fhir/Patient/ePatient-8675309
Authorization: Bearer {access_token}

Used on the certificate page for the patient header and by buildSummary() for the dashboard subtitle. Returns a HAPI Patient object directly — not mapped to a custom record since the full Patient resource is passed to the Thymeleaf template.

Summary aggregation

buildSummary() is not a separate FHIR query — it calls the three cached methods and counts from the results:

public ImmunizationSummary buildSummary(SmartLaunchContext ctx) {
List<VaccinationRecord> history = getVaccinationHistory(ctx); // cache hit
List<VaccineRecommendation> recs = getRecommendations(ctx); // cache hit
Patient patient = getPatient(ctx); // cache hit

long overdueCount = recs.stream().filter(VaccineRecommendation::isOverdue).count();
long dueSoonCount = recs.stream().filter(VaccineRecommendation::isDueWithin30Days).count();

return new ImmunizationSummary(
history.size(), recs.size(),
(int) overdueCount, (int) dueSoonCount,
patientDisplayName(patient),
...
);
}

The dashboard GET /dashboard call hits HAPI at most once per resource type per session — three initial cache-miss calls followed by Caffeine hits for every subsequent page load.

Error handling

All query methods catch Exception broadly:

} catch (Exception ex) {
log.error("Failed to load immunization history for patient={}: {}",
ctx.getPatientId(), ex.getMessage());
return List.of();
}

A HAPI error (403 from the scope interceptor, 404 for missing patient, network timeout) returns an empty list. The template handles empty lists by showing a "No records found" message. This is the correct clinical behaviour — a connectivity issue should not crash the app or show an error page when the user might still be able to use other views.


Next: VDS-NC Certificates →