Transaction States and Atomic Execution

Imagine a bank transfer that debits one account and credits another. The power fails between the two writes. Without a disciplined notion of transaction state, the database cannot say what happened. It cannot tell if the transfer completed, half-completed, or never happened. Real systems must answer this question precisely, every time. This must hold for millions of concurrent operations. The answer comes from modeling each transaction as a process. That process moves through a small, well-defined set of states. Strict rules govern which transitions are allowed. This state model is the conceptual backbone of atomic execution. Atomicity guarantees that a transaction's effects appear either entirely or not at all. It is not merely bookkeeping. The states dictate exactly when a transaction's writes become visible. They also dictate when writes can still be undone. Finally, they dictate when the system must redo them after a crash. Every recovery algorithm rests on this lifecycle being tracked correctly. Every commit protocol depends on it too. So does every discussion of the ACID Properties. The big picture is simple to state, but subtle to implement. A transaction begins in an active state and does its work. It then reaches a fragile partially committed state right after its last operation. The system then pushes it into one of two outcomes. Either it becomes committed (permanent, durable success). Or it becomes failed → aborted (a full, clean rollback). There is no fourth outcome. This binary destiny — commit or abort, nothing in between — is exactly what atomicity means operationally. This article gives the formal state diagram. It also gives the pseudocode a transaction manager uses to drive transitions. It walks through a worked trace of a crash scenario. Finally, it covers the common ways implementers get the edge cases wrong. It builds directly on The Transaction Concept. It also sets up the machinery needed for Serializability and Schedules and Log-Based Crash Recovery.

Key takeaways
  • A transaction moves through five states: active, partially committed, committed, failed, aborted.
  • Only two terminal states exist — committed or aborted — enforcing atomicity: all-or-nothing.
  • Partially committed is a brief, fragile state after the last statement but before durability is guaranteed.
  • A transition to failed can happen from either active or partially committed, triggering rollback.
  • Aborted transactions are either retried as a fresh transaction or killed, never resumed in place.
  • The state diagram is the contract that Log-Based Crash Recovery and the Transaction Manager must honor exactly.
The core insight

A transaction is atomic because the system forces every execution into exactly one of two terminal states — committed or aborted — never leaving a partial effect stranded in between.

The idea

A transaction is a unit of work: a sequence of read and write operations bracketed by an implicit or explicit `begin` and terminated by `commit` or `rollback`. The transaction state is a label maintained by the Transaction Manager that records how far a given transaction has progressed toward finishing, and what recovery obligations apply to it right now. Precisely because failures (crashes, deadlocks, constraint violations, disk errors) can strike at any instant, the database cannot simply "run the code" — it must track, at every moment, whether the transaction's partial effects are safe to keep, must be undone, or must eventually be redone. The five states are defined as follows.

  • Active: the initial state; the transaction stays here while it is executing statements (reads and writes to the buffer/cache, not yet finalized to stable storage as a unit).
  • Partially committed: entered immediately after the final statement executes. The transaction believes it is done, but the system has not yet confirmed that all effects are safely and durably recorded. This is the shortest-lived and most dangerous state — a crash here is exactly the scenario atomicity exists to handle.
  • Committed: entered after the system confirms that all updates have been durably recorded (typically once a commit log record is flushed to stable storage). Once committed, the transaction's effects are permanent, satisfying the durability property, and cannot be rolled back.
  • Failed: entered when the system discovers that normal execution can no longer proceed — a hardware error, a violated constraint, a deadlock the scheduler chose to break, or an explicit abort request.
  • Aborted: entered after the system has rolled back the transaction — undone every effect it had made — restoring the database to its state before the transaction began.

The crucial structural fact is that committed and aborted are the only two states with no outgoing transitions: they are terminal. Every transaction, no matter how it started or what went wrong along the way, ends in exactly one of these two states. That guarantee — not any single piece of code — is atomicity. It is also tightly linked to the other ACID properties: durability is what "committed" certifies, and consistency is what the application logic and constraints are trusted to preserve on the committed path, while atomicity is enforced purely by this state machine plus the recovery mechanism that implements it.

