Skip to content

Close of Business (COB) - Technical Guide

The Loan COB job is Apache Fineract's nightly batch process. It runs arrears calculations, applies overdue charges, posts accrual journal entries, and advances the business date across every active loan in the portfolio. This guide covers its configuration, worker architecture, and operational management.

See the Close of Business feature page for a business-level explanation of what COB does and why it matters.

How COB Is Triggered

COB is a Spring Batch job named Loan COB. It is scheduled by the batch manager node and executed by one or more batch worker nodes. The manager partitions the loan portfolio into chunks, distributes them to workers via the remote job messaging system, and waits for all partitions to complete before finalising the run and advancing the business date.

On a single-node deployment (the default), the same process acts as both manager and worker. On scaled deployments, the roles are separated.

Environment Variables

Enabling COB

VariableDefaultDescription
FINERACT_JOB_LOAN_COB_ENABLEDtrueEnable the Loan COB job. Set to false only if COB is managed externally via direct API triggers.
FINERACT_MODE_BATCH_MANAGER_ENABLEDtrueThis node schedules and partitions COB.
FINERACT_MODE_BATCH_WORKER_ENABLEDtrueThis node executes COB partitions.

Tuning Chunk and Partition Size

VariableDefaultDescription
LOAN_COB_CHUNK_SIZE10Loans processed per step chunk within a single worker. Increase for faster single-worker throughput; decrease if individual chunks are hitting timeouts.
LOAN_COB_PARTITION_SIZE10Loans per partition distributed across workers. Increase to reduce partition count (and manager overhead) for large portfolios.
LOAN_COB_POLL_INTERVAL1000Milliseconds workers wait between polling for available partitions. Reduce to speed up partition pickup; increase to reduce polling load.
FINERACT_JOB_STUCK_RETRY_THRESHOLD5How many times a stuck or failing partition is retried before being marked failed.

Task Executor Pool

VariableDefaultDescription
FINERACT_DEFAULT_TASK_EXECUTOR_CORE_POOL_SIZE10Core thread pool size for async task execution (including COB partition steps).
FINERACT_DEFAULT_TASK_EXECUTOR_MAX_POOL_SIZE100Maximum thread pool size.

Single-Node vs Multi-Node Architecture

Single Node (Default)

All four modes are enabled on the same instance:

bash
FINERACT_MODE_READ_ENABLED=true
FINERACT_MODE_WRITE_ENABLED=true
FINERACT_MODE_BATCH_MANAGER_ENABLED=true
FINERACT_MODE_BATCH_WORKER_ENABLED=true
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=true

In single-node mode, job messages pass via in-process Spring events - no external broker is needed for COB coordination.

This is sufficient for portfolios up to a few tens of thousands of loans, depending on the COB processing window available.

Multi-Node (Scaled Deployments)

For large portfolios, separate the manager and worker roles across multiple instances. The manager partitions the work; workers process partitions in parallel:

bash
# Manager node
FINERACT_NODE_ID=1
FINERACT_MODE_READ_ENABLED=true
FINERACT_MODE_WRITE_ENABLED=true
FINERACT_MODE_BATCH_MANAGER_ENABLED=true
FINERACT_MODE_BATCH_WORKER_ENABLED=false
FINERACT_LIQUIBASE_ENABLED=true

# Worker node (one or more)
FINERACT_NODE_ID=2
FINERACT_MODE_READ_ENABLED=false
FINERACT_MODE_WRITE_ENABLED=false
FINERACT_MODE_BATCH_MANAGER_ENABLED=false
FINERACT_MODE_BATCH_WORKER_ENABLED=true
FINERACT_LIQUIBASE_ENABLED=false

In multi-node mode, in-process Spring events cannot cross JVM boundaries. An external message broker is required.

WARNING

FINERACT_NODE_ID must be unique across all nodes in a multi-node deployment. Nodes sharing the same ID will cause job coordination failures.

Remote Job Messaging (Multi-Node Transport)

The remote job messaging system carries partition work items from the manager to workers. It is separate from the external business events system - do not use the same Kafka topic or JMS queue for both.

Exactly one transport must be enabled. The three options are:

Spring Events (Single-Node Only)

bash
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=true
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_ENABLED=false
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_ENABLED=false

JMS / ActiveMQ

bash
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=false
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_ENABLED=true
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_BROKER_URL=tcp://activemq:61616
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_QUEUE_NAME=JMS-request-queue
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_BROKER_USERNAME=fineract
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_BROKER_PASSWORD=changeme
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_ENABLED=false

Kafka

bash
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=false
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_ENABLED=false
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_ENABLED=true
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_BOOTSTRAP_SERVERS=kafka:9092
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_TOPIC_NAME=job-topic
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_TOPIC_PARTITIONS=10
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_TOPIC_REPLICAS=3
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_TOPIC_AUTO_CREATE=true
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_CONSUMER_GROUPID=fineract-consumer-group-id

Only one transport at a time

Enabling more than one transport simultaneously causes duplicate job dispatch. Enable exactly one and set the others to false.

Docker Compose Example (Multi-Node with Kafka)

