FullStack Course LogoFullStack Course
Module: SQL
SQL·192·13 MIN READ

192: Transactions and ACID: Atomicity, Consistency, Isolation, and Durability

TOPICS COVERED: Transactions and ACID: Atomicity, Consistency, Isolation, and Durability

Learning outcomes

By the end of this lesson, you can:

  • explain and apply atomicity in a realistic implementation;
  • explain and apply consistency in a realistic implementation;
  • explain and apply isolation in a realistic implementation;
  • explain and apply durability in a realistic implementation;
  • explain and apply transaction scope in a realistic implementation.

Prerequisites and retrieval

This lesson assumes that you have the earlier 01–06 foundation and the preceding lessons in this module. Before you read, retrieve one concrete example from a previous project where this same concern appeared. It might have involved two related writes, a retry, a concurrent request, or a failure halfway through a workflow. The point is not to memorize four terms. The point is to make a defensible decision in a PostgreSQL-backed transactional application, where schema design, correctness, query plans, and concurrency all affect the result.

Terminology

The terms below are related, but they answer different questions about a unit of database work:

  • Atomicity: Does the transaction commit all of its changes, or none of them?
  • Consistency: Do constraints and transactional rules move the database from one valid state to another?
  • Isolation: How do concurrent transactions observe one another, and how can they interfere with one another?
  • Durability: After a commit is acknowledged under the configured durability settings, should the data survive qualifying failures?
  • Transaction scope: How much work belongs inside the transaction? Keep it as short as correctness allows.
  • Savepoints: Where can a transaction roll back part of its work without abandoning the whole transaction? Savepoints support complex workflows, but excessive nesting can hide the real atomic boundary.

The useful distinction is that ACID is not a single switch called “safe.” Each property describes a different guarantee, and each guarantee has an implementation boundary. A database transaction can coordinate database statements; it does not automatically coordinate an email provider, an HTTP service, or a message queue.

Mental model

Treat Transactions and ACID: Atomicity, Consistency, Isolation, and Durability as a design problem with observable inputs, outputs, invariants, and failure modes. A transaction defines a unit of database work, so its guarantees need to line up with the domain invariants you are trying to protect. Saying “use a transaction” is incomplete until you identify which statements are included and what isolation behavior those statements require.

A strong implementation makes its assumptions visible, narrows uncertainty at system boundaries, and leaves enough evidence to justify the design. That evidence might be tests, types, database constraints, metrics, execution plans, or diagrams. The goal is not to make failure impossible. The goal is to make the allowed states and the failure behavior precise enough to test and debug.

A useful interview and production sequence is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement directly to a library call. First state what must remain true. Then choose the mechanism that enforces it. For example, “an order must not reserve more inventory than is available” is a constraint to reason about; “wrap two calls in a transaction” is only one possible implementation detail.

Deep dive

1. Atomicity

The problem atomicity addresses is a partially completed database operation. If an order row is created but its inventory reservation fails, the database can be left with a state that represents neither a successful order nor a clean failure unless both changes are treated as one unit.

Atomicity means that a transaction commits all its changes or none of them. A failure after one statement does not leave that statement committed while the rest disappears. This guarantee applies to the database work in the transaction. It does not automatically make external HTTP, email, or queue side effects atomic with the database. If an email is sent and the database transaction later rolls back, the email cannot be unsent by the database.

Decision rule: Use atomicity deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. In particular, name the exact writes that must succeed together instead of treating every nearby operation as part of one transaction.

2. Consistency

Consistency matters when a write could otherwise violate an invariant. A database should not move from “a unique account ID exists” to “two rows claim the same unique ID,” and an application should not represent a completed transfer with only one side of the balance change.

Database consistency means that constraints and transactional rules move the database between valid states. Constraints such as foreign keys, unique constraints, checks, and non-null requirements can enforce rules at the persistence boundary. The application must still encode business invariants that storage constraints alone cannot express. A rule involving several decisions or an external business policy may need service-layer logic, a transaction, locking, or a combination of these.

Decision rule: Use consistency deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Ask which rule belongs in a database constraint and which rule needs application code, and do not assume that validation performed only in the client protects the stored data.

3. Isolation

Isolation addresses interference between concurrent work. Two requests can read related rows, make decisions from what they read, and then write conflicting results. The fact that each request is inside a transaction does not by itself tell you whether that race is prevented.

Isolation controls how concurrent transactions observe and interfere with each other. Stronger isolation can reduce anomalies, but it can also introduce retries, blocking, or transaction aborts. The correct choice depends on the invariant, the database's behavior, and the application's ability to retry safely. Row locks such as FOR UPDATE, an appropriate isolation level, uniqueness constraints, and conflict handling are tools for specific concurrency problems, not interchangeable decorations.

Decision rule: Use isolation deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Describe the concurrency scenario first: what can another transaction read, update, or insert between this transaction's statements?

4. Durability

Durability matters after the application has told a caller that a commit succeeded. If the process or machine fails immediately afterward, the acknowledged data should not simply vanish under the configured failure model.

