ACID
Concept
Four properties a database guarantees so that grouped work can't leave data broken when it's interrupted or when two operations overlap.
Why it exists
Changing data rarely takes one line. A single transfer between accounts means reducing one balance, increasing another, and writing a record of what happened — three operations. The trouble is the gap between them. If the server dies between the first and the second, the money leaves one side and arrives nowhere.
Two people touching the same row at once produces the same class of damage. With one seat left, two simultaneous checkouts both read "1 remaining" and both write 0, leaving inventory at zero against two orders. This is a race condition.
Early on, application code defended against all of this by hand. Every feature carried its own logic to undo whatever had already been done when a step failed. That undo path could itself fail partway, which had to be handled too, and as features multiplied the defensive code diverged until nobody could say which parts of the system were safe. Pushing the guarantee out of the application and onto the database is how that ended, and ACID is the name for the four promises that got written down. The letters stand for atomicity, consistency, isolation and durability, which is why the term usually arrives as an acronym rather than as four separate ideas — they were adopted together as one package.
How it works
Send several operations as one transaction — a group that must either fully succeed or fully unwind, with its start and end marked in code — and the database promises four things about the group.
- Atomicity — every operation in the group lands, or none does. Lose power mid-transfer and the sending balance returns to what it was. Underneath, the database writes down what it intends to change before changing it, then reads that record backward to undo the work if the group breaks.
- Consistency — the group can't finish in a state that breaks a rule. Declare that a balance may not drop below zero, or that no order can reference a nonexistent member, and any group violating it is rejected whole.
- Isolation — concurrent groups can't see each other's half-finished work. In the seat example, the first group holds that row while the second waits, and when the second's turn comes it reads the already-updated zero and blocks the purchase.
- Durability — once the database answers "done," the result stays done. It writes to disk before replying, so a server that dies immediately afterward comes back with the change intact.
The four cover different failures. Atomicity and durability handle being cut off partway; consistency and isolation handle colliding with another operation or a rule. Isolation is the one that fights performance most often. Strict serialization is safe but slow, so databases offer isolation levels and let each feature pick. Anything touching money runs at the strictest level while a read-only dashboard runs looser. The looser levels have specific failure modes worth knowing: at one level the same query run twice inside a group can return different rows because another group committed in between. That's acceptable on a dashboard and unacceptable when the second read decides whether to charge someone.
Where it shows up
Whether a workload needs these properties drives the choice of database. A relational database — rows and columns in tables, with tables pointing at each other by value — provides all four out of the box. NoSQL products skip the fixed table shape, and many of them originally guaranteed these properties only within a single record. More of them now cover multi-record groups, but coverage varies by product, so it's worth reading the documentation before choosing.
The properties also shape feature design. Money, inventory, permissions — anything a human would have to repair by hand if it drifted — goes inside one transaction. Counters and activity logs, where a few wrong entries hurt nobody, stay outside, because every additional operation inside a group extends how long other requests wait. Length is the practical constraint. A group that calls an external service and waits two seconds for a reply holds its rows for those two seconds, and under load that single choice is enough to stall unrelated requests queueing behind the same rows.
Knowing the boundary matters just as much. The guarantee holds inside one database. Split data across servers with sharding, or break a system into services that each own their storage, and work crossing those lines falls outside all four. That work needs its own compensating steps for partial failure, plus idempotency so that retrying a request produces the same result rather than a second charge.
How it sounds at work
"Put the payment record and the seat increase in one transaction. ACID covers us there."
Handled separately, a team could end up charged without any new seats, so group them.
"This spans two services, so ACID won't help."
The work doesn't finish inside a single database, which puts it outside the guarantee and means the undo path has to be written by hand.
"Check whether this screen can drop an isolation level first."
Asking whether this is a place where a little accuracy can be traded for speed.
At Pagedeck the conversation came up around the upgrade flow from free to paid. After payment cleared, three things had to follow: switch the plan, raise the seat count to what was purchased, and activate members who had been sitting in the invite queue. The first implementation ran all three separately, and an error in the notification step left several teams stopped halfway.
The symptoms varied. Some teams were charged with the seat count unchanged; others got the seats but never saw their queued members appear. Theo grouped the three into one transaction and moved the notification outside it. A notification that fails can simply be sent again, so it had no reason to be in the group.
What people get wrong
The common assumption is that ACID means data can never drift. The database says it guarantees things, and the trust extends further than it should. The unit of guarantee is one transaction inside one database. Work that crosses a boundary — charging through a payment gateway while recording seats locally — sits outside it. Designing without knowing that produces a slow pile of records where the external charge succeeded and the internal write never happened, and from then on someone reconciles two lists by hand every month.
The second is that turning everything up is always safer. Safety and speed don't move together. At the strictest isolation level, requests touching the same row queue behind each other, and during traffic peaks that queue turns into visible latency. Real designs don't max everything out; they decide per feature which values must never drift and apply strictness only to those.
Where to go next
- Database
see where these four properties sit among everything a database handles. Choosing a type, shaping the schema, and tuning performance all interact with consistency, and that map is laid out there
- Transaction
the thing the four properties actually attach to. How a group gets opened and closed in code, what conditions roll it back, and what slows down when a group runs long
Last updated 2026-09-16