How it works

The transaction manager is the component responsible for driving these transitions. It does not run application logic itself; it wraps the execution of read/write operations with bookkeeping and, critically, writes log records describing state transitions to the Write-Ahead Log before it lets those transitions take effect on the actual database. This "log first" discipline (the write-ahead logging rule) is what makes it possible to recover the correct state after a crash, and it is introduced lightly here because it is developed fully in Log-Based Crash Recovery. The state transition graph has exactly these directed edges: ``` active -> partially committed (on executing the final operation) active -> failed (on detecting an error / cannot continue) partially committed -> committed (on successful, durable completion of commit) partially committed -> failed (on system failure before commit record is durable) failed -> aborted (after rollback finishes) ``` No edge leads out of committed or aborted. No edge skips from active directly to committed — every transaction must pass through partially committed, even if that stay lasts microseconds. This forced intermediate step exists because "the application finished issuing statements" and "the system has durably recorded the outcome" are logically distinct events, separated by disk I/O and possibly a crash window.

Pseudocode: driving the transaction lifecycle

The following pseudocode models a transaction manager's handling of one transaction, `T`, showing exactly where each transition occurs and what log record accompanies it. ``` State(T) := ACTIVE write_log("start", T.id) function EXECUTE(T): while T has more operations: op := next operation of T try: if op is a write: buffer_update(op) // update goes to buffer/cache write_log("update", T.id, op.before, op.after) else if op is a read: perform_read(op) catch error: State(T) := FAILED ROLLBACK(T) return ABORTED // last statement has just executed successfully State(T) := PARTIALLY_COMMITTED return COMMIT_ATTEMPT(T)

function COMMIT_ATTEMPT(T): try: force_write_log("commit", T.id) // MUST be durable before declaring success catch io_error: State(T) := FAILED ROLLBACK(T) return ABORTED State(T) := COMMITTED flush_remaining_buffers_lazily(T) // may happen before or after, doesn't affect atomicity return COMMITTED

function ROLLBACK(T): // State(T) is FAILED when this begins for each ("update", T.id, before, after) in reverse(log entries of T): restore(before) // undo using the before-image write_log("abort", T.id) State(T) := ABORTED return ABORTED

function REQUEST_ABORT(T): // e.g., called by deadlock detector or user if State(T) == ACTIVE or State(T) == PARTIALLY_COMMITTED: State(T) := FAILED ROLLBACK(T) ``` Two details matter and are easy to miss. First, the commit log record must be forced to stable storage (`force_write_log`, i.e., an actual disk/flash flush, not just a buffer write) before the state is set to `COMMITTED` and before the transaction manager reports success to the caller. This ordering is what lets recovery, after a crash, distinguish "this transaction's commit record made it to disk, so redo its effects" from "it didn't, so undo them." Second, `ROLLBACK` always uses before-images captured at write time — this is only possible because the write-ahead log recorded the pre-update value before the update was applied to the buffer, foreshadowing the undo/redo logging discussed in Log-Based Crash Recovery.

Figure
The five transaction states and their allowed transitions
active executing ops partially committed committed terminal failed aborted terminal last operation executes commit record forced to disk error / cannot continue crash before commit durable rollback complete

Complexity & correctness

Time complexity. State transitions themselves are O(1)O(1) label updates. The real cost is proportional to the work each transition triggers. Executing a transaction with nn operations costs O(n)O(n) for the operations plus O(1)O(1) amortized log writes per operation, so the active phase is O(n)O(n). The transition into committed costs O(1)O(1) for the commit record plus one forced I/O (a constant-time but relatively expensive disk flush, independent of nn). The transition through failed → aborted costs O(k)O(k), where kk is the number of update log records written by this transaction so far, since rollback walks the log backward and undoes each one. In the worst case k=nk = n, so an abort late in a long transaction costs the same order as the work already done — this is the honest price of atomicity: undoing must touch everything that was done.