Durability means that once a commit is acknowledged under configured durability settings, data should survive qualifying failures. Real durability depends on storage, replication, write-ahead logging, and operational configuration. It is therefore more precise to state which failures are covered than to promise that data survives every possible outage. Backups, restore procedures, replication, and monitoring are part of the operational meaning of durability; a successful local test is not proof of all of them.

Decision rule: Use durability deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Make sure the application's success response matches the durability guarantee it is actually configured to provide.

5. Transaction scope

Transaction scope determines how long locks and transactional resources are held. A transaction that includes a slow provider call or a wait for user input can turn an otherwise correct workflow into a source of blocking, contention, and timeouts.

Keep transactions as short as correctness allows. Do not hold a database transaction open while waiting for user input or a slow remote provider. Put the smallest set of database statements needed to protect the invariant inside the transaction, and coordinate external work with an explicit workflow rather than assuming it shares the database's commit boundary.

Decision rule: Use transaction scope deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. A shorter transaction is not automatically better if it drops a required write outside the atomic boundary, so balance contention against correctness.

6. Savepoints

Savepoints allow partial rollback within a transaction. They can be useful when a complex workflow has an optional step that should be undone without discarding the work that must remain, or when a repository needs a controlled recovery point.

They do not eliminate the need to define the real transaction boundary. Excessive nesting can make it difficult to see which changes are required as one unit and which failures are intentionally recoverable.

Decision rule: Use savepoints deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Document which part may roll back and what state the caller should observe after that rollback.

Worked example

Consider a PostgreSQL-backed transactional application where schema design, correctness, query plans, and concurrency all matter. Start by writing the requirement in one sentence, list the input and output contracts, and identify which concept owns each failure mode. The key move is separation: parsing and boundary validation belong at the boundary; domain rules belong in the domain or service layer; persistence rules belong in the database or repository; presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases much harder to reason about.

For a transfer between two accounts, the balance decrease and the balance increase need to be treated as one database unit. The row lock makes the read-and-decide step explicit for the row being inspected. The application still needs to define what happens when the account is missing, when the amount is invalid or too large, and when another transaction is competing for the same rows.

sql
BEGIN;

SELECT balance
FROM accounts
WHERE id = $1
FOR UPDATE;

UPDATE accounts SET balance = balance - $2 WHERE id = $1;
UPDATE accounts SET balance = balance + $2 WHERE id = $3;

COMMIT;

The SQL demonstrates atomic database work, but it is not a complete transfer contract by itself. The caller must validate the amount, verify that the source account exists and has sufficient balance, decide how to handle a missing destination, and roll back on every failure path. The query plan and indexes also matter: a lock held while an unexpectedly expensive query runs can affect other requests.

Walk the example through at least four cases: the normal path, an empty or missing value, a duplicate/retry/concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. This is the level of explanation expected in a senior code review or technical interview. In particular, distinguish an invalid request from a serialization or lock conflict, because the former usually needs correction while the latter may be safely retried under the right conditions.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

For transactional systems, inspect lock waits, transaction duration, rollback rates, retry rates, and query plans instead of guessing. A design can be logically correct and still fail operationally if transactions remain open too long, an index is missing, or a retry storm amplifies contention.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and the network input is untrusted. Client-side validation may improve the user experience, but it cannot replace server-side authorization, validation, or database guarantees.

Guided lab

Implement inventory reservation plus order creation in one transaction. Inject a failure between statements and prove rollback. Then explain why sending an email inside the transaction does not become atomic with the database. Your test should demonstrate the observable result: after the injected failure, neither the reservation nor the order should remain committed, while an email sent before the failure would still be an external side effect.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

The alternative-design explanation should include the boundary of the database transaction. For example, explain what would change if inventory reservation were coordinated with an outbox or a later asynchronous notification rather than with an email sent while the transaction is open.

Edge cases and failure modes

Use the following cases when testing each concern:

  • Atomicity: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Consistency: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Isolation: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Durability: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Transaction scope: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes.

These categories overlap in real systems, but keeping them separate helps diagnose the cause. A duplicate may be an input problem or a retry problem; a blocked request may be an isolation or transaction-scope problem; a missing acknowledged record may be a durability or operational-configuration problem.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case, inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch. In a transaction problem, also inspect the statements included in the transaction, the isolation level, lock waits, rollback behavior, and the database logs. An unexpected partial result often means that a statement escaped the intended transaction or that an external side effect was mistaken for database state.

Interview questions

  1. What problem does Atomicity solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does Consistency solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does Isolation solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does Durability solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem does Transaction scope solve, and what trade-off or failure mode would make you choose a different approach?

When answering, do not stop at the definition. Name an invariant, identify the competing operation or failure, and explain what the caller should observe. That is what separates a memorized ACID answer from a design explanation.

Checkpoint

Without notes, explain Transactions and ACID: Atomicity, Consistency, Isolation, and Durability to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.

As a self-check, make sure you can say which statements belong inside the transaction, which rules are enforced by constraints, what concurrent work may observe, what failures durability covers, and why the transaction should not remain open during a slow remote operation.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /sql/lesson/192/transactions-and-acid-atomicity-consistency-isolation-and-durability