Build Your Own PostgreSQL
Transactions and the illusion of instant change
What You Will Learn
- Explain how transaction status makes physical tuple traces interpretable
- Trace commit and abort without assuming immediate physical undo
- Distinguish statement autocommit, explicit transactions, and savepoints
Many steps, one meaning
A bookstore confirms an order, reserves inventory, and records payment. Those statements execute one after another, but the business meaning is indivisible: either all three effects belong to the database history or none do. A transaction supplies that boundary. The central invariant is not that bytes change simultaneously; it is that every observer interprets their physical traces according to one transaction outcome.
Internally, buffers are dirtied, WAL records are appended, index entries appear, and tuple versions are created at different moments. If the transaction aborts, PostgreSQL does not need to erase every heap byte immediately. A tuple inserted by an aborted transaction can remain physically present until cleanup, but transaction status ensures no valid snapshot treats it as committed data.
Status makes bytes interpretable
The transaction manager assigns identities to transactions and tracks their outcomes. A transaction may be in progress, committed, aborted, or in a prepared state for two-phase commit. Tuple headers store the creating and deleting transaction identifiers. Visibility checks consult transaction status to decide whether a tuple version belongs to the snapshot being read. So transaction state is part of ordinary row access, not a separate ledger consulted only at commit.
Transaction path
Trace commit and abort
Transaction A inserts an order. Its tuple header names A as creator, while A's status is still in progress. A's own command can account for its work, but other snapshots do not treat the version as committed. If A commits, durable WAL records the outcome and later visibility checks can accept the version when their snapshot rules allow it. The tuple bytes did not jump into existence at commit; their interpretation changed.
Transaction B inserts a reservation and then aborts. Its tuple may remain in the heap and its index entry may remain as a candidate route. Status marks B aborted, so visibility checks reject the version. Vacuum can reclaim the dead traces after no relevant access still needs them. Commit and abort therefore close an interpretation boundary first; physical cleanup follows on a different schedule.
The file-system version breaks fast
Direct autocommit writes to files show why the boundary matters. If a process crashes after the debit but before the credit, the stored state is wrong. If two users update related data without a shared boundary, the database cannot know which partial states are forbidden. Transactions give users a way to tell the system which actions belong together. Without that boundary, correctness leaks into every application caller.
Autocommit does not remove transactions. It wraps each statement in its own transaction. That is a practical default because many statements are complete on their own. But multi-step business changes still need explicit transaction boundaries. The database cannot infer that three separate statements form one unit unless the session says so.
Rollback is often misunderstood as time travel that erases every byte immediately. In an MVCC design, rollback is closer to changing how bytes already written are interpreted. If a transaction inserts a tuple and aborts, the tuple version may remain on disk, marked by its creating transaction's aborted status. Future readers skip it. Vacuum later reclaims the space. This delayed physical cleanup makes abort cheaper and keeps concurrency manageable.
Boundaries inside boundaries
Savepoints add nested recovery points inside a transaction. They do not create independent committed units, but they let a session recover from part of its work. If a statement fails after a savepoint, the transaction can roll back to that point and continue. This helps with complex application flows where one optional operation may fail without invalidating the entire transaction. Internally, the system must track subtransaction effects and visibility carefully.
Isolation is the second major illusion, and it is subtler than atomicity. A transaction should not see a random blend of other transactions' half-finished work. Different isolation levels offer different promises about what changes are visible and when. Even before studying those levels in detail, the core idea is clear: transaction boundaries organize time for readers and writers.
Durability completes the visible promise. Once a transaction commits, its effects must survive a crash according to the configured policy. Atomicity without durability would let complete changes vanish. Durability without atomicity would preserve broken partial changes. Transactions bind these properties together so users can reason about state transitions rather than storage events.
The transaction manager must also handle wraparound. Transaction identifiers are finite. If the system keeps assigning them forever without care, old and new identities can become confused. PostgreSQL therefore freezes old tuple metadata and vacuums aggressively enough to prevent ambiguity. Even abstract properties eventually meet physical limits.
Three transaction questions
Ask which statements share one outcome, which status makes their physical traces interpretable, and which observers may see that outcome. Then ask what remains after abort: tuple bytes, index candidates, WAL records, locks, and application side effects do not all disappear by the same mechanism. This inventory clarifies why rollback can restore database meaning without reversing an email already sent by application code.
Into MVCC
A transaction is the unit users think in, but readers still need a precise rule for which committed and in-progress changes they can see. That rule is MVCC. It lets old and new versions coexist so readers do not block writers by default, and writers do not erase the past out from under readers.
From above, the transaction is a clean boundary around intent. From below, it is metadata on tuple versions, log records, status bits, and locks. MVCC is where those two views meet and become readable as time.
That bridge is what lets many transactions share one database without sharing one instant.