Space complexity. The transaction manager needs O(1)O(1) space to store the current state label per active transaction, plus O(n)O(n) space in the log for the update records of a transaction with nn writes (necessary so rollback has something to undo from). Systems bound this in practice with log truncation and checkpointing once a transaction commits, which is part of what Log-Based Crash Recovery covers.

Correctness argument. The claim to prove is: no execution can leave the database in a state that reflects only some of a transaction's writes, once the transaction manager reports a final outcome. The proof rests on three invariants maintained by the state machine and its pseudocode: 1. Single-terminal-state invariant. Every transaction's `State(T)` sequence, read as a path in the transition graph, ends at `COMMITTED` or `ABORTED`, and once there, no code path assigns a different value (inspect the pseudocode: only `COMMIT_ATTEMPT` sets `COMMITTED`, only `ROLLBACK` sets `ABORTED`, and neither calls the other). This rules out a transaction being reported as both, or neither, forever. 2. Durability-precedes-report invariant. `State(T)` is set to `COMMITTED` only after `force_write_log("commit", …)` returns successfully. Because this is a synchronous, blocking call in the pseudocode, no external observer can see a commit acknowledgment before the commit record is durable. Consequently, if the system crashes at any point before this call finishes, the commit record is not on stable storage, `State(T)` was never set to `COMMITTED`, and the recovery procedure (run at restart) is entitled to treat `T` as if it never finished — it undoes `T`'s updates using the same before-images, producing exactly the outcome `ROLLBACK` would have produced had the crash not occurred but the failure detector fired instead. This is precisely why partially committed is modeled as a distinct, separate state from committed: it is the window during which this race exists, and the invariant says the observable outcome is well-defined regardless of exactly when in that window the crash lands. 3. Undo completeness invariant. `ROLLBACK` iterates over every update log record for `T`, in reverse order, before setting `State(T) := ABORTED`. By induction on the number of updates undone, after processing all of them the database's state for every item `T` touched equals its pre-transaction value. The base case (zero updates) is trivial; the inductive step holds because each undo step restores exactly the before-image recorded at write time, and updates from other concurrently running transactions are assumed (via concurrency control, developed in Serializability and Schedules) not to have overwritten `T`'s items in a way that invalidates the before-image — a guarantee provided by locking or another concurrency-control protocol, not by the state machine itself. Together these invariants establish atomicity: the observable end state of any transaction, under any interleaving of failures, is exactly "all its writes are visible and durable" or exactly "none of its writes are visible," with the transition point unambiguously marked by the durability of the commit record.

Honest tradeoffs. Forcing the commit log record to disk is a genuine latency cost — it is the reason systems batch commits ("group commit") to amortize the flush over many transactions. Rollback cost being proportional to work done means long-running transactions that abort late are expensive to undo; this motivates keeping transactions short. The model also assumes a competent concurrency-control layer underneath; the state machine alone does not prevent one transaction's rollback from being invalidated by another transaction reading or overwriting its uncommitted data — that is the job of isolation, covered separately.

Worked example

Consider transaction `T1`, a funds transfer: debit account `A` by 50, credit account `B` by 50, both initially holding 100.

StepOperation`State(T1)`Log writtenBuffer / disk contents
0`begin`active``A=100, B=100 (disk)
1`read(A)`active—A buffer = 100
2`A := A - 50`active``A buffer = 50
3`write(A)`active—A buffer = 50 (not yet on disk)
4`read(B)`active—B buffer = 100
5`B := B + 50`active``B buffer = 150
6`write(B)`active—B buffer = 150 (not yet on disk)
7last statement finishespartially committed—A=50, B=150 in buffer only
8attempt commitpartially committed`` forced to diskcommit record durable
9commit confirmedcommitted—A=50, B=150 eventually flushed

