Skip to main content

Architecture — Event Bus Map

LumisCare Event Bus — Current State Map

Date: 2026-04-04 Author: Martin Kleppmann (Distributed Systems Audit) Method: Static analysis of all publisher and listener classes in backend/services/ Source of truth: Actual Java source files, not design documents


Architecture

Azure Service Bus Premium tier, topics/subscriptions model. Each service owns its own topic and publishes events to it. Consumers subscribe to specific topics with SQL filters on event_type.

The design document (Service-Bus-Subscriptions-Design.md) specifies three topics: visit-events, schedule-events, notification-events. The actual implementation has diverged — publishers use five distinct topic names across six services, and the notification-service listener is wired to a different topic (domain-events) than what the visit and scheduling publishers write to.


Event Flow Diagram

graph LR
  subgraph WIRED["FULLY WIRED"]
    HR["hr-service<br/>(HrEventPublisher)"]
    SCHED["scheduling-service<br/>(AppointmentEventPublisher)"]
    SCHED_AVAIL["scheduling-service<br/>(HrAvailabilityUpdatedEventHandler)"]
    NOTIF_PUB["notification-service<br/>(NotificationEventPublisher)"]
  end

  subgraph PARTIAL["PARTIALLY WIRED — publisher exists, consumer absent or mis-wired"]
    VISITS["visits-service<br/>(VisitEventPublisher)"]
    INCIDENTS["incidents-service<br/>(EventPublisher)"]
    SAFETY["safety-service<br/>(SafetyEventPublisher)"]
    NOTIF_LISTEN["notification-service<br/>(ServiceBusEventListener)"]
    SCHED_CAREPLAN["scheduling-service<br/>(CarePlanPublishedEventHandler)"]
  end

  subgraph BROKEN["BROKEN — no publisher class exists"]
    ASSESS["assessment-service"]
    CAREPLAN["careplan-service"]
    FINANCE["finance-service<br/>(OutboxEvent entity only)"]
  end

  HR -->|"employee.created/updated<br/>availability.updated<br/>leave.approved<br/>certification.expiring<br/>topic: domain-events"| SCHED_AVAIL
  HR -->|"leave.approved<br/>topic: domain-events"| NOTIF_LISTEN
  VISITS -->|"visit.started/completed/missed<br/>visit.task.completed<br/>topic: visits-events"| NOTIF_LISTEN
  VISITS -->|"visit.completed<br/>topic: visits-events"| FINANCE_STUB["finance-service<br/>(NO listener class)"]
  SCHED -->|"schedule.published/updated<br/>appointment.created/updated/cancelled<br/>topic: scheduling-events"| NOTIF_LISTEN
  NOTIF_PUB -->|"notification.sent/failed/acknowledged<br/>topic: notification-events"| ANALYTICS_STUB["analytics-service<br/>(NOT IMPLEMENTED)"]
  INCIDENTS -->|"incident.created<br/>topic: incidents-events"| NOTIF_LISTEN
  SAFETY -->|"lwp.escalation.*<br/>topic: safety-events"| NOTIF_LISTEN
  CAREPLAN_STUB["careplan-service<br/>(NO publisher)"] -.->|"careplan.published — MISSING"| SCHED_CAREPLAN
  ASSESS -.->|"assessment.completed — MISSING"| CAREPLAN_STUB

  style WIRED fill:#1a4731,color:#fff
  style PARTIAL fill:#713f12,color:#fff
  style BROKEN fill:#7f1d1d,color:#fff

Critical Architectural Finding: Topic Name Mismatch

The notification-service ServiceBusEventListener subscribes to topic domain-events (default configured at ServiceBusConfig.java:29):

azure.servicebus.topic-name:domain-events

The visits-service VisitEventPublisher publishes to topic visits-events (default configured at VisitEventPublisher.java:45):

azure.servicebus.visits-topic-name:visits-events

The scheduling-service AppointmentEventPublisher publishes to topic scheduling-events (default configured at AppointmentEventPublisher.java:45):

