Why Lokta Is Polylithic: One Binary, Many Modules
Microservices were wrong for lending; monoliths can't scale teams. Polylithic Gradle modules in one Spring Boot binary let 50 engineers do what 500 used to — without the saga tax.
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 Core is built on a third option, and it is what this post is about.
What the microservice answer got 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 — disburse a loan, post a payment, restructure an account — 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’s one transaction, one rollback boundary, and one unambiguous answer to “did this happen?”
In a microservice topology, that’s four-to-seven network calls and a saga. Sagas are not an architectural style. Sagas are a confession. Every saga is “we couldn’t make this transactional, so we wrote a compensation handler and hoped.” In lending — where the cost of a half-completed disbursement is real money on the wrong side of a ledger — that confession is not acceptable.
The microservice answer was right for its domain and wrong for ours. We watched too many teams learn that the expensive way.
What the monolith answer got 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. We watched it happen to several Fineract derivatives. The architecture is sound; the human dynamics around it are not.
So: microservices give you bad transactional semantics for sound organizational reasons. Monoliths give you sound transactional semantics for bad organizational reasons. The whole industry has been lurching between these two failure modes for fifteen years.
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 17 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 today:
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.
The trade-offs we accept
This is not a free architecture. It costs us two things, and we want to be honest about both.
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.
If you are a lender whose engineering team is currently maintaining a saga library, or whose CTO is trying to decide between “rewrite as microservices” and “live with the monolith forever” — there is a third option. We are happy to walk through it.
Ashok Auty is the co-founder 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.