Next Decade LMS

Modular monolith vs microservices for a lending core: why Lokta is polylithic

Why a loan platform needs one transaction per disbursement: Lokta's modular monolith, Gradle modules shipped as one Spring Boot binary, versus microservices.

Modular monolith vs microservices for a lending core: why Lokta is polylithic: cover art
Contents 8 sections
Quick answer

For a lending core, a modular monolith beats microservices. A disbursement touches the loan, schedule, charges, ledger and audit trail, and it has to commit or roll back as one transaction. Lokta builds each domain as its own Gradle module with enforced compile boundaries, then ships all of them as one Spring Boot binary. We call that pattern polylithic.

There is a particular meeting that happens in every lending engineering team eventually. The architect stands at a whiteboard. There is a microservice diagram with seventeen boxes. There are arrows. The CTO is asking, politely, why a single loan disbursement now requires a distributed transaction across four services to complete.

We have been in that meeting. We were the architect, more than once.

The honest answer, the one we wish we had given sooner, is that microservices were the wrong unit of decomposition for lending. But the monolith was the wrong unit of deployment. Lokta is built on a third option, and it is what this post is about.

What did the microservice answer get wrong?

Microservices made sense for a specific class of problem: independently scalable workloads with weak transactional coupling. A product catalogue. A search index. A recommendation service. Domains where eventual consistency is fine and the cost of a stale read is, at worst, a slightly worse user experience.

Lending is not that domain. A single user action, such as disbursing a loan, posting a payment or reversing a charge, touches the customer, the loan account, the schedule, the charges, the accounting ledger, the audit trail and the maker-checker queue. In a well-designed monolith, that is one transaction, one rollback boundary, and one unambiguous answer to “did this happen?”

In a microservice topology, that is four to seven network calls and a saga: every step that cannot share a transaction needs a compensation handler to undo it. In lending, where a half-completed disbursement leaves real money on the wrong side of a ledger, we would rather the database roll it back.

The microservice answer was right for its domain and wrong for ours. We watched too many teams learn that the expensive way.

What did the monolith answer get wrong?

The instinct, after that lesson, is to retreat to the monolith. This is also what we did at Fineract, and it is also wrong, but for an entirely different reason.

The monolith’s failure mode is not technical. It is organizational. When every line of code lives in one deployable unit, every team’s pace becomes everyone else’s pace. The accounting team’s release cadence dictates when the loan-product team can ship. A bug in the customer module blocks a deploy of the dashboard. There are no compile-time boundaries between domains, so over time there are no boundaries between domains, so over time the monolith calcifies into a single hairball that nobody can refactor without breaking three teams’ workflows.

We watched this happen at Fineract, and in Fineract derivatives too. The transactions held up. The boundaries between domains did not.

So: microservices give you bad transactional semantics for sound organizational reasons. Monoliths give you sound transactional semantics for bad organizational reasons. Lending engineering teams have swung between these two failure modes for years.

What is the third option?

The pattern that solves this, and we are not the first to recognise it, just the latest to commit to it, is modules at code time, single binary at deploy time.

Concretely: every domain in Lokta is its own Gradle module. It has its own build.gradle, its own Liquibase changelog, its own jOOQ-generated classes against its own schema, its own Spring Boot configuration, its own OpenAPI controllers. A team can work on lokta-loan-product without ever opening a file in lokta-customer. The compile boundary is enforced. If lokta-loan-product wants to reach into lokta-customer’s persistence layer, the build refuses.

But at deploy time, all the modules are component-scanned into a single Spring Boot application. One JVM. One transaction manager. One Postgres connection pool. One audit trail. One binary that your operations team starts, monitors, and rolls back if something goes wrong.

The 17 modules in Lokta when this post was first published in May 2026:

lokta-core              lokta-customer          lokta-party
lokta-tenant            lokta-identity-core     lokta-user-management
lokta-authorization-engine                      lokta-maker-checker
lokta-loan-product      lokta-loan-account      lokta-loan-participant
lokta-charge            lokta-accounting        lokta-numbering
lokta-code-values       lokta-opr               lokta-dashboard