azure.servicebus.scheduling-topic-name:scheduling-events

The HR-service HrEventPublisher publishes to topic domain-events (default configured at HrEventPublisher.java:40):

azure.servicebus.topic-name:domain-events

This means: the notification-service listener will receive HR events but will silently miss all visit and schedule events at runtime unless the application properties are overridden in deployment configuration. There are no application.properties or application.yml files with explicit topic overrides found in the source tree — only Spring @Value defaults. This must be validated against the actual Azure Container Apps environment variables before treating any visit-to-notification flow as working.


Event-by-Event Status

Event Topic (default) Producer Service Publisher Class Consumer Service Listener Class Status
employee.created domain-events hr-service HrEventPublisher scheduling-service HrAvailabilityUpdatedEventHandler WIRED — both sides present
employee.updated domain-events hr-service HrEventPublisher scheduling-service HrAvailabilityUpdatedEventHandler WIRED — both sides present
availability.updated domain-events hr-service HrEventPublisher (via AvailabilityPreferencesService) scheduling-service HrAvailabilityUpdatedEventHandler (listens for hr.availability.updated) WIRED — subject used in code is hr.availability.updated, design doc says availability.updated — schema mismatch risk
leave.approved domain-events hr-service HrEventPublisher (via LeaveManagementService) notification-service ServiceBusEventListener (case leave.approved) WIRED — both sides on same topic
certification.expiring domain-events hr-service HrEventPublisher (via CertificationExpiryScheduler) notification-service ServiceBusEventListener (case certification.expiring) WIRED — both sides on same topic
visit.started visits-events visits-service VisitEventPublisher.publishVisitStarted() notification-service ServiceBusEventListener (case visit.checkedin — event name mismatch) BROKEN — publisher emits visit.started, listener handles visit.checkedin; different names
visit.completed visits-events visits-service VisitEventPublisher.publishVisitCompleted() notification-service ServiceBusEventListener (case visit.completed) TOPIC MISMATCH — publisher on visits-events, listener on domain-events
visit.completed visits-events visits-service VisitEventPublisher.publishVisitCompleted() finance-service No listener class found NOT WIRED — finance has no Service Bus consumer
visit.missed visits-events visits-service VisitEventPublisher.publishVisitMissed() notification-service No explicit case in listener NOT HANDLED — listener has no visit.missed case
visit.task.completed visits-events visits-service VisitEventPublisher.publishTaskCompleted() notification-service No explicit case in listener NOT HANDLED
checkin.timeout visits-events visits-service NOT FOUND in VisitEventPublisher methods notification-service ServiceBusEventListener (case checkin.timeout) PUBLISHER ABSENT — listener expects this event but publisher has no publishCheckinTimeout() method
assistance.requested visits-events visits-service NOT FOUND in VisitEventPublisher methods notification-service ServiceBusEventListener (case assistance.requested) PUBLISHER ABSENT — listener expects this event but publisher has no method
task.flagged visits-events visits-service NOT FOUND in VisitEventPublisher methods notification-service ServiceBusEventListener (case task.flagged) PUBLISHER ABSENT — listener expects this but only visit.task.completed is published
redflag.detected unknown unknown NOT FOUND notification-service ServiceBusEventListener (case redflag.detected) PUBLISHER ABSENT — no publisher produces this event
visit.reassigned unknown unknown NOT FOUND notification-service ServiceBusEventListener (case visit.reassigned) PUBLISHER ABSENT
schedule.published scheduling-events scheduling-service AppointmentEventPublisher.publishSchedulePublished() notification-service ServiceBusEventListener (case schedule.published) TOPIC MISMATCH — publisher on scheduling-events, listener on domain-events
schedule.updated scheduling-events scheduling-service AppointmentEventPublisher (no explicit publishScheduleUpdated found — has publishScheduleApproved) notification-service ServiceBusEventListener (case not found) PARTIAL — method naming diverges from design doc
careplan.published careplan-events careplan-service NO PUBLISHER CLASS scheduling-service CarePlanPublishedEventHandler (subscribes to careplan-events) BROKEN — listener fully implemented, publisher does not exist
careplan.updated unknown careplan-service NO PUBLISHER CLASS notification-service ServiceBusEventListener (case careplan.updated) BROKEN — no publisher
assessment.completed unknown assessment-service NO PUBLISHER CLASS careplan-service NO LISTENER CLASS NOT WIRED — entire chain absent
incident.created incidents-events incidents-service EventPublisher notification-service ServiceBusEventListener — no incident.created case found in routing switch TOPIC MISMATCH + case absent — publisher on incidents-events, listener on domain-events
lwp.escalation.level1/2/3/4 safety-events safety-service SafetyEventPublisher.publishLWPEscalation() notification-service ServiceBusEventListener (cases lwp.escalation.level1 through level4) TOPIC MISMATCH — publisher on safety-events, listener on domain-events
invoice.created none finance-service NO PUBLISHER — only OutboxEvent entity + repository notification-service ServiceBusEventListener (no case found) NOT WIRED — outbox pattern incomplete, no dispatcher daemon
invoice.paid none finance-service NO PUBLISHER analytics-service NOT IMPLEMENTED NOT WIRED
timesheet.created none finance-service NO PUBLISHER — TimesheetBatchJobProcessor writes OutboxEvent but no Service Bus dispatch notification-service ServiceBusEventListener (no case found) NOT WIRED — outbox records written to DB but no relay to Service Bus
notification.sent notification-events notification-service NotificationEventPublisher.publishNotificationSent() analytics-service NOT IMPLEMENTED PUBLISHER WIRED, consumer service does not exist
notification.failed notification-events notification-service NotificationEventPublisher.publishNotificationFailed() analytics-service NOT IMPLEMENTED PUBLISHER WIRED, consumer service does not exist
daily.summary unknown unknown NOT FOUND notification-service ServiceBusEventListener (case daily.summary) PUBLISHER ABSENT

