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.

transactions.tsts
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 workBanking-grade work
Logs for debuggingImmutable audit trail for every state change
Retries handled looselyIdempotency designed in from the start
Single reviewer, fast mergeMultiple reviewers, explicit edge-case sign-off
Feature flags for gradual rolloutFeature 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.