Writing custom batch jobs for Apache Fineract

Custom Fineract batch jobs run scheduled work beside the core end-of-day process. This page explains how to define, register, configure and deploy them in a custom module, and what to confirm before production.

On this page

When a custom batch job for Apache Fineract is the right tool

A custom batch job fits when your institution needs scheduled work that Fineract does not already perform, such as a nightly extract of loan balances for a data warehouse or a periodic reconciliation between Fineract accounts and an external ledger. In Fineract, a batch job is a Spring Batch job that runs on the batch instance type, which the platform documentation describes as separate from the write and read instances. Before you write code, confirm which instance will host the job, because a job that writes data competes with the write workload and needs the same care as any other write path.

The Close of Business Day (COB) job is a different thing. The README describes it as a Spring Batch job that processes accounts at end of day and can run for several hours on large deployments, and it can be extended with business steps that run inside that job. A custom batch job is a separate scheduled unit with its own name and configuration. Choose a business step when the logic must run as part of end-of-day processing and respect the business date. Choose a custom job when the work is independent of COB, such as an export or a reconciliation that can run on its own schedule. Confirm with the platform documentation how each job name is configured and how the scheduler lists and triggers it in your release, since this is the detail most likely to differ between versions.

An external scheduler, such as cron, Kubernetes CronJobs or an orchestration tool already in your estate, is often the simpler choice when the work only reads data through the REST API or the database. It keeps Fineract's codebase untouched and its failure handling visible to your operations team. A custom job earns its place when the work needs transactional access to Fineract's internal services, must run in the same deployment lifecycle as the core, or must be registered so Fineract's own scheduler and job history record it. Ask your team which of these needs applies, and test the chosen option against a non-production tenant with realistic volumes before committing to it.

Options for scheduling and structuring a custom job

This table compares the main choices for a custom Fineract batch job: job structure, step type, where it runs and whether it is partitioned. Read each row against your batch volume, your restart expectations and how much load your database can absorb during the run.

DecisionOptionTrade-offChoose it when
Job structureStandalone Spring Batch job in a custom moduleRegistered by a database migration and runs on the batch instance, so it shares lifecycle and deployment with the core job schedulerThe logic is independent of Close of Business and needs its own schedule and restart state
Step typeTaskletSimple single action per step with no read, process and write cycle; restart resumes at step level, so a failed action may need to be idempotentThe work is one set-based SQL operation or one call to a service method
Step typeChunk-oriented stepReads, processes and writes in fixed-size chunks with commit per chunk; restart resumes from the last committed chunk, at the cost of more configurationThe job iterates over many accounts or rows and partial progress must survive a failure
Execution locationIn-process on the batch instanceUses the Fineract batch instance and its database connections directly, so load and queueing must be planned against the scheduled core jobsThe job needs internal services or transactions and runs in a window the team controls
Execution locationExternal scheduler calling a Fineract APIScheduler owns timing and retries, but each call passes through authentication, tenant headers and API transactions, and state lives outside FineractThe job is a sequence of standard API operations and the scheduler already governs other jobs
ParallelismSingle-node executionSimplest to operate and reason about; run time grows with data volume, and a long run may overlap other batch workData volume is modest or the run window is generous
ParallelismRemote partitioningSpreads work across worker nodes coordinated through a message broker, adding broker, worker and monitoring components to operateVolume is large enough that a single node cannot finish in the window, and the team can operate ActiveMQ or Kafka as the coordination layer
Database loadHeavy writes during the runLarge updates can contend with online transactions and reporting, so chunk size, commit frequency and read replica use need testingConfirm load behavior in a test environment with production-like data volume before scheduling

How to build and register a custom batch job step by step

