Module P-3·21 min read

BEGIN, COMMIT, ROLLBACK, isolation levels, and safe atomic operations — what every production application must understand.

P-3 — Transactions and ACID in Practice

Who this module is for: You can write queries and design schemas. Now you need to understand how to group multiple operations into an atomic unit — so that either all of them succeed or none of them do. This is the mechanism that prevents your application from leaving the database in a half-updated, inconsistent state.


The Problem Transactions Solve

Imagine a bank transfer: debit $100 from Alice, credit $100 to Bob.

sql

What happens if the server crashes, the network drops, or an error occurs between the two statements? Alice has lost $100 and Bob never received it. The money has vanished.

Transactions prevent this by grouping operations: either both updates happen, or neither does.


BEGIN, COMMIT, ROLLBACK

sql

If anything goes wrong between BEGIN and COMMIT:

sql

Without a BEGIN, every statement is its own transaction — it auto-commits immediately. This is fine for isolated statements, but dangerous for multi-step operations.

With a BEGIN, nothing is visible to other connections until COMMIT. If the session disconnects before COMMIT, PostgreSQL automatically rolls back the transaction.


ACID: What It Actually Means

A — Atomicity: All operations in a transaction succeed, or none do. There is no partial success.

sql

C — Consistency: A transaction brings the database from one valid state to another. Constraints (NOT NULL, FOREIGN KEY, CHECK) are enforced at commit time.

sql

I — Isolation: Transactions run as if they are the only transaction in the system. Other transactions' uncommitted changes are invisible.

sql

D — Durability: Once committed, changes survive crashes. PostgreSQL's Write-Ahead Log (WAL) ensures this.


Isolation Levels in Practice

PostgreSQL supports four isolation levels. The default — and the one you will use for almost everything — is READ COMMITTED.

READ COMMITTED (default)

Each query in a transaction sees a fresh snapshot of committed data at the time that query starts. This means a transaction can see different data at the beginning vs. the end if other transactions commit in between.

sql

This is acceptable for most application reads. Each query gets a consistent view; the transaction as a whole does not.

REPEATABLE READ

The entire transaction sees the same snapshot of data as of when the transaction started. New inserts/updates by other transactions are invisible for the duration.

sql

Use this when your transaction needs a consistent view across multiple queries — for example, generating a report where all queries must see the same data point in time.

SERIALIZABLE

The strongest level. Transactions execute as if they were serialised one after another. PostgreSQL detects situations where concurrent transactions could produce results different from any serial execution and fails one of them.

sql

Use this sparingly — it adds overhead and can cause transaction failures that need retrying.

Rule for most applications: use the default READ COMMITTED. Move to REPEATABLE READ only when you need a consistent multi-query snapshot. Use SERIALIZABLE only for complex financial operations where non-serialisable anomalies are a real risk.


SAVEPOINT — Partial Rollback

A SAVEPOINT marks a point within a transaction you can roll back to without abandoning the entire transaction.

sql

Transaction Best Practices

Keep transactions short

A transaction holds locks on the rows it modifies until it commits. Long-running transactions block other operations and accumulate dead tuples (see Phase 3, autovacuum).

sql

Never ignore transaction errors

In application code, check for errors after every database call. In PostgreSQL, once an error occurs inside a transaction, the transaction is aborted — all subsequent statements fail until you ROLLBACK.

sql

In application code (Node.js example):

javascript

SELECT ... FOR UPDATE — Pessimistic Locking

When multiple transactions need to read and then modify the same row, you need to prevent another transaction from changing that row between your SELECT and your UPDATE.

sql

Without FOR UPDATE, two sessions could read the same balance, both decide they can proceed, and both subtract — leading to a negative balance.

FOR UPDATE SKIP LOCKED — useful for job queues:

sql

Multiple workers can run this concurrently — each claims a different task because SKIP LOCKED skips rows already locked by another worker.


Common Transaction Patterns

Safe inventory deduction

sql

Atomic counter increment

sql
sql

Practical Exercise: Safe Bank Transfers

sql

Summary

ConceptKey Takeaway
BEGIN / COMMIT / ROLLBACKGroup operations atomically; ROLLBACK undoes all changes since BEGIN
Auto-commitWithout BEGIN, every statement is its own transaction
READ COMMITTED (default)Each query sees the latest committed data; use for most application work
REPEATABLE READEntire transaction sees same snapshot; use for consistent multi-query reports
SERIALIZABLEStrongest isolation; use for complex financial operations
SAVEPOINTPartial rollback to a named point within a transaction
Keep transactions shortLong-held locks block other transactions and cause dead tuple accumulation
Handle errorsAfter any error, a transaction is aborted — you must ROLLBACK before continuing
FOR UPDATELock rows at read time to prevent concurrent modification
FOR UPDATE SKIP LOCKEDClaim rows without blocking — the correct pattern for job queues

Module P-4 covers schema design for real applications — normalisation, the correct data types for money and time, soft deletes, audit fields, and the migration tools that keep schemas manageable as they evolve.

Next: P-4 — Schema Design for Real Applications →

Knowledge Check

You are designing a job queue system in PostgreSQL where multiple background workers concurrently claim and process pending jobs from a tasks table. If multiple workers frequently attempt to claim the exact same job simultaneously, which concurrency control mechanism is the most efficient and robust choice to prevent locking contention and ensure high throughput?


During a complex financial reconciliation process, an application uses the default READ COMMITTED isolation level. The application executes a SELECT COUNT(*) on a ledger table, then performs some internal logic, and executes the exact same SELECT COUNT(*) again within the same transaction. The second query returns a different result than the first. What is the fundamental reason for this behavior?


A backend developer has written an API endpoint that opens a database transaction, queries a pending_orders table, makes an HTTP request to a third-party payment gateway that takes 4 seconds, and finally updates the order status to confirmed before committing. From a database performance perspective, why is this an anti-pattern?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.