Skip to main content

Immunizations Service — Application Logic

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template

1. Commands

CommandUse case classKey stepsRoles
RecordAdministrationCommandRecordAdministrationUseCaseValidate patient, resolve vaccine code, check contraindications, persist record, refresh forecast, emit immunization.recordedCLINICIAN, NURSE, VACCINATION_OFFICER
RecordRefusalCommandRecordRefusalUseCaseValidate patient, persist not-done record with refusal reason, emit immunization.refusedCLINICIAN, NURSE, VACCINATION_OFFICER
AmendImmunizationCommandAmendImmunizationUseCaseOptimistic lock check, validate allowed amendment fields (lot, site, route, notes), persist, emit immunization.updatedCLINICIAN, VACCINATION_OFFICER
CorrectImmunizationCommandCorrectImmunizationUseCaseTransition to entered-in-error, audit previous state, emit immunization.updatedCLINICIAN, ADMIN
ImportHistoricalRecordCommandImportHistoricalRecordUseCaseAccept source reference, persist with status historical, trigger forecast refresh, emit immunization.reportedCLINICIAN, DATA_MIGRATION_ROLE
AddContraindicationCommandAddContraindicationUseCasePersist ContraindicationRecord, invalidate forecast for affected antigenCLINICIAN
RemoveContraindicationCommandRemoveContraindicationUseCaseMark contraindication inactive, refresh forecastCLINICIAN
RefreshForecastCommandRefreshForecastUseCaseLoad EPI schedule, load patient records, compute recommendations, persist forecast, emit forecast.updatedSystem (internal), CLINICIAN
SyncToRegistryCommandSyncToRegistryUseCaseBuild RegistrySyncJob, write to outbox, interop-service sends VXUSystem (scheduled), ADMIN

2. Queries

QueryUse case classDescriptionRoles
GetImmunizationByIdQueryGetImmunizationUseCaseSingle record by immunizationIdCLINICIAN, NURSE, PATIENT (own)
ListPatientImmunizationsQueryListPatientImmunizationsUseCaseAll records for a patient; filterable by status, vaccine code, date rangeCLINICIAN, NURSE, PATIENT (own)
GetForecastQueryGetForecastUseCaseCurrent forecast for a patientCLINICIAN, NURSE, PATIENT (own)
ListDefaultersQueryListDefaultersUseCasePatients with overdue doses; filterable by facility, antigen, days overdueVACCINATION_OFFICER, ADMIN
GetCoverageReportQueryGetCoverageReportUseCasePopulation coverage by antigen and facility; date-range aggregationADMIN, ANALYST
GetImmunizationCertificateQueryGetImmunizationCertificateUseCaseGenerate signed JWT certificate for patientCLINICIAN, PATIENT (own)
SearchImmunizationsQuerySearchImmunizationsUseCaseFull-text + filter search across tenantCLINICIAN, ADMIN
GetRegistrySyncStatusQueryGetRegistrySyncStatusUseCaseSync job status and last sync timestampADMIN

3. Sequence Diagrams

3.1 Record Administration

Clinician → API: POST /v1/immunizations { patientId, vaccineCode, doseNumber, lotNumber, administeredAt, ... }
API → RecordAdministrationUseCase: execute(command)
RecordAdministrationUseCase → PatientPort: assertPatientActive(patientId)
RecordAdministrationUseCase → EpiSchedulePort: resolveVaccineCode(vaccineCode)
RecordAdministrationUseCase → ContraindicationRepository: findActive(patientId, vaccineCode)
alt contraindicated and no override
RecordAdministrationUseCase → API: throw ContraindicationViolationError (422)
end
RecordAdministrationUseCase → ImmunizationRepository: save(record) [+ outbox entry in transaction]
RecordAdministrationUseCase → RefreshForecastUseCase: enqueue(patientId)
OutboxRelay → NATS: IMMUNIZATIONS.immunization.recorded
NATS → communication-service: appointment reminder suppression update
NATS → analytics-service: coverage metric update
API → Clinician: 201 { immunizationId, status: "completed", ... }

3.2 Forecast Refresh

Trigger: RecordAdministrationUseCase OR scheduled cron (nightly)
RefreshForecastUseCase → EpiSchedulePort: loadSchedule(tenantId)
RefreshForecastUseCase → ImmunizationRepository: findAll(patientId, status: [completed, not-done])
RefreshForecastUseCase → ContraindicationRepository: findActive(patientId)
RefreshForecastUseCase → ForecastEngine: compute(records, contraindications, schedule)
ForecastEngine → RefreshForecastUseCase: recommendations[]
RefreshForecastUseCase → ForecastRepository: upsert(forecast) [+ outbox entry]
OutboxRelay → NATS: IMMUNIZATIONS.forecast.updated
NATS → communication-service: defaulter notification trigger

3.3 Registry Sync

Scheduler (cron) → SyncToRegistryUseCase: execute({ since: lastSyncAt })
SyncToRegistryUseCase → ImmunizationRepository: findModifiedSince(lastSyncAt)
SyncToRegistryUseCase → RegistrySyncRepository: createJob(records)
SyncToRegistryUseCase → OutboxTable: insert VXU sync command [in transaction]
OutboxRelay → NATS: IMMUNIZATIONS.registry.sync-requested
interop-service → NationalRegistry: HL7 VXU batch
NationalRegistry → interop-service: ACK / NACK
interop-service → NATS: IMMUNIZATIONS.registry.sync-completed
SyncToRegistryUseCase (listener) → RegistrySyncRepository: markComplete / markFailed

4. Ports (Hexagonal Boundaries)

Port nameDirectionPurpose
ImmunizationRepositoryPortOutboundPersist and query ImmunizationRecord aggregates
ForecastRepositoryPortOutboundPersist and query ImmunizationForecast
ContraindicationRepositoryPortOutboundPersist and query ContraindicationRecord
EpiSchedulePortOutboundLoad EPI national schedule from terminology-service or config
ForecastEnginePortInternalCompute recommendations from schedule + records
PatientPortOutboundAssert patient active status (calls registration-service)
OutboxPortOutboundWrite events to outbox table atomically
EventPublisherPortOutboundNATS JetStream publisher (used by outbox relay)
RegistrySyncPortOutboundEnqueue registry sync jobs
CertificatePortOutboundSign JWT vaccination certificates

5. Outbox and Event Patterns

  • All state-changing use cases write domain events to the outbox table within the same DB transaction as the aggregate mutation.
  • The outbox relay polls WHERE status = 'pending', publishes to NATS JetStream, then marks delivered.
  • Events are idempotent: downstream consumers must deduplicate on eventId.
  • RefreshForecastUseCase is enqueued asynchronously after record creation to avoid blocking the HTTP response. Uses BullMQ queue backed by Redis.
  • Registry sync uses a separate scheduled job (cron expression configurable) with an exponential-backoff retry policy (max 5 retries).

6. Error Handling

ErrorHTTP codeDomain code
Patient not found404PATIENT_NOT_FOUND
Patient deceased422PATIENT_DECEASED
Invalid vaccine code422INVALID_VACCINE_CODE
Active contraindication422CONTRAINDICATION_ACTIVE
Optimistic lock conflict409OPTIMISTIC_LOCK_CONFLICT
Record already entered-in-error409IMMUNIZATION_CORRECTED
EPI schedule not configured503EPI_SCHEDULE_UNAVAILABLE
Registry sync failure503REGISTRY_SYNC_FAILED
Tenant not entitled403MODULE_NOT_ENTITLED