Overview

The bahmni.appointment.forwarder plugin is an OpenMRS OMOD that intercepts Bahmni appointment events and forwards them to a central RabbitMQ broker. It uses Spring AOP to hook into the AppointmentsService without modifying any Bahmni code. Every time an appointment is created or rescheduled, the plugin publishes a JSON event to the appointment.exchange topic exchange.


Prerequisites

The following must be installed on the build machine:

SoftwareVersionPurpose
Java JDK21Running OpenMRS, building the plugin requires JDK 8+
Maven3.xBuilding the plugin
GitAnyCloning repositories
Docker + Docker ComposeAnyRunning OpenMRS locally

The following must be built and installed to your local Maven repository before building the plugin:

  • openmrs-module-event built from master branch (installs as event-api:4.1.0-SNAPSHOT)
  • openmrs-module-appointments built from tag 2.1.0 (installs as appointments-api:2.1.0)

Build the event module

git clone git@github.com:openmrs/openmrs-module-event.git
cd openmrs-module-event
mvn clean install -DskipTests

Build the appointments module

git clone git@github.com:Bahmni/openmrs-module-appointments.git openmrs-module-appointments-2.1
cd openmrs-module-appointments-2.1
git checkout 2.1.0
mvn clean install -DskipTests

Building the Plugin

Clone the repository and build:

cd AvansModuleOpenMRS/bahmni.appointment.forwarder
mvn clean install -DskipTests

The OMOD file is generated at:

omod/target/bahmni.appointment.forwarder-1.0.0-SNAPSHOT.omod

Deploying to OpenMRS

Step 1, Set up the modules folder

Create a modules folder next to your docker-compose.yml and copy the OMOD into it:

mkdir -p modules
cp omod/target/bahmni.appointment.forwarder-1.0.0-SNAPSHOT.omod modules/

Step 2, Update docker-compose.yml

Add a bind mount under the backend service volumes section. The full volumes block should look like this:

volumes:
  - openmrs-data:/openmrs/data
  - ./modules/bahmni.appointment.forwarder-1.0.0-SNAPSHOT.omod:/openmrs/distribution/openmrs_modules/bahmni.appointment.forwarder-1.0.0-SNAPSHOT.omod

The bind mount points to the distribution folder, not the data folder. OpenMRS copies modules from the distribution folder into the data folder on every startup. Do not copy the OMOD directly into /openmrs/data/modules/ as it will be wiped on restart.

Step 3, Start OpenMRS

docker compose up -d

Wait approximately 3 minutes for OpenMRS to fully start. Monitor startup with:

docker logs -f $(docker ps --filter name=backend -q) 2>/dev/null \
  | grep -i "done refreshing\|forwarder\|started"

Once you see the following lines the plugin is loaded:

=== BAHMNI APPOINTMENT FORWARDER CONTEXT REFRESHED ===
=== BAHMNI APPOINTMENT FORWARDER STARTED ===

Configuring Global Properties

The plugin reads all its configuration from OpenMRS global properties. Set them directly in the database after the first startup:

docker exec $(docker ps --filter name=db -q) \
  mysql -u openmrs -popenmrs openmrs -e "
INSERT INTO global_property (property, property_value, description, uuid) VALUES
('communicatie.tenantId',         'your-tenant-id',    'Tenant identifier',   UUID()),
('communicatie.rabbitmq.host',    'your-host',         'RabbitMQ host',       UUID()),
('communicatie.rabbitmq.port',    '5672',              'RabbitMQ port',       UUID()),
('communicatie.rabbitmq.username','your-username',     'RabbitMQ username',   UUID()),
('communicatie.rabbitmq.password','your-password',     'RabbitMQ password',   UUID())
ON DUPLICATE KEY UPDATE property_value=VALUES(property_value);" 2>/dev/null

Replace the placeholder values:

PropertyExampleDescription
communicatie.tenantIdhospital-amsterdamUnique ID for this OpenMRS instance
communicatie.rabbitmq.host86.48.5.113IP or hostname of the RabbitMQ server
communicatie.rabbitmq.port5672AMQP port
communicatie.rabbitmq.usernamecommunicatieRabbitMQ username
communicatie.rabbitmq.passwordyourpasswordRabbitMQ password
Do not use a URI format for the RabbitMQ connection. The plugin uses individual ConnectionFactory setters to avoid URI encoding issues with special characters in passwords.

Step 4, Restart to apply properties

docker compose restart backend

Wait for OpenMRS to start again. You should now see the RabbitMQ connection confirmed in the logs:

=== BAHMNI APPOINTMENT FORWARDER STARTED ===
=== CONNECTED TO RABBITMQ AT your-host:5672 ===

Verifying the Setup

Check the exchange exists

curl -s -u 'username:password' \
  'http://your-rabbitmq-host:15672/api/exchanges/%2F/appointment.exchange'

A JSON response describing the exchange confirms the plugin connected successfully. A 404 means the plugin has not connected yet.

Create a test queue binding

Before testing, create a queue bound to the exchange so messages are not dropped:

curl -s -u 'username:password' \
  -X PUT 'http://your-rabbitmq-host:15672/api/queues/%2F/debug.queue' \
  -H 'Content-Type: application/json' \
  -d '{"durable":true,"auto_delete":false}' 2>/dev/null
 
curl -s -u 'username:password' \
  -X POST 'http://your-rabbitmq-host:15672/api/bindings/%2F/e/appointment.exchange/q/debug.queue' \
  -H 'Content-Type: application/json' \
  -d '{"routing_key":"appointment.#"}' 2>/dev/null

