A
AgentKick
Back to blog
Regulated fintech·

A nine-state settlement workflow and the type mismatch the compiler missed

Money workflows accrete states. A settlement that starts as "calculate, invoice, get paid" turns into nine states once you account for the two rounds of review a finance team wants, the invoicing and payment phases, and the rollback path for when something has to be unwound. Ours runs: added, first check started, first check done, second check done, check complete, invoicing, payment, rollback, completed. Each transition is a point where money and an audit obligation meet, which is why the state is stored as a readable name rather than an integer code. Renumbering an integer enum silently reassigns the meaning of every historical row; a named state survives that.

Two state machines on one event

A single settlement carries more than one status. The billing record tracks its place in that nine-state flow, the settlement statement tracks a status of its own, and invoicing tracks an apply, success, and recall sequence separate from both. These grew out of different parts of the system and they are genuinely distinct concerns, so the design keeps them as separate tracks that reference the same settlement rather than forcing them into one field. The cost is that you reason about their combinations. The benefit is that an invoice reversal does not have to invent a billing state to live in, which matters in a regulated context where a reversal is a legal record, not a delete.

The bug the compiler could not see

The most expensive defect we hit was a type mismatch that static typing should have caught and did not. One entity carried the plant identifier as a string; another carried it as a number. Both compiled, both ran, and the ORM coerced between them quietly on most queries. The mismatch stayed invisible until one specific settlement join returned zero rows for data that plainly existed. Nothing threw an error. The query simply found nothing, inside an approval workflow, where "no results" reads as "nothing to approve."

The lesson reaches past "use types," because both sides were typed. The exposure is at the seam: a type boundary between two entities or two services is only as safe as the conversion that crosses it, and an ORM that coerces for you will hide a mismatch rather than surface it. We now treat an identifier that crosses a boundary as one shared type, defined once, so the two ends cannot drift apart.

Bidirectional calls are a contract you keep on both ends

The billing service and the operations service call each other. That bidirectional dependency is convenient and it is also a standing risk, because a change to the shape of a call on one side breaks the other at runtime with nothing to warn you at build time. Where two services call back and forth, the request and response shapes have to be governed as a shared contract and tested from both directions, or the coupling quietly rots until a deploy surfaces it in production.

The end-of-month problem in scheduled settlement

Monthly settlement runs on a configured day of the month, and the naive version breaks in February. A site set to settle on the 30th has no 30th in February, so the scheduled day has to be clamped to the days the month actually has. The job also has to be idempotent, because a retry after a partial failure must not generate a second settlement for the same period. Both are obvious in hindsight and both are the kind of thing that ships broken because nobody runs the calendar forward a year in their head.

Where AgentKick fits

We build and modernize financial and settlement systems where audit trails, state correctness, and cross-service consistency are the whole point. If you are working on billing, settlement, or reconciliation that has to be right and has to be auditable, that is the work we do, usually as a short scoping engagement into a phased build.

fintechjavadistributed-systemsarchitecture

Working on something like this?

Tell us the system, the timeline, and a budget range. You will get a feasibility note and rough sizing within one business day, or an honest no.

Tell us about your project