Most of my career has been in fairly typical product engineering, where a bug means a frustrated user and a quick hotfix. Working on an enterprise banking-sector application was a different category of work entirely — not harder in a purely technical sense, but governed by a different set of defaults that reshaped how I write code everywhere else, too.
Auditability is a feature, not an afterthought
In most products, logging is a debugging convenience. On a banking-grade system, every state-changing action needed a clear, immutable trail of who did what, when, and why — not because anyone expected fraud, but because the ability to reconstruct exactly what happened after the fact is a compliance requirement, not a nice-to-have. That habit — treating audit trails as a first-class design concern rather than something to bolt on later — has stuck with me on every project since, even ones with no regulatory requirement at all.
If you can't reconstruct exactly what happened and why, you don't actually understand your own system — you're just hoping it behaves.
Idempotency stops being optional
Network failures, retries, and double-submissions are everywhere in real systems, but in most consumer apps the worst case is a duplicate row you can clean up later. In a financial context, a retried request that isn't idempotent can mean a duplicated transaction — a genuinely serious problem. I got in the habit of asking "what happens if this exact request arrives twice?" for every mutation, and designing idempotency keys in from the start rather than patching them in after an incident.
async function createTransaction(input: TransactionInput) {
// idempotencyKey is generated client-side and unique per attempt
const existing = await db.transaction.findUnique({
where: { idempotencyKey: input.idempotencyKey }
});
if (existing) return existing; // safe to retry
return db.transaction.create({ data: input });
}
Code review gets slower, on purpose
Review cycles on this kind of work are intentionally more deliberate — more reviewers, more explicit sign-off, more discussion of edge cases that would get waved through elsewhere. It felt slow at first. Over time I came to see it as the correct trade: shipping a day later is nearly free; shipping a financial calculation error is not.
| Typical product work | Banking-grade work |
|---|---|
| Logs for debugging | Immutable audit trail for every state change |
| Retries handled loosely | Idempotency designed in from the start |
| Single reviewer, fast merge | Multiple reviewers, explicit edge-case sign-off |
| Feature flags for gradual rollout | Feature flags plus formal change control |
Input validation happens at every boundary, not just the edge
It's tempting to validate input once, at the API boundary, and trust it internally from there. On this project, validation happened again at each service boundary internally too — not because anyone distrusted the previous layer specifically, but because the cost of a bad assumption compounding silently through several services was too high to accept. It's more code. It's also meaningfully harder to introduce a silent data-corruption bug.
What carried over to everything else
I don't apply banking-level process to every project — that would be the wrong trade-off for a marketing site or an early-stage MVP where speed matters more than ceremony. But three specific habits carried over permanently: treating audit trails as a real design concern, asking "what if this runs twice" for anything that mutates state, and being honest with myself about which parts of a system actually deserve the extra scrutiny. Knowing when to apply that level of care — and when not to — turned out to be the actual skill.