Broken Chains

Chain 1: Assessment to Care Plan AI Generation (Clinical Safety Risk)

The primary clinical workflow trigger is broken at its first link.

  • assessment-service has no event publisher class anywhere in its source tree. When an assessment is completed, no assessment.completed event is emitted.
  • careplan-service has no event listener class. Even if an event were emitted, nothing would receive it.
  • Both sides of the Assessment → CarePlan trigger are absent.

Where it breaks: assessment-service/src/main/java/ — no *Publisher*.java file exists in this directory.

Chain 2: Care Plan to Scheduling (Scheduling Cannot Start Automatically)

The CarePlanPublishedEventHandler in scheduling-service is fully implemented (300+ lines, idempotency logic, recurring schedule creation). It is waiting for a careplan.published event on topic careplan-events. That event is never published.

  • careplan-service has no publisher class. The service can mark a care plan as published in the database (CarePlan.publishedAt, CarePlan.publishedBy fields exist) but does not emit the event.

Where it breaks: careplan-service/src/main/java/ — no *Publisher*.java file exists. The handler waiting for the event is at scheduling-service/src/main/java/com/lumiscare/scheduling/event/CarePlanPublishedEventHandler.java:101.

Chain 3: Visit Check-in to Notification (Event Name Mismatch)

The design document specifies visit.checkedin. The VisitEventPublisher publishes visit.started (set at VisitEventPublisher.java:127). The ServiceBusEventListener routes on visit.checkedin (at ServiceBusEventListener.java:207). These names do not match. Even if the topic mismatch were fixed, this chain would still silently fail.

Where it breaks:

  • Publisher sets name: VisitEventPublisher.java:127"visit.started"
  • Listener routes on: ServiceBusEventListener.java:207case "visit.checkedin"

Chain 4: Visit Completed to Finance (Finance Cannot Generate Invoice Line Items)

VisitEventPublisher.publishVisitCompleted() publishes to topic visits-events. The finance-service has no ServiceBusProcessorClient, no listener class, and no subscription configuration. The billing data embedded in the visit.completed payload (rates, task categories, mileage) is never consumed. Invoice line item generation is not automated.