Each one owns its domain. None of them ships separately. All of them deploy together.

The disbursement flow that used to require a saga is now a single Spring @Transactional method that calls a few services across module boundaries. If anything fails, a charge calculation error, a schedule miscalculation, an audit-trail write that throws, the transaction rolls back. There is no compensation handler to maintain, because there is nothing to compensate.

Polylithic is our word for a modular monolith

The industry term for this pattern is a modular monolith. It means one deployable application, split inside into modules with enforced boundaries. The Spring ecosystem has its own tooling for it, Spring Modulith, which verifies module boundaries in a Spring Boot application. Lokta’s boundaries are enforced at the Gradle build: each domain is its own module with its own dependencies, so a forbidden reach across modules fails the build. Apache Fineract has since moved the same way: its current codebase splits loans, charges and accounting into separate Gradle modules.

We use “polylithic” for two reasons. It puts the modules in the name, where “monolith” hides them. And each module carries its own Liquibase changelog and jOOQ-generated classes, where Spring Modulith treats packages as modules and checks their boundaries in tests. The deployable is still one binary, and that is the property lending needs.

For a lender, the same modularity shows up as how the platform is adopted. The platform ontology describes modules a lender can take up one at a time, and the fintech LMS page covers how an engineering team extends it in-tree.

Microservices, modular monolith and monolith compared

The three patterns differ mainly in where they put the transaction boundary and where they put the team boundary.

DimensionMicroservicesLokta’s modular monolithMonolith
What shipsOne deployable per service, each on its own schedule.One Spring Boot binary built from separate Gradle modules.One binary with no internal module lines.
Domain boundariesHeld by the network between services.Held by the build: a reach into another module’s persistence layer fails to compile.Held by convention only, so they wear away as the codebase grows.
One loan disbursementFour to seven network calls, plus a saga with a compensation handler per step.One transactional method; any failure rolls the whole disbursement back.One transaction, the same as the modular monolith.
Unit of scaleThe individual service.The tenant, isolated by schema-per-tenant Postgres.The whole application.
Release cadence and version skewEach service ships alone, so versions between services can drift.All modules ship together; every module runs against the code it was compiled with.All code ships together, the same as the modular monolith.
Where it breaksMoney that must move exactly once, where a half-finished step leaves the ledger wrong.A single module that needs to scale or ship on its own timetable.A growing engineering team, as one codebase turns into a hairball nobody can safely refactor.

The trade-offs we accept

This is not a free architecture. It costs us three things.

You can’t independently scale a single module. If lokta-dashboard is the slowest part of the system, you can’t put more replicas behind it without also replicating the rest. We are fine with this trade. For lending, the right scaling unit is the tenant, not the module, and tenant isolation is what schema-per-tenant Postgres gives us. If a particular tenant outgrows a deployment, we deploy them separately. The unit of horizontal scale is the customer, not the subsystem. That maps to how lenders actually grow.

You can’t deploy modules on different cadences. A change to lokta-charge ships at the same time as everything else. The flip side is that you can never have version drift between modules: lokta-loan-account always sees the same lokta-customer it was compiled against. The version-skew problems that plague microservice estates simply do not exist for us. We trade per-module deploy autonomy for cross-module integrity, and for a lending platform that is the right trade.

There is a third trade-off that is sometimes raised: you can’t write modules in different languages. Correct. We don’t want to. Polyglot estates are an organizational tax we have paid before and would not pay again.

Why this matters for an engineering reviewer

If you are evaluating Lokta for an RFP and your team has been through the microservice cycle, here is what we want you to take away.

You can read every line of Lokta in a single IDE window. Your engineers can debug a disbursement end-to-end without attaching to four containers. Your operations team operates one binary. Your DBA looks at one Postgres instance with one schema-per-tenant pattern. Your security team audits one network surface.