This sequence takes a custom Spring Batch job from a blank module to a scheduled run on a batch instance, with a check at each stage so that a failure is caught where it starts. The result is a job that is visible to operators, tested against non-production data, and recorded in the execution history before it touches production accounts.

  1. Create the job and tasklet definitions

    Your engineers write the Spring Batch job configuration and the tasklet classes inside a custom module, following the module layout that Fineract expects for extensions. Keep the tasklet focused on one unit of work, make it idempotent so a rerun does not double-post, and avoid editing core Fineract sources. For packaging conventions, use the custom module topic rather than repeating them here. Done means the module compiles and the job bean loads in a local Fineract build with no startup errors.

  2. Write the migration that registers the job

    Add a Liquibase changeset to the module that inserts the job's entry into the scheduler job table, so the job appears alongside the built-in jobs and can be scheduled from the administration API. Give the job a name that matches the Spring Batch job bean exactly, and review the changeset as you would any schema change because it runs on write instances. Check it by running the migration against a copy of a non-production database and confirming the new row exists with the expected name and status.

  3. Configure the job name in environment settings

    Set the environment variable that tells the batch instance which job names it should load and run, following the Fineract deployment documentation for your release. Use the same job name you registered in the migration, and keep the setting in your secrets or configuration store rather than hard-coding it in the image. Verify the setting by starting a batch instance and checking its logs for the job name being picked up without warnings about unknown jobs.

  4. Package the module into the image

    Build the module artifact and include it in the Fineract image your organization builds and scans, so that every write and batch instance runs identical code. Record the image tag, module version and commit in your release record. Confirm the package by listing the jar contents in the image and checking that the job class and migration resources are present.

  5. Deploy to a batch instance and confirm the job is listed

    Roll the new image out to a batch instance first, and where the deployment supports it, run the Liquibase migration through the write path or the liquibase-only profile before the batch instance starts. Open the job list in the administration API or user interface and confirm the job is present, enabled and shows the expected name. If it is missing, check the migration ran on the write instance and the environment setting matches the job name exactly.

  6. Run against a non-production tenant

    Trigger the job manually against a non-production tenant that holds representative loan and savings data, with a business date you control. Confirm the tasklet does what its specification says, such as the intended accounts being updated and no others, and that a second run does not repeat side effects. Check the output records and reconcile counts against the expected figures before moving on.

  7. Confirm the execution is recorded

    Review the batch execution history for the test run and confirm it shows the job name, start and end times, status and step-level results, including any failures that were logged. Test a deliberate failure path as well, so you know how a failed run appears to operators and how it is restarted. Only after the recorded history matches what you observed should the job be scheduled on production tenants, under your change control process.

Why the registration migration must be ordered and tested

The Liquibase migration that registers your job is the step most likely to be deployed out of order, so treat it as a controlled release step rather than a file that rides along with the code. According to the Fineract documentation, Liquibase database migrations run only on write instances. That means the row or configuration that makes your job known to the system is created by the write tier, while the batch instance is the one that actually executes the job. If your batch instance starts with new code but the migration has not yet run on the write tier, the job class may exist without a matching registration, or the registration may point to a state the database does not yet have. Confirm for your Fineract version exactly which tables the migration touches and how the job name is matched at startup, using the platform documentation as the starting point.

On Kubernetes, the documentation describes running migrations separately with the liquibase-only Spring profile. Decide early whether a dedicated migration job runs before any write or batch pod is rolled out, and make that ordering a gate in your deployment pipeline. Verify registration in a staging environment that mirrors production: confirm the migration completed on the write database, confirm the job name in configuration matches the registered name exactly, and confirm a batch instance logs the job as available before you enable a schedule. Rolling back is the harder half. Liquibase changesets are not automatically reversible in the way application code is, so decide before release whether the registration change has a tested rollback changeset, or whether your fallback is to disable the schedule and leave the registration in place. Confirm which option your version supports.

Security and operational controls for custom batch jobs

A custom batch job runs with the same database reach as the platform it extends, and it often runs unattended at night. The six controls below cover what the job can touch, how it gets credentials, how it moves between tenants, how the platform's default logins are handled, how its endpoints are reached, and how failures are noticed.

Least-privilege database access

A job that runs inside Fineract inherits the database access of the instance it runs on, so a bug in a tasklet can read or change any tenant's data. Ask the implementation team which database role the batch instance uses, whether that role can write to tables the job never touches, and whether schema changes are limited to the Liquibase migration that registers the job. Review the grants before the first production run, and confirm they match the batch instance's role in the documented instance model.

Secrets kept out of job configuration

Job names, schedules and parameters may live in configuration, but database passwords, API keys for downstream systems and signing keys should not. Store them in your secrets manager and inject them as environment variables at startup, so they never appear in the Liquibase changelog, in version control or in the job's execution metadata. Confirm how the job logs its parameters, since a careless log line can expose a credential.

Tenant isolation across iterations

Fineract is multi-tenant, and tenant connections come from the tenants database. A job that loops over tenants must set the tenant context for each iteration and must not carry state, cached records or open transactions from one tenant into the next. Test with at least two tenants that hold different data, and confirm that a failure in one tenant does not stop or corrupt the others unless that is the intended behavior.

Default development credentials replaced

The README documents a default development login of mifos with a password that must be changed for any real deployment. Batch jobs often use API or service credentials that are created quickly during development and then forgotten. Before go-live, confirm that no default login exists in any tenant, that service accounts for jobs have only the permissions they need, and that credentials are rotated under a documented procedure.

HTTPS-only access to job endpoints