Where it breaks: finance-service/src/main/java/ — no Service Bus consumer class of any kind. Finance only writes OutboxEvent records to its own database for the timesheet batch job, which itself has no Service Bus dispatcher.

Chain 5: Critical Safety Alerts Silently Dropped (Topic Mismatch)

SafetyEventPublisher publishes to topic safety-events. ServiceBusEventListener in notification-service subscribes to topic domain-events. The LWP escalation events (lwp.escalation.level1 through level4) will never reach the notification service unless an application property override is set in deployment. For lone worker protection, this is a CQC compliance risk.

Where it breaks: SafetyEventPublisher.java:39 (topic: safety-events) vs ServiceBusConfig.java:29 (subscribed to: domain-events).

Chain 6: Visit Events Unreachable by Notification Service (Topic Mismatch)

VisitEventPublisher publishes to visits-events. AppointmentEventPublisher publishes to scheduling-events. ServiceBusEventListener subscribes to domain-events. Unless deployment environment variables override these defaults, all visit and schedule events are invisible to the notification service. This silences checkin.timeout, task.flagged, assistance.requested, and schedule.published notifications.

Where it breaks: Topic default misalignment across three services — see Critical Architectural Finding section above.

Chain 7: Events Listened For But Never Published

The ServiceBusEventListener routing switch includes cases for events that have no publisher anywhere in the codebase:

Event Missing Publisher Listener Location
checkin.timeout No publisher method in VisitEventPublisher ServiceBusEventListener.java:207
assistance.requested No publisher method in VisitEventPublisher ServiceBusEventListener.java:211
redflag.detected No publisher anywhere in any service ServiceBusEventListener.java:215
visit.reassigned No publisher anywhere in any service ServiceBusEventListener.java:220
task.flagged No publisher method in VisitEventPublisher (publishes visit.task.completed instead) ServiceBusEventListener.java:224
daily.summary No publisher anywhere in any service ServiceBusEventListener.java:249

Chain 8: Notification Outbound Events Have No Consumers

NotificationEventPublisher publishes notification.sent, notification.failed, and notification.acknowledged to topic notification-events. The analytics-service and audit-service that should consume these are not implemented as standalone services. The events will be published to a topic with no subscribers, age to TTL, and be discarded.


Fix Priority

Ordered by clinical and operational impact.

Priority 1 — Fix Topic Name Coherence (All Services)

Impact: Unblocks all visit and schedule notifications in one config change.

The notification-service ServiceBusConfig must subscribe to multiple topics, or all publishers must align on a shared topic name. The least-invasive fix is to add per-topic subscription processors in ServiceBusConfig.java for visits-events and scheduling-events in addition to domain-events. Alternatively, if the design intent is a single domain-events topic, all publishers must update their @Value defaults to domain-events.

This is a configuration and wiring change, not a logic change. No new business logic required.

Files to change:

  • notification-service/src/main/java/com/lumiscare/notification/config/ServiceBusConfig.java
  • OR all publisher @Value defaults (4 publisher files)

Priority 2 — Fix visit.started / visit.checkedin Event Name

Impact: Allows check-in notifications to reach care managers.

Either rename the event type in VisitEventPublisher.java:127 from "visit.started" to "visit.checkedin", or update the listener case to "visit.started". The design document and listener both say visit.checkedin — the publisher is the outlier.

File to change: visits-service/src/main/java/com/lumiscare/visits/event/VisitEventPublisher.java:127

Priority 3 — Add Missing Publisher Methods to VisitEventPublisher

Impact: Enables checkin.timeout (clinical safety), task.flagged (care quality), assistance.requested (lone worker safety).

VisitEventPublisher needs three new publish methods. The listener routing and notification handlers for these events already exist. This is purely adding publisher-side methods and calling them from the appropriate visit execution service layer.

Events to add: checkin.timeout, task.flagged, assistance.requested File to change: visits-service/src/main/java/com/lumiscare/visits/event/VisitEventPublisher.java

