Revamping Bahmni's Event Driven Architecture

Hello Community :waving_hand:,

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&lt;T&gt; 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&lt;Patient&gt;, EMREvent&lt;Encounter&gt;"| SEC[Spring Events Context]
        AP -->|"publishes EMREvent&lt;Appointment&gt;"| SEC
        BM[bed-management] -->|"publishes EMREvent&lt;BedAssignment&gt;"| SEC

        SEC -->|"@EventListener(EMREvent&lt;Patient&gt;) @EventListener(EMREvent&lt;Encounter&gt;) @EventListener(EMREvent&lt;Appointment&gt;) @EventListener(EMREvent&lt;BedAssignment&gt;)"| 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&lt;T&gt; 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&lt;Patient&gt;, EMREvent&lt;Encounter&gt;"| SEC[Spring Events Context]
        AP -->|"publishes EMREvent&lt;Appointment&gt;"| SEC
        BM[bed-management] -->|"publishes EMREvent&lt;BedAssignment&gt;"| SEC

        SEC -->|"@EventListener(EMREvent&lt;Patient&gt;) @EventListener(EMREvent&lt;Encounter&gt;) @EventListener(EMREvent&lt;Appointment&gt;) @EventListener(EMREvent&lt;BedAssignment&gt;)"| OAF
        OAF --> ERQ[(event_records_queue)]

        SEC -->|"@EventListener(EMREvent&lt;Patient&gt;) @EventListener(EMREvent&lt;Appointment&gt;)"| 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:

  1. 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.

  2. No type-safe Spring Eventsopenmrs-module-event uses JMS MessageListener with untyped MapMessage payloads (classname and uuid as strings). There is no EMREvent<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 in bahmni-common-interfaces

  • Migrating bahmni-core and appointments to publish via Spring Events

  • Refactoring openmrs-atomfeed to consume via @EventListener rather 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

This looks like a non bahmni specific requirement. Instead of doing it in bahmni-common-interfaces, have you evaluated having it in openmrs-core, such that the entire openmrs ecosystem benefits and maintains it? FWIW, @raff has started doing some of this: Jira

Just in case you need this too: Jira

2 Likes

Great post @mohant .

This is no longer true as of version 4.0.0 (recently released) of event. See EVNT-36 and this commit EVNT-36 - Improve implementation of how events are fired (#21) · openmrs/openmrs-module-event@49265a8 · GitHub in which a new EntityEvent class was introduced, and Spring event firing that happens at multiple stages of the transaction lifecycle, and then backwards-compatibility with the previous asynchronous, post-transaction JMS implementation by listening on this new mechanism.

And as @dkayiwa points out, @raff has been doing additional work around OpenMRS events for the community. I would definitely second @dkayiwa ‘s notion that it would be great to all be trying to evolve in the same direction, with shared, well-tested underlying frameworks.

2 Likes

Awesome!. We absolutely love to have this on OpenMRS core. That would really help a lot of implementations.

We will go through the attached JIRA tickets and get back. We can have a sub-group huddle or have a discussion in platform call.

Thanks @dkayiwa for sharing details