Transactions matter because many important operations are not truly one-step changes.
When multiple reads and writes belong to one business action, PostgreSQL needs a way to keep them correct together.
Concurrency matters because databases serve multiple users and processes at once, which creates overlap, contention, and possible conflict.
This topic is about protecting truth under real application conditions, not only in calm single-user demos.
A business action often spans several statements: create an order, reserve inventory, write payment metadata, and record an audit trail. If only half of that work succeeds, the data may become misleading or broken.
Transactions solve this by treating related operations as one protected unit of work. Either the meaningful set completes, or the system rolls back to a safer state.
In real systems, multiple users or jobs may read and update the same data at overlapping times. That means the database has to manage isolation and consistency under pressure, not just in single-threaded examples.
Concurrency bugs are especially nasty because they may not appear during light manual testing. They often emerge only when real traffic overlaps.
A transaction groups statements into one logical unit. BEGIN starts the unit, COMMIT makes all successful changes visible, and ROLLBACK discards them. PostgreSQL also wraps every standalone statement in an implicit transaction. Explicit transactions matter when a business action requires several statements to succeed or fail together.
Consider transferring money. The debit and credit must not commit separately. Lock the required account rows in a consistent order, validate the balance, write both entries, and commit. If validation or any statement fails, rollback preserves the invariant. Constraints remain valuable because they protect data even when application code has a defect.
Concurrent transactions do not simply run one after another. PostgreSQL uses MVCC so readers can often proceed without blocking writers. Each statement or transaction sees a snapshot according to its isolation level. Row locks coordinate conflicting writes, but poorly designed access order can still create waits or deadlocks.
Professionals think in business invariants: what must always remain true even when many users act simultaneously? Then they choose transaction boundaries, locking strategies, or isolation approaches that support those invariants.
This mindset is more valuable than memorizing concurrency vocabulary alone because it ties database behavior back to business correctness.
Run two concurrent purchases against the same inventory row. Lock the row, verify remaining quantity, update it, and commit so stock never becomes negative.
A read-then-write sequence outside one transaction allows both sessions to approve the same stock. Retrying serialization failures without idempotency can duplicate an order.
Verification must use evidence that matches the concept. Coordinate two sessions, inspect waiting locks, assert one valid outcome, and test rollback plus bounded retry behavior. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.
Read Committed gives each statement a fresh snapshot and is the default. Repeatable Read keeps one transaction snapshot and prevents several anomalies, but serialization failures can still occur. Serializable provides the strongest behavior by detecting dangerous dependency patterns; applications must be prepared to retry the whole transaction.
Deadlocks occur when transactions wait on each other in a cycle. PostgreSQL detects the cycle and aborts one participant. Inspect pg_stat_activity and pg_locks during contention, keep a stable lock order, index statements that locate rows for update, and avoid network calls while locks are held.
Retries must be bounded and idempotent. Retry serialization failures and deadlock victims with jitter, but do not blindly retry validation errors. If the transaction triggers an external side effect, use an outbox or idempotency record so a database retry does not send duplicate payment or email requests.
At Read Committed, each statement sees a snapshot taken when that statement begins. Two SELECT statements in one transaction can therefore observe different committed data. Repeatable Read uses one transaction snapshot, giving stable reads, while Serializable also detects dependency patterns that could not occur in a serial execution. Stronger isolation reduces anomaly risk but may increase aborts and required application retries.
SELECT FOR UPDATE locks selected rows against conflicting changes and is appropriate when the application must read current state before a guarded update. Lock only rows that belong to the invariant and keep the transaction short. Missing rows cannot be locked in the same way, so uniqueness constraints, exclusion constraints, serializable transactions, or advisory coordination may be needed for “create if absent” rules.
Savepoints allow part of a transaction to be rolled back without discarding earlier work, but they do not make an unsafe workflow correct or release every resource as if the transaction ended. Use them for a deliberate recoverable unit, and keep error handling explicit so the final commit cannot include a partially failed business action by accident.
Prefer one guarded UPDATE when the invariant can be expressed in its WHERE clause. Decrement inventory with WHERE available >= requested and inspect the affected-row count; zero means the precondition failed. This avoids a read-then-write race and keeps the decision inside one statement. A surrounding transaction is still needed when several rows or tables must change together.
When PostgreSQL aborts a deadlock victim, preserve the deadlock log detail, statements, transaction order, and lock targets. Fix the cycle by acquiring shared resources in a consistent order or reducing lock scope; increasing a timeout does not remove the cycle. Retry the complete transaction with a fresh snapshot and a bounded randomized delay.
A session left idle in transaction can retain a snapshot and locks, delay vacuum cleanup, and consume a connection while doing no useful work. Set appropriate idle-transaction and statement timeouts, commit or roll back in every code path, and avoid holding a database transaction open while waiting for a user or remote service.
This kind of workflow explains why transactions are essential.
Place order -> reduce stock -> save payment result -> write order history entry; if one critical step fails, the database should not pretend the full business action succeeded
BEGIN;
SELECT quantity FROM inventory WHERE product_id = 7 FOR UPDATE;
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 7 AND quantity >= 1
RETURNING quantity;
COMMIT;
Lock accounts in deterministic order and update both inside one transaction.
BEGIN;
SELECT id, balance FROM accounts
WHERE id IN (10, 20)
ORDER BY id
FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 10;
UPDATE accounts SET balance = balance + 100 WHERE id = 20;
COMMIT;
Use PostgreSQL activity views before terminating anything.
SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE datname = current_database();
SELECT locktype, relation::regclass, mode, granted, pid
FROM pg_locks
ORDER BY granted, pid;
Not every write needs complex transaction boundaries, but related changes that must succeed or fail together usually do.
Because they often require overlapping traffic or timing that does not happen during simple manual testing.
Explore 500+ free tutorials across 20+ languages and frameworks.