Priority 4 — Add careplan.published Publisher to careplan-service

Impact: Allows scheduling to automatically create recurring visits when a care plan is approved. This is the central automation trigger for care delivery.

The CarePlanPublishedEventHandler in scheduling-service is complete and production-ready. Only the publisher side is missing. Add a publisher class to careplan-service that emits careplan.published on topic careplan-events when a care plan transitions to published status (the state transition already exists in CarePlanMapper.java).

Files to create/change:

  • Create: careplan-service/src/main/java/com/lumiscare/event/CarePlanEventPublisher.java
  • Wire it into the publish endpoint in the careplan service controller

Priority 5 — Add assessment.completed Publisher to assessment-service

Impact: Triggers automatic AI care plan generation after assessment completion.

Create an event publisher in assessment-service and publish assessment.completed when an assessment reaches a completed status. The careplan-service needs a corresponding listener (this does not yet exist either — both sides must be built).

Files to create:

  • assessment-service/src/main/java/.../event/AssessmentEventPublisher.java
  • careplan-service/src/main/java/.../event/AssessmentCompletedEventListener.java

Priority 6 — Add Finance Service Bus Consumer for visit.completed

Impact: Automates invoice line item generation from completed visits.

Add a ServiceBusProcessorClient to finance-service that subscribes to the visits-events topic with filter event_type = 'visit.completed'. The billing data payload already contains all required fields (billing_data section with visit_type, care_types, day_type, travel_time_minutes, mileage_miles).

Files to create:

  • finance-service/src/main/java/.../event/VisitCompletedEventListener.java
  • Wire to existing rate calculation and invoice generation logic

Priority 7 — Complete the Finance Outbox Dispatcher

Impact: Enables timesheet.created and invoice.created notifications.

TimesheetBatchJobProcessor writes OutboxEvent records to the database but the comment in the code (TimesheetBatchJobProcessor.java:35) refers to an "external outbox-processor daemon" that does not exist. Either implement the outbox polling scheduler within the finance-service or replace the outbox pattern with a direct ServiceBusSenderClient call inside the existing transaction.

Files to change:

  • finance-service/src/main/java/com/lumiscare/icon/finance/service/TimesheetBatchJobProcessor.java
  • Optionally create a dedicated outbox publisher component

Priority 8 — Resolve hr.availability.updated vs availability.updated Event Name

Impact: Prevents silent message drops when carer availability changes.

HrAvailabilityUpdatedEventHandler expects message subject hr.availability.updated (at HrAvailabilityUpdatedEventHandler.java:94). HrEventPublisher publishes with event type availability.updated (called at AvailabilityPreferencesService.java:161). This is a subject field mismatch. The handler will silently complete all messages that do not match its expected subject, meaning availability changes will not trigger schedule recomputation.

Files to change: Align the event type string in AvailabilityPreferencesService.java:161 to hr.availability.updated, or update the handler constant at HrAvailabilityUpdatedEventHandler.java:94.


Summary Counts

Category Count
Fully wired event chains (publisher + consumer on matching topic) 3 (employee.created, employee.updated, leave.approved/certification.expiring via domain-events)
Partially wired (publisher exists, consumer absent or topic mismatch) 8
Events listener handles but no publisher produces 6
Events completely absent (no publisher, no consumer) 3 (assessment.completed, invoice.created, invoice.paid)
Publisher classes that exist 6 (HrEventPublisher, VisitEventPublisher, AppointmentEventPublisher, NotificationEventPublisher, SafetyEventPublisher, incidents EventPublisher)
Publisher classes missing 2 (assessment-service, careplan-service)
Services with no Service Bus integration at all 3 (assessment-service, careplan-service, finance-service consumer side)
Topic name mismatches between publisher default and notification-service subscription 3 (visits-events, scheduling-events, incidents-events, safety-events vs domain-events)

The HR to Notification chain (leave.approved, certification.expiring) is the only end-to-end path that works without deployment configuration overrides. Everything that involves visit execution events, schedule events, or safety escalations depends on either a topic name fix or missing publisher code.