Database Transactions
A bank teller who won't let you leave mid-transfer
July 10, 2026 · 8 min read
The problem: work that can't be allowed to happen halfway
Picture a transfer of $500 from your checking account to your savings account. Under the hood, that's at least two separate writes to the database: subtract $500 from checking, then add $500 to savings. Now picture the exact wrong moment for the server to crash, the network to drop, or the process to be killed: right after the first write commits, and before the second one runs. If nothing guards against this, you've just made $500 disappear — not stolen, not misplaced, just gone, because the system did the first half of a two-part promise and never got to the second half. Multiply this by every transfer happening across every account at a bank, every second, and it stops being a rare edge case and becomes a near-certainty that it will eventually happen to someone, on some server, at some unlucky instant.
This shape of problem is everywhere once you start looking for it, and it's rarely confined to banking. An e-commerce checkout has to charge the customer's card and decrement the item's inventory count — charge them without reserving the item, and you've sold something you don't have; reserve the item without the charge going through, and you've given away inventory for free. A signup flow creates a user record, provisions their account, and sends a welcome email — crash between the first two steps and you've got a user who exists but can't log in, silently broken until someone notices and can't figure out why. In every one of these cases, the code that looks correct on a whiteboard — "first do this, then do that" — is quietly assuming that the computer will either finish everything or fail before starting anything, and computers don't actually offer that guarantee for free.
Like this
A bank teller doesn't hand you a debit receipt and then wander off to lunch before finishing the credit side of your transfer. The whole transfer happens, completely, before you're told it's done — or none of it happens at all, and you're told that instead.
Like this
A bank teller doesn't hand you a debit receipt and then wander off to lunch before finishing the credit side of your transfer. The whole transfer happens, completely, before you're told it's done — or none of it happens at all, and you're told that instead.
What a transaction actually guarantees
A transaction is a boundary you draw around a group of operations, telling the database: treat everything inside this boundary as one indivisible unit. You open the boundary (BEGIN), perform however many reads and writes the operation actually needs, and then close it in one of exactly two ways: COMMIT, which makes every change inside permanent and visible all at once, or ROLLBACK, which undoes everything inside as if none of it had ever been attempted — including the parts that technically already ran. There is no third outcome where some of it stuck and some of it didn't. That's the entire contract, and it's a remarkably strong one: from the outside, a transaction either happened or it didn't, with nothing observable in between.
Making that promise real is genuinely hard work, and it's worth knowing roughly what the database is doing on your behalf so the guarantee doesn't feel like magic. Before it touches the actual data files, it writes down, to a durable log, exactly what it's about to do and why — this is usually called a write-ahead log. If the process dies mid-transaction, the database doesn't have to guess what state it was left in; on restart, it reads that log, finishes replaying any transaction that had fully committed before the crash, and discards any partial work from transactions that hadn't. You never write this recovery logic yourself. It's the entire reason the transaction boundary is worth drawing: all of that bookkeeping happens once, correctly, inside the database, instead of being reinvented — and inevitably gotten wrong in some rare edge case — inside every application that uses it.
Like this
The teller doesn't tell you "transfer complete" and then go fill out the paperwork later from memory. The paperwork — the actual record of what happened — is what makes it real, and it's filed as part of the transfer itself, not as an afterthought.
Like this
The teller doesn't tell you "transfer complete" and then go fill out the paperwork later from memory. The paperwork — the actual record of what happened — is what makes it real, and it's filed as part of the transfer itself, not as an afterthought.
Isolation: pretending you're the only one in the room
Real databases don't process one transaction at a time and then politely wait for the next customer — hundreds or thousands of transactions are often in flight simultaneously, reading and writing overlapping data. Isolation is the guarantee that, even so, your transaction behaves as though it had the entire database to itself for the duration it runs. Without it, you get what are usually called read phenomena: a dirty read, where you see a change from another transaction that hasn't even committed yet and might still be rolled back; a non-repeatable read, where you query the same row twice in one transaction and get two different answers because someone else's committed change slipped in between; a phantom read, where a whole new row matching your filter appears between two identical queries in the same transaction. Each of these can quietly corrupt logic that assumed the data underneath it was holding still.
The catch is that perfect isolation and high throughput pull in opposite directions. The strictest isolation level effectively serializes conflicting transactions — making them queue up and wait for each other — which is exactly what protects you from every one of those read phenomena, but it also means less real concurrency, more waiting, and more chances for one slow transaction to hold up several others behind it. This is why databases expose multiple isolation levels instead of forcing the strictest one on every query: a reporting query that can tolerate a slightly stale, consistent-enough view of the data doesn't need to pay the same cost as a transaction that's moving real money, and being able to choose the weaker guarantee where it's safe is what keeps the whole system fast.
Like this
A strict teller won't let another customer even glance at your paperwork while your transfer is mid-flight. A more relaxed one might let someone peek at yesterday's already-settled balance while you're being helped — faster for everyone, as long as nobody needs to see your transfer itself while it's still happening.
Like this
A strict teller won't let another customer even glance at your paperwork while your transfer is mid-flight. A more relaxed one might let someone peek at yesterday's already-settled balance while you're being helped — faster for everyone, as long as nobody needs to see your transfer itself while it's still happening.
Durability: surviving the crash right after you hit save
Durability is the guarantee that once a transaction has committed — once the database has told you it succeeded — that result survives, even if the power is cut to the server one millisecond later. This sounds like it should be obvious, but it's a genuinely deliberate engineering property, not a side effect of writing data to disk. A commit doesn't return "success" to you until the write-ahead log entry for that transaction has actually been flushed to durable storage; if the database acknowledged success before that flush and the machine lost power in between, you'd have a system that lies to you about what it saved, which is arguably worse than one that's simply slow.
On restart after any crash, a database doesn't trust the last-known state of its data files at face value — it replays its write-ahead log from the last consistent checkpoint, reapplying every transaction that had fully committed and discarding every transaction that hadn't. This is exactly what lets the durability guarantee hold up against the messiest failure mode there is: not a clean shutdown, but the plug being pulled at the worst possible instant. You never have to ask "did that actually save, though?" after the fact — durability is the reason that question already has a guaranteed answer.
Like this
The teller doesn't wait until the end of their shift to file today's transfers. Each one is filed the moment it's finalized, specifically so that if the building loses power that night, your transfer already happened — on paper, permanently — regardless of what happens to the building.
Like this
The teller doesn't wait until the end of their shift to file today's transfers. Each one is filed the moment it's finalized, specifically so that if the building loses power that night, your transfer already happened — on paper, permanently — regardless of what happens to the building.
The cost: why you don't wrap everything in one giant transaction
Given how strong these guarantees are, it's tempting to reach for one big transaction around anything that touches multiple pieces of data. The cost is concurrency: a transaction typically holds locks on the rows it touches until it commits or rolls back, and the longer it stays open, the longer everything else waiting on those same rows has to sit in line behind it. A transaction that does one fast, well-scoped job returns and releases its locks quickly; a transaction that tries to do everything, including slow work like calling an external API or waiting on user input in the middle, can end up blocking a meaningful slice of your whole system for as long as it stays open.
It gets harder still the moment a single logical operation spans more than one database — which is the normal case in a microservice architecture, where the order service, the inventory service, and the payments service each own their own data and their own database. A transaction, in the classic sense, doesn't stretch across that boundary; there's no single write-ahead log shared between separate services. This is exactly why distributed systems tend to trade strict, single-transaction atomicity for patterns like sagas — a sequence of local transactions, each in its own service, paired with an explicit compensating action for every step, so that if step three fails, steps one and two are deliberately undone rather than assumed to roll back automatically. It's more code to write, and it asks you to think honestly about what "undo" means for each step, but it's the accepted price of a system built from several databases instead of one.
Like this
One teller, handling your whole transfer themselves, can make the all-or-nothing promise easily. Once the job is split across tellers at three different bank branches, each keeping their own separate ledger, "all or nothing" stops being something any single teller can promise alone — someone has to explicitly call the other branches and say "undo that" if one part fails.
Like this
One teller, handling your whole transfer themselves, can make the all-or-nothing promise easily. Once the job is split across tellers at three different bank branches, each keeping their own separate ledger, "all or nothing" stops being something any single teller can promise alone — someone has to explicitly call the other branches and say "undo that" if one part fails.
Got a concept you want explained like this?
Ask me about itRelated explainers
New explainers, straight to your inbox
One email whenever a new concept goes up. No spam, unsubscribe anytime.