If a job exposes an endpoint for manual triggering, status or restart, that endpoint must be reached only over HTTPS. The Fineract documentation strongly encourages HTTPS only and documents SSL settings through environment variables such as FINERACT_SERVER_SSL_ENABLED. Confirm that the setting is enforced on every instance type, that plain HTTP ports are closed at the gateway or load balancer, and that manual triggers require the same authentication as other administrative calls.

Audit trail and alerts for failed runs

A job that fails at 2 a.m. and is not noticed can leave a book of accounts unprocessed until a customer or auditor finds it. Record who triggered each manual run, the parameters used, the start and end times and the outcome. Alert on failed executions and on executions that stay in a running state longer than the expected window, and agree on the owner who responds. Ask the implementation team to show these alerts firing in a test environment before production.

Questions teams ask before writing a custom batch job

How much effort does a custom batch job for Fineract usually need?

Effort depends mostly on the job logic, not on the scheduling mechanics. A job that reads a few tables, applies one rule and writes results is a small change. A job that touches loan balances, accounting entries or client status needs careful reading of the existing Fineract flows, transaction boundaries and failure handling. Four drivers matter most: the business rule itself, the data volume the job will process, the depth of testing against realistic loan and savings states, and how the job behaves across tenants. Ask any partner to split its estimate along those four lines so you can see where the time goes.

Can our in-house team write a custom batch job without a partner?

Yes, if the team has Java and Spring Batch experience and can read the Fineract source that the job touches. The harder part is usually domain knowledge: knowing when a loan or savings balance is final, which business date applies, and what the close of business day already does. Teams without that knowledge often write a job that works on test data and then misbehaves at month end. An in-house team can build the job, but it should plan a review of the financial logic and a test pass against production-like data before the job runs on live accounts.

What does the work need from our side before it starts?

The build depends on inputs that only your organization can provide. Plan to supply:

  • A non-production environment running the same Fineract version and database platform as production
  • Test data that covers the loan and savings states the job will meet, including closed, written-off and overdue accounts
  • A written business date plan that says how the job interacts with the logical business date and the close of business day
  • Named business owners who can sign off the rule the job applies
  • Access to the tenants where the job will run, with the tenant list and any tenant-specific differences
How long does a custom batch job take to deliver?

Duration follows the same drivers as effort: the complexity of the rule, the number of tenants and the amount of test data needed to exercise edge cases. Testing and business sign-off usually take longer than the code. Schedule time for a run against a copy of production-scale data, because a job that completes quickly on a small dataset can run for much longer on a large one. Ask for a timeline that separates design, build, test and rollout, and check that each stage has a defined exit condition.

Is an external scheduler a simpler option than a Fineract batch job?

Sometimes. An external scheduler can call Fineract's REST API on a timetable, which keeps custom logic outside the Fineract codebase and avoids a custom module. The trade-off is that the scheduler runs as a client, so each change goes through API calls with their own authentication, error handling and transaction scope. A Spring Batch job inside a custom module runs in the same application context, uses the same transactions and tenant handling as core jobs, and is easier to keep consistent with close of business day processing. Choose the external route for reporting or notifications that only read data. Choose an in-application job when the work changes ledger or loan state.

Which parts of a custom batch job cause the most production problems?

Most incidents come from three areas. First, jobs that run while close of business day is active and compete for the same loan rows. Second, jobs that assume a single tenant and fail or write to the wrong database when several tenants are configured. Third, jobs without restart behavior, so a failure midway leaves partial updates that nobody can trace. Check how the job is registered, how it is named in configuration, how it reruns after failure, and how each run is logged. Confirm these points against the Fineract version you run, using its documentation and source, before you approve the design.

What should we ask for in a quote for a custom batch job?

Ask for a written scope that names the job's business rule, the tables and Fineract services it touches, the tenants it covers and the test cases it must pass. Ask for effort split into design, build, testing, deployment and handover, with the assumptions behind each part. Ask who performs the business sign-off and how many review cycles are included. Also ask what recurs after go-live, such as maintenance across Fineract upgrades and support for the job's runs, and what is one-time. Quotes that leave these items out are hard to compare.

How deployment differs across container and Kubernetes setups

