Skip to content

Business Events - Developer Guide

Apache Fineract publishes domain events to external message brokers in real time. Every significant state change - a loan disbursed, a repayment posted, a client created - fires a typed event that downstream systems can consume. This is the primary integration point for building event-driven architectures on top of Fineract.

How the Pipeline Works

Business event delivery is a two-stage pipeline.

Stage 1 - Event capture (transactional): When a domain action completes, BusinessEventNotifierService raises the event. The external event service writes a row to the m_external_event table with status TO_BE_SENT. This write is inside the same database transaction as the domain change - the event record is committed atomically with the business action.

Stage 2 - Event dispatch (async batch): A scheduled job (Send Async Events) polls m_external_event for TO_BE_SENT rows, serializes them as Avro, and sends them to the configured broker via an ExternalEventProducer. On success the row status moves to SENT.

Domain action (transaction)


m_external_event (TO_BE_SENT)

   [Send Async Events job]


ExternalEventProducer
  ├── KafkaExternalEventProducer   → FINERACT_EXTERNAL_EVENTS_KAFKA_ENABLED=true
  ├── JMSMultiExternalEventProducer → FINERACT_EXTERNAL_EVENTS_PRODUCER_JMS_ENABLED=true
  └── NoopExternalEventProducer    → default (no broker configured)

Storing events in the database before dispatch gives durability - broker outages do not lose events. The batch job will drain the backlog when the broker recovers.

Two separate messaging systems

Fineract uses Kafka and JMS for two independent purposes. They are configured separately and must not be confused:

  • External Business Events (fineract.events.external.*) - domain events for external integration. This guide covers this system.
  • Remote Job Messaging (fineract.remote-job-message-handler.*) - COB batch worker coordination. Internal only - not for business event consumers.

The master switch for external events is FINERACT_EXTERNAL_EVENTS_ENABLED. It defaults to false. Set it to true alongside a connector (Kafka or JMS) to start publishing events.

See the connector guides for setup:

Message Format

All events are serialized using Apache Avro. Every message uses MessageV1 as its outer envelope - the same schema regardless of event type:

json
{
  "name": "MessageV1",
  "namespace": "org.apache.fineract.avro",
  "type": "record",
  "fields": [
    { "name": "id",             "type": "long",   "doc": "The ID of the event record in m_external_event" },
    { "name": "source",         "type": "string", "doc": "Source service identifier" },
    { "name": "type",           "type": "string", "doc": "Event type name, e.g. LoanApprovedBusinessEvent" },
    { "name": "category",       "type": "string", "doc": "Event category, e.g. LOAN" },
    { "name": "createdAt",      "type": "string", "doc": "ISO_LOCAL_DATE_TIME, e.g. 2024-01-15T10:30:00" },
    { "name": "businessDate",   "type": "string", "doc": "ISO_LOCAL_DATE, e.g. 2024-01-15" },
    { "name": "tenantId",       "type": "string", "doc": "Tenant identifier, e.g. default" },
    { "name": "idempotencyKey", "type": "string", "doc": "Unique key per event for consumer deduplication" },
    { "name": "dataschema",     "type": "string", "doc": "Fully qualified Avro schema name of the payload" },
    { "name": "data",           "type": "bytes",  "doc": "The payload, Avro-serialized using the schema in dataschema" }
  ]
}

The type field identifies the event. The dataschema field tells the consumer which Avro schema to use to decode the data bytes. For example:

  • type = "LoanApprovedBusinessEvent"dataschema = "org.apache.fineract.avro.loan.v1.LoanAccountDataV1"
  • type = "SavingsDepositBusinessEvent"dataschema = "org.apache.fineract.avro.savings.v1.SavingsAccountTransactionDataV1"

All Avro schemas are in the fineract-avro-schemas module at fineract-avro-schemas/src/main/avro/.

Schema modules by domain

DomainSchema path in repoKey schemas
Loanloan/v1/LoanAccountDataV1, LoanTransactionDataV1, LoanChargeDataV1, LoanProductDataV1
Clientclient/v1/ClientDataV1
Savingssavings/v1/SavingsAccountDataV1, SavingsAccountTransactionDataV1
Fixed depositfixeddeposit/v1/FixedDepositAccountDataV1
Recurring depositrecurringdeposit/v1/RecurringDepositAccountDataV1
Groupgroup/v1/GroupGeneralDataV1
Shareshare/v1/ShareAccountDataV1
Documentdocument/v1/DocumentDataV1

Partitioning and ordering

