Appearance
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
| Domain | Schema path in repo | Key schemas |
|---|---|---|
| Loan | loan/v1/ | LoanAccountDataV1, LoanTransactionDataV1, LoanChargeDataV1, LoanProductDataV1 |
| Client | client/v1/ | ClientDataV1 |
| Savings | savings/v1/ | SavingsAccountDataV1, SavingsAccountTransactionDataV1 |
| Fixed deposit | fixeddeposit/v1/ | FixedDepositAccountDataV1 |
| Recurring deposit | recurringdeposit/v1/ | RecurringDepositAccountDataV1 |
| Group | group/v1/ | GroupGeneralDataV1 |
| Share | share/v1/ | ShareAccountDataV1 |
| Document | document/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/configurationResponse:
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 type | Fired when |
|---|---|
ClientCreateBusinessEvent | A client record is created |
ClientActivateBusinessEvent | A client is activated |
ClientRejectBusinessEvent | A client application is rejected |
Group and Centre
| Event type | Fired when |
|---|---|
GroupsCreateBusinessEvent | A group is created |
CentersCreateBusinessEvent | A centre is created |
Loan lifecycle
| Event type | Fired when |
|---|---|
LoanCreatedBusinessEvent | A loan application is submitted |
LoanApprovedBusinessEvent | A loan is approved |
LoanApprovedAmountChangedBusinessEvent | The approved amount is modified |
LoanUndoApprovalBusinessEvent | Loan approval is reversed |
LoanRejectedBusinessEvent | A loan application is rejected |
LoanDisbursalBusinessEvent | A loan is disbursed |
LoanDisbursalTransactionBusinessEvent | A disbursement transaction is recorded |
LoanUndoDisbursalBusinessEvent | A disbursement is reversed |
LoanUndoLastDisbursalBusinessEvent | The most recent disbursement is reversed |
LoanUpdateDisbursementDataBusinessEvent | Disbursement data is modified |
LoanStatusChangedBusinessEvent | The loan status changes |
LoanBalanceChangedBusinessEvent | The outstanding balance changes |
LoanCloseBusinessEvent | A loan is closed |
LoanCloseAsRescheduleBusinessEvent | A loan is closed via rescheduling |
Loan transactions
| Event type | Fired when |
|---|---|
LoanTransactionMakeRepaymentPreBusinessEvent / ...Post | Repayment applied |
LoanRefundPreBusinessEvent / ...Post | Loan refund |
LoanTransactionMerchantIssuedRefundPreBusinessEvent / ...Post | Merchant-issued refund |
LoanTransactionPayoutRefundPreBusinessEvent / ...Post | Payout refund |
LoanTransactionGoodwillCreditPreBusinessEvent / ...Post | Goodwill credit |
LoanTransactionRecoveryPaymentPreBusinessEvent / ...Post | Recovery payment |
LoanTransactionInterestRefundPreBusinessEvent / ...Post | Interest refund |
LoanForeClosurePreBusinessEvent / ...Post | Foreclosure |
LoanCreditBalanceRefundPreBusinessEvent / ...Post | Credit balance refund |
LoanChargebackTransactionBusinessEvent | Chargeback applied |
LoanAdjustTransactionBusinessEvent | Transaction adjusted |
LoanAccrualTransactionCreatedBusinessEvent | Accrual transaction created (COB) |
LoanCapitalizedIncomeTransactionCreatedBusinessEvent | Capitalized income transaction created |
Loan charges
| Event type | Fired when |
|---|---|
LoanAddChargeBusinessEvent | Charge added to a loan |
LoanDeleteChargeBusinessEvent | Charge removed |
LoanUpdateChargeBusinessEvent | Charge modified |
LoanWaiveChargeBusinessEvent | Charge waived |
LoanWaiveChargeUndoBusinessEvent | Charge waiver reversed |
LoanChargePaymentPreBusinessEvent / ...Post | Charge payment processed |
LoanChargeRefundBusinessEvent | Charge refund issued |
LoanApplyOverdueChargeBusinessEvent | Overdue charge applied (COB) |
LoanWaiveInterestBusinessEvent | Interest waived |
Loan write-off and charge-off
| Event type | Fired when |
|---|---|
LoanWrittenOffPreBusinessEvent / ...Post | Loan written off |
LoanUndoWrittenOffBusinessEvent | Write-off reversed |
LoanChargeOffPreBusinessEvent / ...Post | Loan charged off |
LoanUndoChargeOffBusinessEvent | Charge-off reversed |
Loan rescheduling and restructuring
| Event type | Fired when |
|---|---|
LoanRescheduledDueCalendarChangeBusinessEvent | Rescheduled due to calendar change |
LoanRescheduledDueHolidayBusinessEvent | Rescheduled due to holiday |
LoanRescheduledDueAdjustScheduleBusinessEvent | Schedule adjustment applied |
LoanScheduleVariationsAddedBusinessEvent | Schedule variation added |
LoanScheduleVariationsDeletedBusinessEvent | Schedule variation removed |
LoanInterestRecalculationBusinessEvent | Interest recalculated |
LoanReAgeBusinessEvent / LoanUndoReAgeBusinessEvent | Re-ageing applied or reversed |
LoanReAgeTransactionBusinessEvent / LoanUndoReAgeTransactionBusinessEvent | Re-ageing transaction |
LoanReAmortizeBusinessEvent / LoanUndoReAmortizeBusinessEvent | Re-amortization applied or reversed |
LoanReAmortizeTransactionBusinessEvent / LoanUndoReAmortizeTransactionBusinessEvent | Re-amortization transaction |
Loan officer and transfer
| Event type | Fired when |
|---|---|
LoanReassignOfficerBusinessEvent | Loan reassigned to new officer |
LoanRemoveOfficerBusinessEvent | Officer removed from loan |
LoanInitiateTransferBusinessEvent | Loan transfer initiated |
LoanAcceptTransferBusinessEvent | Transfer accepted |
LoanRejectTransferBusinessEvent | Transfer rejected |
LoanWithdrawTransferBusinessEvent | Transfer withdrawn |
Loan product and COB
| Event type | Fired when |
|---|---|
LoanProductCreateBusinessEvent | A loan product is created |
LoanAccountsStayedLockedBusinessEvent | Loans remain locked after COB run |
Savings
| Event type | Fired when |
|---|---|
SavingsCreateBusinessEvent | Savings application created |
SavingsApproveBusinessEvent | Savings account approved |
SavingsActivateBusinessEvent | Savings account activated |
SavingsRejectBusinessEvent | Savings application rejected |
SavingsCloseBusinessEvent | Savings account closed |
SavingsPostInterestBusinessEvent | Interest posted |
SavingsDepositBusinessEvent | Deposit made |
SavingsWithdrawalBusinessEvent | Withdrawal made |
Fixed and recurring deposits
| Event type | Fired when |
|---|---|
FixedDepositAccountCreateBusinessEvent | Fixed deposit account created |
RecurringDepositAccountCreateBusinessEvent | Recurring deposit account created |
Shares
| Event type | Fired when |
|---|---|
ShareAccountCreateBusinessEvent | Share account created |
ShareAccountApproveBusinessEvent | Share account approved |
ShareProductDividentsCreateBusinessEvent | Share dividend created |
Datatables
| Event type | Fired when |
|---|---|
DatatableEntryCreatedBusinessEvent | A datatable row is added |
DatatableEntryUpdatedBusinessEvent | A datatable row is updated |
DatatableEntryDeletedBusinessEvent | A datatable row is deleted |
Documents
| Event type | Fired when |
|---|---|
DocumentCreatedBusinessEvent | A document is uploaded |
DocumentDeletedBusinessEvent | A 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:
| Status | Meaning |
|---|---|
TO_BE_SENT | Written, waiting for the batch job |
SENT | Dispatched 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