On a single host, the containers you start decide when migrations run and which process executes scheduled work, so each container's role must be set explicitly. The Fineract documentation describes read, write and batch instance types selected through environment variables, and states that Liquibase migrations run only on write instances. A common layout for illustrative purposes uses one write container that migrates and serves requests, and one batch container that runs scheduled jobs, which makes startup order important.

  • Startup order: Start the write container first and confirm migrations have finished before the batch container starts, because the batch instance depends on the schema the write instance creates.
  • Instance type per container: Set the instance type variable on each container and check the effective value at runtime, not only the image defaults.
  • Single write instance: Keep one write instance against the database, consistent with the documentation's recommended high-load layout.
  • Shared database configuration: Confirm both containers point to the same PostgreSQL database and the same tenants database, or the batch job will not see the data your tenants use.
  • Restart behavior: Check in the Fineract documentation what a restart does to migrations and batch state before you automate restarts.

Before relying on the first scheduled run, confirm the batch container logs show the custom job registered and the write container logs show migrations complete.

How DeployOpen would scope and test a custom Fineract job

A fixed-scope engagement for a custom batch job starts with a written definition of what the job is for, before any code is written. The statement of purpose names the business outcome, such as which accounts the job touches, which balances or statuses it reads and writes, and what must be true when it finishes. Alongside that, the scope records the expected data volumes, the account population at peak, the required schedule and its position relative to the Close of Business Day job, and the failure behavior the institution accepts. Volumes and schedule are the inputs that most often change the design, because a tasklet that is harmless over a few thousand accounts can become the longest step in a nightly run on a large book. Ask for these figures from your own environment and from your operations team, and record them in the scope document with a named owner for each.

Acceptance tests are agreed in the same document, phrased as observable results: expected postings, expected status changes, counts of records processed and skipped, and what a rerun after a forced failure must produce. Testing then runs against a non-production tenant, selected with the Fineract-Platform-TenantId header, that holds data shaped like production but no live customer records. Business dates are controlled on that tenant so the job can be run for a chosen date, repeated, and checked against the expected results without waiting for the real calendar. The test plan should cover a normal run, a run with an empty population, a run interrupted partway through, and a run repeated for the same date. Confirm with your platform team which environment settings and database access the test tenant needs, and who may change its business date.

How DeployOpen delivers and supports custom Fineract jobs

This sequence takes a scheduled job from a written requirement to a tested, documented module running in your own environment, with optional ongoing support after handover.

  1. Define the job requirement and schedule

    DeployOpen works with your operations and product owners to state what the job must do, which tenants and records it touches, how often it runs, and where it must sit relative to the Close of Business Day job. The output is a short requirements note that names the inputs, outputs, failure expectations and the business dates the job depends on. Engineers confirm the requirement against the Fineract version you run before any code is written, so the scope reflects your actual platform.

  2. Agree a fixed-scope written proposal

    Based on the requirements note, DeployOpen prepares a written scope listing the job and tasklet deliverables, the custom module, the database migration that registers the job, test coverage, deployment steps and handover documents. Timelines and fees follow from that scope, and both parties sign before work starts. Your team should ask the quote to separate build effort from any test environment setup, data preparation and retainer terms, so each cost driver is visible.

  3. Build the job in a custom module

    Engineers implement the Spring Batch job and tasklet classes in a custom module that follows Fineract's custom folder convention, leaving core sources untouched. They add the Liquibase migration that registers the job name, and wire configuration keys so operators can enable or disable the job per environment. The step closes when the module compiles against the target Fineract release and the job appears in the scheduler configuration on a non-production instance.

  4. Test restarts, failures and tenant behavior

    DeployOpen tests the job against a running backend with representative test data, covering normal runs, reruns after a failure, restarts mid-chunk, empty input and multi-tenant execution. Where the job depends on business dates, testers control the date and confirm results on each run. The output is a test report listing scenarios, results and any defects fixed, which your team can keep as a regression baseline for future upgrades.

  5. Deploy the module to your infrastructure

    Engineers package the module into the image your team builds, then deploy it to the environments you control, whether cloud accounts, data centers or air-gapped networks. Database migrations run on the write instance, consistent with how Fineract handles Liquibase, and the job is enabled only on the batch instance you designate. The step is complete when the job runs once in production-like conditions and your operators confirm its logs, status and alerts are visible.

  6. Hand over runbooks and documentation

    DeployOpen delivers documentation that covers the job purpose, configuration keys, schedule, expected duration drivers, restart procedure, rollback steps and known limits. Operators receive a walkthrough so they can run the job, interpret its status and escalate issues without the build team. Handover is complete when your operations lead accepts the runbook and the source repository, tests and deployment scripts are in your control.

  7. Support upgrades under an optional retainer

    If you choose a retainer, DeployOpen reviews the custom module when you move to a new Fineract release, reruns the regression suite, fixes compatibility issues and documents changes. Retainer terms, response expectations and included hours are set in writing and are separate from the fixed-scope build. Your team should confirm which upgrade work, environments and reporting the retainer covers before signing.

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.