What you give up (independent module scaling, polyglot teams, per-module deploy cadence) is the part of the microservice promise that turned out to be expensive. What you keep (clean compile-time boundaries, per-module ownership, schema isolation, version safety) is the part that was always genuinely valuable.

This is what we would have built at Fineract if Spring Boot’s component scanning, Gradle’s incremental builds, and jOOQ’s compile-time SQL safety had been what they are now. They are now. So we built it. What we learned building Fineract tells the longer story, and the Fineract alternative comparison sets the two side by side.

If your engineering team is maintaining a saga library, or your CTO is choosing between “rewrite as microservices” and “live with the monolith forever”, there is a third option. We are happy to walk your engineering reviewers through it.

Frequently asked questions

What is a modular monolith?

A modular monolith is one deployable application split inside into modules with enforced boundaries. Each module owns its domain and its data, and one module cannot reach into another's internals. All of them ship and run as a single process, so a change that spans modules still commits or rolls back as one database transaction. Lokta calls its version of the pattern polylithic.

Why is Lokta polylithic instead of microservices or a monolith?

Microservices suit independently scalable workloads with weak transactional coupling, such as catalogues, search and recommendations. Lending is not that domain. A single action (disburse, post a payment, reverse a charge) touches the customer, loan, schedule, charges, accounting ledger, audit trail and maker-checker queue. In microservices that becomes a saga. A monolith solves the transaction but lets domain boundaries erode, so every team moves at everyone else's pace. Polylithic is the third option: modules at code time, one binary at deploy time.

What is a 'polylithic' architecture in software terms?

Modules with hard compile-time boundaries combined into a single deployable unit. At Lokta, every domain is its own Gradle module with its own build.gradle, Liquibase changelog, jOOQ-generated classes, Spring Boot configuration and OpenAPI controllers. The compile boundary is enforced: if loan-product wants to reach into customer's persistence layer, the build refuses. At deploy time, the modules are component-scanned into a single Spring Boot application: one JVM, one transaction manager, one Postgres connection pool, one audit trail.

Why are microservices a poor fit for lending workloads?

Lending is transactionally tight. A single disbursement touches seven or eight domains and needs one rollback boundary. In microservices, that becomes four to seven network calls plus a saga, which means writing a compensation handler for every step that cannot be made transactional. Where a half-completed disbursement leaves real money on the wrong side of a ledger, compensation handlers do not meet the bar.

What trade-offs does Lokta accept with the polylithic approach?

Three. A single module cannot scale on its own, because the right scaling unit for lending is the tenant, not the module, and tenant isolation comes from schema-per-tenant Postgres. Modules cannot deploy on different cadences, though the flip side is no version drift between them. And modules cannot be written in different languages, a polyglot tax the team has paid before and chose not to pay again.

How many modules does Lokta ship as?

When this post was first published in May 2026, Lokta's core was seventeen Gradle modules in one Spring Boot binary: lokta-core, lokta-customer, lokta-party, lokta-tenant, lokta-identity-core, lokta-user-management, lokta-authorization-engine, lokta-maker-checker, lokta-loan-product, lokta-loan-account, lokta-loan-participant, lokta-charge, lokta-accounting, lokta-numbering, lokta-code-values, lokta-opr and lokta-dashboard. Each owns its domain. None ships separately. All deploy together.


Ashok Auty is the co-founder and Chief Customer Officer of Lokta and co-creator of Apache Fineract. He has spent two decades building lending infrastructure for emerging markets and has watched the microservice / monolith pendulum swing twice.


Read next:

Ashok Auty

Co-founder and Chief Customer Officer of Lokta. Co-creator of Apache Fineract. 15+ years building lending infrastructure.

More about the team →
Founder-led adoption

Adopt the agentic loan servicing platform.

Lokta is built for enterprise deployment, VPC or single-tenant cloud, with an audit trail in every state change. We work with a select group of institutions through a founder-led model: deep adoption, deliberate scope, a delivery window the team commits to in writing.