Now trace the failure branch: suppose the system crashes at step 7, immediately after the last statement but before the commit record is forced (i.e., while `State(T1)` is still partially committed). On restart, the recovery procedure scans the log, finds `` and two update records but no `` record. By the durability-precedes-report invariant, `T1` must be treated as never having successfully finished. Recovery undoes `T1`: it applies the before-images in reverse — restore `B` to 100, then restore `A` to 100 — and (conceptually) transitions `T1` to failed then aborted. The final disk state is A=100, B=100, exactly as if `T1` had never run. No partial transfer (money debited from `A` but never credited to `B`) is ever observable, which is the concrete payoff of the state machine: atomicity held even though a real crash occurred mid-transaction. Contrast this with a crash at step 9, after the commit record was forced but before the lazy flush of buffered pages completes. Recovery now finds `` in the log, so `T1` is treated as committed; recovery redoes any updates not yet reflected on disk using the after-images (`A=50`, `B=150`), guaranteeing the committed effect survives. This redo side of recovery is developed fully in Log-Based Crash Recovery; the point here is only that the state at the moment of crash — determined entirely by whether the commit record was durable — completely determines which recovery action (undo vs. redo) is correct.

Common pitfalls & traps

PitfallWhy it happensConsequence
Treating "last statement executed" as "committed"Confusing application-level completion with durabilityReports success to the user before the commit is safe; a crash then loses a "committed" transaction
Skipping the partially committed state in a diagramSeems like a redundant extra stepHides exactly the race window where undo-vs-redo decisions matter, breaking recovery reasoning
Rolling back using current values instead of before-imagesNot logging old values, or logging after the update instead of beforeRollback restores the wrong value if the item was already changed again
Allowing a transition out of committedTrying to support "undo my commit" as a UI featureViolates durability; the correct fix is a new compensating transaction, not a state transition
Not forcing the commit log record before acknowledging commitOptimizing for latency by buffering the log writeClassic durability bug: transaction reported committed, but a crash loses it entirely
Assuming abort is freeIgnoring that rollback cost scales with work already doneLong transactions that abort late cause large, slow rollbacks, sometimes cascading into a `failed` storm

Pioneers & history

The transaction state model and its guarantee of atomicity emerged from the practical demands of early database and transaction-processing systems, where hardware failures were frequent and the cost of losing or corrupting financial records was unacceptable.

Origins · 1 minJim Gray and how this was discovered
JG
Jim Gray
1944–2007 (disappeared at sea) · Pioneer of transaction processing and database reliability at IBM and Tandem Computers

Jim Gray formalized much of the transaction concept, including atomicity, durability, and the recovery mechanisms needed to guarantee them, work recognized with the 1998 Turing Award. His writing on transaction processing systems, co-authored with Andreas Reuter, remains a foundational reference for the state-machine view of a transaction's lifecycle described in this article.

History
1976
— System R at IBM implements early transaction management with logging and locking.
1978
— Jim Gray's foundational papers formalize transactions, atomicity, and recovery.
1981
— Gray publishes "The Transaction Concept: Virtues and Limitations," cementing ACID-style thinking.
1993
— Gray and Reuter's "Transaction Processing: Concepts and Techniques" becomes the standard reference.
1998
— Jim Gray receives the Turing Award for contributions to database and transaction processing research.

Where this matters

Every production relational database — PostgreSQL, MySQL/InnoDB, Oracle, SQL Server — implements exactly this state machine internally, exposing it to applications only through `BEGIN`, `COMMIT`, and `ROLLBACK` statements while enforcing the durability-precedes-report invariant via write-ahead logging (WAL) or equivalent mechanisms. Distributed systems extend the same core states across multiple nodes: two-phase commit protocols add a "prepared" sub-state that refines partially committed into a coordinator-visible checkpoint, ensuring atomicity across machines rather than just across disk writes on one machine. NoSQL and NewSQL systems that offer multi-document or multi-row transactions (MongoDB with multi-document transactions, Google Spanner, CockroachDB) implement the same lifecycle, often layering it under stronger isolation guarantees. Financial systems, airline reservation systems, and e-commerce checkout flows all depend on this state machine to guarantee that a payment, a booking, or an order is never left half-applied. Understanding this lifecycle is also the necessary prerequisite for understanding how Log-Based Crash Recovery decides, after a crash, which transactions to redo and which to undo, and how Serializability and Schedules reasons about the interleavings of many active transactions running through these same states concurrently.

