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-servicehas no event publisher class anywhere in its source tree. When an assessment is completed, noassessment.completedevent is emitted.careplan-servicehas 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-servicehas no publisher class. The service can mark a care plan as published in the database (CarePlan.publishedAt,CarePlan.publishedByfields 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:207—case "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
@Valuedefaults (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.javacareplan-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.
No comments to display
No comments to display