On Kafka, the partition key is aggregateRootId - the database ID of the entity the event belongs to. This guarantees that all events for a given loan or client are delivered to the same partition in the same order they were created. Consumers can rely on within-partition ordering per entity.

Idempotency

Each event includes an idempotencyKey. Consumers should track processed keys and skip duplicates. The key is stable per event instance - a retry by the batch job will not change it.

Enabling and Disabling Individual Event Types

All event types are disabled by default even when FINERACT_EXTERNAL_EVENTS_ENABLED=true. Each must be explicitly enabled via the configuration API. Changes take effect immediately without a restart.

List all event configurations

bash
curl -k -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)" \
  https://localhost:8443/fineract-provider/api/v1/externalevents/configuration

Response:

json
{
  "externalEventConfigurations": [
    { "type": "LoanApprovedBusinessEvent",                       "enabled": false },
    { "type": "LoanDisbursalBusinessEvent",                      "enabled": false },
    { "type": "LoanTransactionMakeRepaymentPostBusinessEvent",   "enabled": false }
  ]
}

Enable specific event types

bash
curl -k -X PUT \
  https://localhost:8443/fineract-provider/api/v1/externalevents/configuration \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "externalEventConfigurations": [
      { "type": "LoanApprovedBusinessEvent",                       "enabled": true },
      { "type": "LoanDisbursalBusinessEvent",                      "enabled": true },
      { "type": "LoanTransactionMakeRepaymentPostBusinessEvent",   "enabled": true },
      { "type": "ClientCreateBusinessEvent",                       "enabled": true }
    ]
  }'

Event Catalog

Every registered event type, grouped by domain. Pre and Post variants exist for transaction events: Pre fires before commit (for validation side effects), Post fires after commit (for downstream integration - use these for external consumers).

Client

Event typeFired when
ClientCreateBusinessEventA client record is created
ClientActivateBusinessEventA client is activated
ClientRejectBusinessEventA client application is rejected

Group and Centre

Event typeFired when
GroupsCreateBusinessEventA group is created
CentersCreateBusinessEventA centre is created

Loan lifecycle

Event typeFired when
LoanCreatedBusinessEventA loan application is submitted
LoanApprovedBusinessEventA loan is approved
LoanApprovedAmountChangedBusinessEventThe approved amount is modified
LoanUndoApprovalBusinessEventLoan approval is reversed
LoanRejectedBusinessEventA loan application is rejected
LoanDisbursalBusinessEventA loan is disbursed
LoanDisbursalTransactionBusinessEventA disbursement transaction is recorded
LoanUndoDisbursalBusinessEventA disbursement is reversed
LoanUndoLastDisbursalBusinessEventThe most recent disbursement is reversed
LoanUpdateDisbursementDataBusinessEventDisbursement data is modified
LoanStatusChangedBusinessEventThe loan status changes
LoanBalanceChangedBusinessEventThe outstanding balance changes
LoanCloseBusinessEventA loan is closed
LoanCloseAsRescheduleBusinessEventA loan is closed via rescheduling

Loan transactions

Event typeFired when
LoanTransactionMakeRepaymentPreBusinessEvent / ...PostRepayment applied
LoanRefundPreBusinessEvent / ...PostLoan refund
LoanTransactionMerchantIssuedRefundPreBusinessEvent / ...PostMerchant-issued refund
LoanTransactionPayoutRefundPreBusinessEvent / ...PostPayout refund
LoanTransactionGoodwillCreditPreBusinessEvent / ...PostGoodwill credit
LoanTransactionRecoveryPaymentPreBusinessEvent / ...PostRecovery payment
LoanTransactionInterestRefundPreBusinessEvent / ...PostInterest refund
LoanForeClosurePreBusinessEvent / ...PostForeclosure
LoanCreditBalanceRefundPreBusinessEvent / ...PostCredit balance refund
LoanChargebackTransactionBusinessEventChargeback applied
LoanAdjustTransactionBusinessEventTransaction adjusted
LoanAccrualTransactionCreatedBusinessEventAccrual transaction created (COB)
LoanCapitalizedIncomeTransactionCreatedBusinessEventCapitalized income transaction created

Loan charges

Event typeFired when
LoanAddChargeBusinessEventCharge added to a loan
LoanDeleteChargeBusinessEventCharge removed
LoanUpdateChargeBusinessEventCharge modified
LoanWaiveChargeBusinessEventCharge waived
LoanWaiveChargeUndoBusinessEventCharge waiver reversed
LoanChargePaymentPreBusinessEvent / ...PostCharge payment processed
LoanChargeRefundBusinessEventCharge refund issued
LoanApplyOverdueChargeBusinessEventOverdue charge applied (COB)
LoanWaiveInterestBusinessEventInterest waived