Create a test appointment

curl -s -u admin:Admin123 -X POST \
  'http://localhost/openmrs/ws/rest/v1/appointment' \
  -H 'Content-Type: application/json' \
  -d '{
    "patientUuid": "YOUR_PATIENT_UUID",
    "serviceUuid": "YOUR_SERVICE_UUID",
    "startDateTime": "2026-06-01T10:00:00.000Z",
    "endDateTime": "2026-06-01T10:30:00.000Z",
    "appointmentKind": "Scheduled",
    "status": "Scheduled"
  }' 2>/dev/null

Read the message from the queue

curl -s -u 'username:password' \
  -X POST 'http://your-rabbitmq-host:15672/api/queues/%2F/debug.queue/get' \
  -H 'Content-Type: application/json' \
  -d '{"count":1,"ackmode":"ack_requeue_true","encoding":"auto"}' \
  2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
if isinstance(data, list) and data:
    payload = data[0].get('payload', '')
    print(json.dumps(json.loads(payload), indent=2))
else:
    print(json.dumps(data, indent=2))
"

Message Format

Every appointment create or reschedule publishes the following JSON payload to appointment.exchange:

{
  "tenantId":          "hospital-amsterdam",
  "eventType":         "BAHMNI_APPOINTMENT_CREATED",
  "timestamp":         "2026-05-20T13:28:00Z",
  "appointmentUuid":   "ad37be8a-227d-4486-8862-513dafb9fe77",
  "appointmentNumber": "0000",
  "appointmentKind":   "Scheduled",
  "patientUuid":       "c35a048b-c311-4f5d-8a21-322f469d16e3",
  "patientName":       "Richard Nelson",
  "patientIdentifier": "10001LL",
  "phoneNumber":       "+31612345678",
  "emailAddress":      "richard.nelson@test.com",
  "startDateTime":     "2026-05-31T10:00:00Z",
  "endDateTime":       "2026-05-31T10:30:00Z",
  "serviceName":       "General Medicine",
  "serviceUuid":       "3bd2766b-538d-11f1-a6ad-a2e877b8b22b",
  "status":            "Scheduled",
  "comments":          null,
  "voided":            false
}

Routing key format: appointment.{tenantId}.{eventType} Event types:

  • BAHMNI_APPOINTMENT_CREATED, fired when validateAndSave is called
  • BAHMNI_APPOINTMENT_UPDATED, fired when rescheduleAppointment is called

Phone number and email are fetched from OpenMRS person attributes using the following attribute type UUIDs:

AttributeUUID
Telephone Number14d4f066-15f5-102d-96e4-000c29c2a5d7
Email Addressc6b52b5c-5376-11f1-90b7-2a50222d1421

These UUIDs are hardcoded in AppointmentForwarderAdvice.java. If your OpenMRS instance uses different UUIDs for these attribute types, update the constants in that file and rebuild.


How It Works

The plugin uses Spring AOP AfterReturningAdvice registered on org.openmrs.module.appointments.service.AppointmentsService via the config.xml advice block. This means every time a method on that service returns successfully, our advice code runs automatically.

The advice:

  1. Checks if the intercepted method is validateAndSave or rescheduleAppointment
  2. Reads the returned Appointment object via reflection (direct class reference causes classloader issues)
  3. Fetches patient phone number and email from OpenMRS via Context.getPersonService()
  4. Builds the JSON payload
  5. Publishes to appointment.exchange with the appropriate routing key The RabbitMqPublisher is a static singleton that reconnects automatically if the channel drops.

Troubleshooting

Plugin not appearing in the module list

Check that the OMOD file is in the distribution folder inside the container:

docker exec $(docker ps --filter name=backend -q) \
  ls /openmrs/distribution/openmrs_modules/ | grep forwarder

If it is missing, the bind mount in docker-compose.yml is not working. Verify the path is correct and restart with docker compose down && docker compose up -d.

Plugin appears but does not start

Check the logs for errors:

docker logs $(docker ps --filter name=backend -q) 2>/dev/null \
  | grep -i "forwarder\|bahmni.appointment.forwarder" | tail -20

Common causes:

  • Old lib-cache conflict. Fix: sudo rm -rf /var/lib/docker/volumes/.../_data/.openmrs-lib-cache/bahmni.appointment.forwarder and restart
  • Spring XML bean declarations in moduleApplicationContext.xml. Fix: leave the file empty with only the root <beans> tag

RabbitMQ not connecting

Check that the global properties are set correctly:

docker exec $(docker ps --filter name=db -q) \
  mysql -u openmrs -popenmrs openmrs \
  -e "SELECT property, property_value FROM global_property WHERE property LIKE 'communicatie%';" 2>/dev/null

Then restart the backend so the plugin reads the updated values.

Events not being published

The AOP advice only intercepts validateAndSave and rescheduleAppointment on AppointmentsService. Appointments must be created through the standard Bahmni Appointments REST API at /openmrs/ws/rest/v1/appointment. Direct database inserts will not trigger the advice.

ClassNotFoundException on startup

The appointments-api or event-api jar bundled in the OMOD does not match the version installed in OpenMRS. Rebuild the dependencies locally from the correct tags and rebuild the plugin.

Old OMOD cached after redeployment

OpenMRS caches extracted OMOD contents in .openmrs-lib-cache. After replacing the OMOD file, clear the cache and restart:

sudo rm -rf /var/lib/docker/volumes/YOUR_VOLUME/_data/.openmrs-lib-cache/bahmni.appointment.forwarder
docker restart YOUR_BACKEND_CONTAINER