Hello Community
,
We’d like to share a proposal around revamping how Bahmni’s internal modules publish and consume domain events. We’d love your feedback, especially from implementers deploying Bahmni in cloud and hybrid environments and development partners extending Bahmni features.
Current events architecture
Today, whenever a Bahmni module (core, appointments, bed-management) needs to signal a domain change — a patient saved, an encounter recorded, an appointment booked — it calls into openmrs-atomfeed directly. The atomfeed module has become the mandatory dependency for all event publishing in the atomfeed stream.
This creates real problems:
-
Every new event type requires to add dependecy for atomfeed module — the publisher, the atomfeed wiring and raising event.
-
Modules that want to publish events are forced to depend on atomfeed even if their concern has nothing to do with atom feeds.
-
Adding a new delivery channel (say, pushing events to ActiveMQ or Kafka for a cloud deployment) means adding more code into atomfeed or creating parallel plumbing from scratch — there is no clean extension point.
-
The architecture does not scale to the increasing variety of Bahmni deployment contexts, where different sites need different event pipelines.
-
The current events also makes it complex to perform simple operations within OpenMRS context like email sending etc.
In short: atomfeed works well for Bahmni today built as a delivery mechanism but has ended up with strong interdependent coupling points for all of event publishing and lacks extension to publish to more modern event handling systems.
What we’re proposing — Spring Events as the internal bus
The fix is to introduce a clean, decoupled internal event contract using Spring’s ApplicationEvent mechanism.
A shared interface EMREvent<T> is defined in bahmni-common-interfaces:
// Any module publishes a domain event — no knowledge of who's listening
applicationEventPublisher.publishEvent(new EMREvent<>(patient));
Any module that cares about that event registers an @EventListener independently:
// openmrs-atomfeed listens to spring events instead of AOP
@EventListener
public void onPatientSave(EMREvent<Patient> event) { ... }
// communications-omod (new)
@EventListener
public void onPatientSave(EMREvent<Patient> event) { ... }
EMREvent<T> implements Spring’s ResolvableTypeProvider so generic types resolve correctly at runtime — EMREvent<Patient> and EMREvent<Encounter> are treated as distinct event types despite Java’s type erasure.
Key shift: publishers fire and forget . They no longer need to know about atomfeed, ActiveMQ, or any downstream system. The Spring Events bus routes events to whoever is listening. The events come into action only when a listener is attached.
Extensibility — Built for Cloud and Varied Deployments
This is where the architecture really opens up. The Spring Events bus becomes the stable contract. Adding support for a new broker or platform simply means writing a new listener module — nothing in the existing publishers changes.
The architecture looks like this:
Option 1 (Recommended): Single Event Infrastructure
flowchart LR
subgraph BCI["bahmni-common-interfaces"]
SE["EMREvent<T> implements ResolvableTypeProvider"]
end
BC[bahmni-core] -.->|depends on| BCI
AP[appointments] -.->|depends on| BCI
OAF[bahmni-event-publisher] -.->|depends on| BCI
subgraph TX["Single Transaction — Outbox Pattern"]
BC -->|"publishes EMREvent<Patient>, EMREvent<Encounter>"| SEC[Spring Events Context]
AP -->|"publishes EMREvent<Appointment>"| SEC
BM[bed-management] -->|"publishes EMREvent<BedAssignment>"| SEC
SEC -->|"@EventListener(EMREvent<Patient>) @EventListener(EMREvent<Encounter>) @EventListener(EMREvent<Appointment>) @EventListener(EMREvent<BedAssignment>)"| OAF
OAF --> ERQ[(event_records_queue)]
end
ERQ -->|Option 1| OMAF[ OpenMRS Scheduler -- openmrs-atomfeed]
OMAF --> ER[(event_records)]
ERQ -->|Option 2| ES["external-service(reads from activemq_events_queueand pushes to ActiveMQ)"]
ES --> AMQ([ActiveMQ])
Option 2: Multiple Event Infrastructure (Implementation specific needs)
flowchart LR
subgraph BCI["bahmni-common-interfaces"]
SE["EMREvent<T> implements ResolvableTypeProvider"]
end
BC[bahmni-core] -.->|depends on| BCI
AP[appointments] -.->|depends on| BCI
OAF[bahmni-event-publisher] -.->|depends on| BCI
subgraph TX["Single Transaction — Outbox Pattern"]
BC -->|"publishes EMREvent<Patient>, EMREvent<Encounter>"| SEC[Spring Events Context]
AP -->|"publishes EMREvent<Appointment>"| SEC
BM[bed-management] -->|"publishes EMREvent<BedAssignment>"| SEC
SEC -->|"@EventListener(EMREvent<Patient>) @EventListener(EMREvent<Encounter>) @EventListener(EMREvent<Appointment>) @EventListener(EMREvent<BedAssignment>)"| OAF
OAF --> ERQ[(event_records_queue)]
SEC -->|"@EventListener(EMREvent<Patient>) @EventListener(EMREvent<Appointment>)"| OAM[aactivemq-event-publisher -- implementation specific]
OAM --> AEQ[(activemq_events_queue)]
end
ERQ --> OMAF[ OpenMRS Scheduler -- openmrs-atomfeed]
OMAF --> ER[(event_records)]
AEQ --> ES["external-service(reads from activemq_events_queueand pushes to ActiveMQ)"]
ES --> AMQ([ActiveMQ])
Each delivery path is fully independent. Adding a Kafka path tomorrow is just a new listener module and a new poller — zero touch to core, appointments, or bed-management.
Why not openmrs-module-event?
We have also looked whether the existing openmrs-module-event can fill this role. Unfortunately it can’t — and it’s worth being explicit about why.
openmrs-module-event is a post-commit, Hibernate-triggered, JMS-based notification bus. It fires events automatically when Hibernate detects an entity save/update/delete, and delivers them to ActiveMQ after the transaction has already closed. That makes it difficult for Bahmni needs in two fundamental ways:
-
Transaction boundary — Bahmni’s outbox pattern requires the listener to write to a DB table inside the same transaction as the business operation. This provides guarantee that all the events are published to the outbox in a transaction or rolled back happens cleanly. That window is gone in
openmrs-module-event— listeners run after commit. -
No type-safe Spring Events —
openmrs-module-eventuses JMSMessageListenerwith untypedMapMessagepayloads (classname and uuid as strings). There is noEMREvent<Patient>concept, no@EventListener, and no way to distinguish event types without manual string parsing.
Additionally, it requires an ActiveMQ broker to be running even for deployments that only need Atom Feed, and its JMS delivery path is hardwired — there is no extension point for Kafka or other brokers.
This proposal is complementary to openmrs-module-event, not a replacement for it. The Spring Events bus handles in-process, transactional routing. openmrs-module-event could still serve as a lightweight trigger for other use cases where post-commit, loosely typed notifications are sufficient.
What this is NOT changing
-
The outbox pattern that already exists in Bahmni remains intact. Event listeners still write to DB outbox tables within the business transaction, and async pollers handle delivery. That reliability guarantee is preserved.
-
The Atom Feed continues to work as before — it simply becomes one listener among potentially many, rather than the central coupling point.
-–
Whats critical breaking change:
While we do this revamp and the atomfeed events are going to work the same as it was before, there is going to be an additional dependency on the modules that publishes events.
As an example if some implementation is using openmrs-module-appointments outside of Bahmni distribution, there is going to be an additional lightweight dependency on the bahmni-common-interfaces OMOD that needs to be added.
What’s next
-
Finalising the
EMREvent<T>contract inbahmni-common-interfaces -
Migrating
bahmni-coreandappointmentsto publish via Spring Events -
Refactoring
openmrs-atomfeedto consume via@EventListenerrather than being called directly for writing to event_records_queue and dependent on AOP. -
Documenting the extension contract so the community can build broker adapters
We’d love your input on the revamp of the events architecture.
Looking forward to the discussion!
@dkayiwa @mseaton @ibacher @angshuonline @binduak @supriyam @rahu1ramesh @sthote @ramashish @burke @sumazmrs @akhilmalhotra @dmukungi @rohit.v @mogoodrich