Loan write-off and charge-off

Event typeFired when
LoanWrittenOffPreBusinessEvent / ...PostLoan written off
LoanUndoWrittenOffBusinessEventWrite-off reversed
LoanChargeOffPreBusinessEvent / ...PostLoan charged off
LoanUndoChargeOffBusinessEventCharge-off reversed

Loan rescheduling and restructuring

Event typeFired when
LoanRescheduledDueCalendarChangeBusinessEventRescheduled due to calendar change
LoanRescheduledDueHolidayBusinessEventRescheduled due to holiday
LoanRescheduledDueAdjustScheduleBusinessEventSchedule adjustment applied
LoanScheduleVariationsAddedBusinessEventSchedule variation added
LoanScheduleVariationsDeletedBusinessEventSchedule variation removed
LoanInterestRecalculationBusinessEventInterest recalculated
LoanReAgeBusinessEvent / LoanUndoReAgeBusinessEventRe-ageing applied or reversed
LoanReAgeTransactionBusinessEvent / LoanUndoReAgeTransactionBusinessEventRe-ageing transaction
LoanReAmortizeBusinessEvent / LoanUndoReAmortizeBusinessEventRe-amortization applied or reversed
LoanReAmortizeTransactionBusinessEvent / LoanUndoReAmortizeTransactionBusinessEventRe-amortization transaction

Loan officer and transfer

Event typeFired when
LoanReassignOfficerBusinessEventLoan reassigned to new officer
LoanRemoveOfficerBusinessEventOfficer removed from loan
LoanInitiateTransferBusinessEventLoan transfer initiated
LoanAcceptTransferBusinessEventTransfer accepted
LoanRejectTransferBusinessEventTransfer rejected
LoanWithdrawTransferBusinessEventTransfer withdrawn

Loan product and COB

Event typeFired when
LoanProductCreateBusinessEventA loan product is created
LoanAccountsStayedLockedBusinessEventLoans remain locked after COB run

Savings

Event typeFired when
SavingsCreateBusinessEventSavings application created
SavingsApproveBusinessEventSavings account approved
SavingsActivateBusinessEventSavings account activated
SavingsRejectBusinessEventSavings application rejected
SavingsCloseBusinessEventSavings account closed
SavingsPostInterestBusinessEventInterest posted
SavingsDepositBusinessEventDeposit made
SavingsWithdrawalBusinessEventWithdrawal made

Fixed and recurring deposits

Event typeFired when
FixedDepositAccountCreateBusinessEventFixed deposit account created
RecurringDepositAccountCreateBusinessEventRecurring deposit account created

Shares

Event typeFired when
ShareAccountCreateBusinessEventShare account created
ShareAccountApproveBusinessEventShare account approved
ShareProductDividentsCreateBusinessEventShare dividend created

Datatables

Event typeFired when
DatatableEntryCreatedBusinessEventA datatable row is added
DatatableEntryUpdatedBusinessEventA datatable row is updated
DatatableEntryDeletedBusinessEventA datatable row is deleted

Documents

Event typeFired when
DocumentCreatedBusinessEventA document is uploaded
DocumentDeletedBusinessEventA document is deleted

In-Process Listeners

For plugin JARs running inside Fineract, the BusinessEventListener<T> interface lets you subscribe to events synchronously within the originating transaction:

java
@Component
public class LoanApprovalListener implements BusinessEventListener<LoanApprovedBusinessEvent> {

    @Override
    public void onBusinessEvent(LoanApprovedBusinessEvent event) {
        LoanAccount loan = event.get();
        // runs inside the same transaction as the approval
    }
}

Internal listeners bypass the m_external_event table and broker entirely. Use them for transactional side effects inside the same JVM. For cross-service integration, use the external event pipeline.

Monitoring

Events are stored in m_external_event in each tenant database before dispatch:

StatusMeaning
TO_BE_SENTWritten, waiting for the batch job
SENTDispatched to the broker

Check the backlog:

sql
-- Summary
SELECT status, COUNT(*) FROM m_external_event GROUP BY status;

-- Inspect oldest pending events
SELECT id, type, category, created_at
FROM m_external_event
WHERE status = 'TO_BE_SENT'
ORDER BY created_at
LIMIT 50;

A Purge External Events job runs periodically to remove SENT rows. The retention window is configurable.

An internal read API also exposes the event log for debugging:

bash
GET /fineract-provider/api/v1/internalevents