Designing a Term Loan Engine That Product Can Reconfigure
Every new lending variant shouldn't be an engineering project. Notes on modelling interest, margin and moratorium as configuration rather than code paths.
- Lending
- Domain Modelling
- Python
- Django
The first term loan product you build is easy. The second one teaches you what you got wrong.
When a lending business is young, every product variation feels like a one-off: this segment needs a moratorium, that one needs a flat rate instead of reducing balance, a third needs margin funding on top. It is tempting to encode each of those as a branch in the schedule generator. That works — right up until you have nine branches, and nobody can say with confidence what the ninth one does to rounding on the final instalment.
The shape of the problem
A term loan schedule is a pure function. Given a principal, a rate model, a tenure, a disbursal date and a set of modifiers, there is exactly one correct amortisation schedule. The difficulty is never the arithmetic — it is that the modifiers interact.
A moratorium doesn't just skip instalments; it changes what interest accrues during the skipped period, and whether that interest is capitalised, collected up front, or spread across the remaining term. Each of those three answers is a legitimate product decision, and each produces a different schedule from identical inputs.
Configuration over branching
The move that made the difference was treating the product definition as data. Interest model, ROI tiers, margin treatment, moratorium behaviour and repayment cadence became fields on a configuration object rather than conditionals inside the generator.
The generator's job shrank to something you can reason about: read a configuration, produce a schedule. Adding a variant stopped being a code change and became a row.
- Interest model and ROI tiers as declared parameters, not branches
- Moratorium behaviour named explicitly — capitalised, collected, or spread
- Margin treatment applied per configuration rather than per caller
- One generator, exercised by many configurations
Correctness checks earn their keep
Configuration-driven systems fail differently from branchy ones. Instead of a wrong branch, you get a valid-looking configuration that produces a subtly wrong schedule — the totals drift by a rupee, or the last instalment absorbs a rounding error nobody budgeted for.
The safeguard is a set of invariants checked on every generated schedule: the sum of principal components equals the disbursed amount, the sum of instalments equals principal plus total interest, and no instalment falls outside its configured bounds. These run on generation, not in a nightly report, because a schedule attached to a live loan is very expensive to correct later.
If you take one thing: money systems should verify their own output at the moment they produce it. Reconciling after the fact is strictly more expensive.
What I would do differently
I would version the configurations from day one. Loans live for years, and a product definition that changes in month eight must not retroactively alter a schedule generated in month three. Attaching the resolved configuration to the loan — not a reference to a mutable product row — avoids an entire category of incident.
Thoughts on this?
Always happy to talk through the engineering trade-offs.