yaml
services:
  kafka:
    image: apache/kafka:latest
    ports:
      - "9092:9092"

  fineract-manager:
    image: apache/fineract:latest
    environment:
      FINERACT_NODE_ID: "1"
      FINERACT_MODE_BATCH_MANAGER_ENABLED: "true"
      FINERACT_MODE_BATCH_WORKER_ENABLED: "false"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED: "false"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_ENABLED: "true"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_BOOTSTRAP_SERVERS: "kafka:9092"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_TOPIC_NAME: "job-topic"
      LOAN_COB_PARTITION_SIZE: "100"

  fineract-worker:
    image: apache/fineract:latest
    environment:
      FINERACT_NODE_ID: "2"
      FINERACT_MODE_BATCH_MANAGER_ENABLED: "false"
      FINERACT_MODE_BATCH_WORKER_ENABLED: "true"
      FINERACT_LIQUIBASE_ENABLED: "false"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED: "false"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_ENABLED: "true"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_BOOTSTRAP_SERVERS: "kafka:9092"
      FINERACT_REMOTE_JOB_MESSAGE_HANDLER_KAFKA_TOPIC_NAME: "job-topic"
      LOAN_COB_CHUNK_SIZE: "50"

Scale the worker service horizontally to reduce the COB processing window. Each additional worker processes partitions in parallel. Ensure each worker has a distinct FINERACT_NODE_ID.

Scheduling COB

COB is scheduled via the Fineract Scheduler API. The schedule is stored per-tenant in the database and persists across restarts. Modify it via the API:

bash
# List all jobs and find the Loan COB job ID
curl https://your-instance.finecko.com/fineract-provider/api/v1/jobs \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)"

# Update the schedule (cron expression)
curl -X PUT \
  https://your-instance.finecko.com/fineract-provider/api/v1/jobs/1 \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "cronExpression": "0 0 1 * * ?",
    "active": true
  }'

The cron expression uses Quartz format: seconds minutes hours day-of-month month day-of-week. 0 0 1 * * ? schedules COB daily at 01:00 server time.

Timezone note: the scheduler uses the timezone configured for the tenant in the fineract_tenants.tenants table. Confirm the tenant timezone is set correctly before scheduling production COB runs. An institution in Africa/Nairobi (UTC+3) running COB at 0 0 1 * * ? will execute at 01:00 East Africa Time, not 01:00 UTC.

Manually Triggering COB

COB can be triggered immediately via the API without waiting for the scheduled run:

bash
curl -X POST \
  https://your-instance.finecko.com/fineract-provider/api/v1/jobs/1/run \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)"

This is useful during initial setup to verify that COB completes correctly, or during recovery after a failed scheduled run.

Monitoring COB Runs

Check the status of recent runs:

bash
curl https://your-instance.finecko.com/fineract-provider/api/v1/jobs/1/history \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)"

Each history entry includes:

FieldDescription
statusSUCCESS or FAILED
jobRunStartTimeWhen the run started
jobRunEndTimeWhen the run completed (empty if still running)
triggerTypeSCHEDULER (scheduled) or APPLICATION (manual)
errorLogStack trace if the run failed

Check the m_loan_cob_error table in the tenant database for details on individual loan processing failures:

sql
SELECT loan_id, error_message, created_date
FROM m_loan_cob_error
ORDER BY created_date DESC
LIMIT 50;

The Business Date API

Read the current business date:

bash
curl https://your-instance.finecko.com/fineract-provider/api/v1/businessdate \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)"

Manually advance the business date (use with care in production - this does not replay any COB steps):

bash
curl -X POST \
  https://your-instance.finecko.com/fineract-provider/api/v1/businessdate \
  -H "Fineract-Platform-TenantId: default" \
  -H "Authorization: Basic $(echo -n 'mifos:password' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "15 March 2024",
    "dateFormat": "dd MMMM yyyy",
    "locale": "en",
    "type": "BUSINESS_DATE"
  }'

WARNING

Manually advancing the business date does not re-run COB steps for the skipped days. It only moves the date forward. Use this only for recovery or testing scenarios, and only after consulting with Finecko support on production instances.

Failure Recovery

If COB fails mid-run:

  1. Check lastRunHistory.errorLog and m_loan_cob_error for the cause
  2. Fix the underlying issue (data integrity error, broker connectivity, resource exhaustion)
  3. Unlock any loans that remain locked: Fineract automatically unlocks stale loan locks on the next COB run startup, but you can confirm via SELECT * FROM m_loan WHERE locked_by_cob_job_id IS NOT NULL
  4. Trigger a manual COB run to process the backlog
  5. Verify the business date advanced correctly after the successful run

If multiple days of COB have been missed (e.g. after an extended outage), COB must be run once per missed day, advancing the business date one day at a time, until it reaches the current date.

COB and the External Events Backlog

COB-triggered events (accruals, overdue charges) are written to m_external_event with status TO_BE_SENT the same as all other business events. If the Send Async Events job has also been stopped during an outage, these events will queue up and dispatch once the job resumes. No events are lost - they drain from the backlog when the broker connection is restored.

Monitor the event backlog alongside COB status:

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

A large TO_BE_SENT count alongside a failed COB run indicates both systems need attention.