Ask the tutor: How does the "partially committed" state interact with two-phase commit in distributed transactions?

Why is there a separate "partially committed" state instead of going straight from active to committed?

Because "the application finished its statements" and "the system has durably guaranteed the outcome" are genuinely different events, separated by at least one disk I/O (forcing the commit log record). A crash can occur in that gap. Modeling it as its own state makes the race window explicit and lets recovery reason correctly: if the commit record never made it to stable storage, the transaction must be undone even though every statement it issued actually ran to completion in memory. Without this state, a diagram would wrongly suggest that finishing all statements is equivalent to being safely committed, which is exactly the assumption that causes lost-update and lost-commit bugs in careless implementations.

Can a transaction go from "aborted" back to "active" and resume where it left off?

No. Once a transaction is aborted, its effects have been fully rolled back and the transaction manager discards its execution context. If the application wants to try the same logical work again, it starts an entirely new transaction — typically with a new transaction ID — that reissues the same operations from the beginning. This is deliberate: allowing resumption from a partially rolled-back or ambiguous state would reintroduce exactly the "partial effect" problem atomicity is designed to eliminate, since the system could not simply guarantee where in the original execution to safely restart.

Is "failed" always caused by a hardware crash?

No. "Failed" is a broader state covering any situation where the transaction cannot proceed normally, including a violated integrity constraint, a deadlock that the concurrency-control system chose to break by aborting this transaction, an explicit `ROLLBACK` request from the application, or a disk I/O error. A hardware crash is only one of several causes; in fact, in most systems most aborts are triggered by application logic or deadlock resolution, not by hardware failure. Regardless of the cause, the transition into "failed" always leads to the same rollback procedure and the same terminal "aborted" state.

Does committing guarantee the data is immediately written to the disk pages holding the table?

Not necessarily. What committing guarantees is that the commit log record is durable, which is sufficient because the log contains enough information (the after-images) to redo the update even if the actual data pages were still sitting in a volatile buffer when the crash happened. This separation — log durability implies logical durability, even if physical pages lag behind — is what allows database systems to delay writing dirty pages to disk (improving performance) without weakening the durability guarantee. The mechanism that reconstructs lagging pages from the log after a crash is redo processing, covered in Log-Based Crash Recovery.

How does this state model relate to the ACID properties as a whole?

The state machine is the mechanism by which atomicity and durability are enforced: atomicity because every transaction ends in exactly one of two mutually exclusive terminal states, and durability because the transition to "committed" is defined precisely as the point where the commit log record becomes permanent. Consistency is a property of the application's logic and integrity constraints, checked before allowing a transition to "partially committed" rather than enforced by the state machine itself. Isolation, which governs how concurrently active transactions interact, is handled by a separate concurrency-control layer discussed in Serializability and Schedules; the state diagram here describes a single transaction's lifecycle in isolation from that concern.

What happens if two transactions are both writing to the database when one aborts?

The state machine by itself does not answer this — it only guarantees that the aborting transaction's own writes are undone using its before-images. Whether that undo is safe and correct in the presence of a concurrently running second transaction depends entirely on the concurrency-control protocol (locking, timestamp ordering, multiversion concurrency control) enforcing isolation, which prevents the second transaction from reading or overwriting the first transaction's uncommitted data in ways that would make rollback impossible or incorrect. This interaction between transaction state and concurrent schedules is exactly the subject developed in Serializability and Schedules.

Why does the pseudocode force the log write synchronously instead of writing it asynchronously for speed?

Because the entire correctness argument for durability depends on the commit record being on stable storage before the transaction manager reports success. If the log write were asynchronous, the caller could receive a "committed" response while the record still sits in a volatile buffer, and a crash immediately afterward would silently lose that transaction despite the earlier success report. Real systems still optimize this cost — for example by grouping several transactions' commit records into a single flush ("group commit") — but the requirement that the flush completes before acknowledging success is never relaxed, only made cheaper per transaction.

Before this

Where this leads