transactions
Transactions
Learn how database transactions group related SQL operations into one reliable unit of work using BEGIN, COMMIT, ROLLBACK, savepoints, conditional updates, locking, error handling, retries, and clearly defined transaction boundaries.
Introduction
Many business operations require several related database changes. Creating an order can involve inserting the order header, creating order lines, reserving inventory, recording an audit entry, and adding an event to an outbox table.
If each statement commits independently, a failure can leave the database with only part of the intended operation.
A transaction groups related SQL statements into one logical unit of work. The application commits the transaction when every required step succeeds or rolls it back when a required step fails.
Core idea: A transaction boundary should represent one complete business operation. The database should not commit a partial result that the business would consider incomplete or invalid.
In your System Design curriculum, Transactions is Topic 5.5 under Relational Data Modeling & SQL. It follows ACID and precedes isolation, B-tree and composite indexes, query plans, and connection pools.
Prerequisites
| # | Prerequisite | Why It Is Needed |
|---|---|---|
| 1 | Entities and relationships | One business transaction commonly modifies several related entities. |
| 2 | Keys and constraints | Transactions must preserve primary, foreign, unique, and check constraints. |
| 3 | ACID | Transactions provide the operational boundary for ACID guarantees. |
| 4 | INSERT, UPDATE, and DELETE | These statements commonly participate in transactional work. |
| 5 | Concurrency fundamentals | Several transactions can read and modify shared data simultaneously. |
| 6 | Error handling | Applications must roll back failed work and map errors safely. |
What Is a Database Transaction?
A database transaction is a sequence of related operations treated as one logical unit of work.
Start transaction
|
v
Read required state
|
v
Validate business conditions
|
v
Apply related changes
|
v
Verify affected rows and constraints
|
+-- Success --> COMMIT
|
+-- Failure --> ROLLBACK
A transaction can contain reads and writes. Its exact visibility, locking, and conflict behaviour depends on the database engine and selected isolation level.
Transaction Boundary
A transaction boundary identifies which operations must succeed or fail together.
Order-creation Boundary
One logical operation:
Create an order
Required database changes:
- Insert order
- Insert order lines
- Reserve inventory
- Store calculated total
- Insert audit record
- Insert outbox event
Required outcome:
All changes commit,
or all changes roll back.
Transaction 1:
Insert order and commit.
Transaction 2:
Insert order lines and commit.
Transaction 3:
Reserve inventory and commit.
Failure:
Order and lines exist,
but inventory was not reserved.
One transaction:
Insert order
Insert order lines
Reserve inventory
Insert audit record
Insert outbox event
Commit only after all required work succeeds.
Starting a Transaction
Transaction-start syntax varies between database systems. Common forms
include BEGIN, BEGIN TRANSACTION, and
START TRANSACTION.
BEGIN;
INSERT INTO departments
(
department_id,
department_name
)
VALUES
(
10,
'Engineering'
);
COMMIT;
Use the syntax and transaction semantics documented for the selected database and application driver.
COMMIT
COMMIT successfully ends the current transaction and makes its changes final according to the database's durability and visibility guarantees.
BEGIN;
UPDATE orders
SET order_status = 'confirmed'
WHERE order_id = :order_id
AND order_status = 'pending';
COMMIT;
The application should not commit merely because SQL execution produced no exception. The application must verify affected-row counts and required business conditions first.
Commit rule: Commit only after every required operation, affected-row check, constraint, and business invariant has succeeded.
ROLLBACK
ROLLBACK ends the transaction and discards its uncommitted changes.
BEGIN;
UPDATE inventory
SET available_quantity =
available_quantity - :quantity
WHERE product_id = :product_id
AND available_quantity >= :quantity;
-- Application discovers that no row was updated.
ROLLBACK;
A rollback cannot normally undo changes that were already committed. Recovery after an incorrect commit requires another corrective transaction, a compensating operation, or restoration from an appropriate recovery source.
COMMIT vs ROLLBACK
| Command | Purpose | Result |
|---|---|---|
| COMMIT | Accept the completed transaction | Changes become committed |
| ROLLBACK | Abandon the current transaction | Uncommitted changes are discarded |
| ROLLBACK TO SAVEPOINT | Undo work after a named point | Earlier work remains part of the open transaction |
Autocommit
Many tools and database drivers use autocommit by default. In autocommit mode, each statement can execute as its own transaction unless an explicit transaction is started.
Autocommit enabled:
UPDATE statement
|
v
Statement succeeds
|
v
Statement commits automatically
Autocommit is convenient for independent statements but unsafe when several changes need one shared atomic boundary.
Autocommit Risk
UPDATE accounts
SET balance = balance - 100.00
WHERE account_id = 101;
UPDATE accounts
SET balance = balance + 100.00
WHERE account_id = 202;
If each statement commits separately and the second statement fails, the transfer remains incomplete.
Configuration rule: Never assume the autocommit setting. Verify the database, driver, framework, connection-pool, and client-tool behaviour being used.
Funds-transfer Transaction
BEGIN;
UPDATE accounts
SET balance = balance - :amount
WHERE account_id = :source_account_id
AND currency = :currency
AND balance >= :amount;
-- Verify exactly one source row was updated.
UPDATE accounts
SET balance = balance + :amount
WHERE account_id = :target_account_id
AND currency = :currency;
-- Verify exactly one target row was updated.
INSERT INTO account_transfers
(
transfer_id,
source_account_id,
target_account_id,
amount,
currency,
transfer_status,
created_at
)
VALUES
(
:transfer_id,
:source_account_id,
:target_account_id,
:amount,
:currency,
'completed',
CURRENT_TIMESTAMP
);
COMMIT;
If either account update fails, the transaction should be rolled back. The transfer operation also needs authentication, authorization, auditability, and idempotency.
Transaction States
| State | Meaning |
|---|---|
| Active | The transaction is executing operations |
| Preparing to commit | Required operations finished and final validation is occurring |
| Committed | The transaction completed successfully |
| Failed | An error prevents successful completion |
| Rolled back | Uncommitted transaction changes were discarded |
Savepoints
A savepoint creates a named position inside a transaction. Where supported, the application can roll back work performed after the savepoint without abandoning all earlier work.
BEGIN;
INSERT INTO orders
(
order_id,
customer_id,
order_status
)
VALUES
(
:order_id,
:customer_id,
'pending'
);
SAVEPOINT before_optional_note;
INSERT INTO order_notes
(
order_id,
note_text
)
VALUES
(
:order_id,
:note_text
);
ROLLBACK TO SAVEPOINT before_optional_note;
COMMIT;
The order remains part of the open transaction, while the note insertion is undone.
Savepoint rule: A savepoint does not create an independent committed transaction. The outer transaction must still end with COMMIT or ROLLBACK.
Releasing a Savepoint
Some databases support releasing a savepoint when it is no longer needed.
BEGIN;
UPDATE orders
SET order_status = 'confirmed'
WHERE order_id = :order_id;
SAVEPOINT after_confirmation;
INSERT INTO order_audit
(
order_id,
action_name
)
VALUES
(
:order_id,
'confirmed'
);
RELEASE SAVEPOINT after_confirmation;
COMMIT;
Savepoint commands and behaviour vary between database platforms.
Nested Transactions
Not every database supports true independently committed nested transactions. Frameworks can simulate nesting through reference counting or savepoints.
Outer service starts transaction
|
v
Inner service requests transaction
|
+--> Framework reuses outer transaction
|
+--> Framework creates savepoint
|
+--> Platform provides another documented behaviour
An inner method returning successfully does not guarantee that its work will remain committed if the outer transaction later rolls back.
Read-only Transactions
A transaction can contain only reads. An explicit read transaction can be useful when several queries need a consistent view according to the database's isolation model.
BEGIN;
SELECT
order_id,
order_status,
total_amount
FROM orders
WHERE customer_id = :customer_id;
SELECT
payment_id,
payment_status,
payment_amount
FROM payments
WHERE customer_id = :customer_id;
COMMIT;
Whether both queries observe one consistent snapshot depends on the database engine, transaction mode, and isolation level.
Conditional Updates
Conditional updates combine checking and changing data in one database statement.
UPDATE orders
SET order_status = 'cancelled'
WHERE order_id = :order_id
AND order_status IN
(
'pending',
'confirmed'
);
The application must verify the affected-row count. No updated row can mean that the order does not exist, is not authorized, or is no longer in a cancellable state.
Inventory Reservation
BEGIN;
UPDATE inventory
SET
available_quantity =
available_quantity - :quantity,
reserved_quantity =
reserved_quantity + :quantity
WHERE product_id = :product_id
AND available_quantity >= :quantity;
-- Verify exactly one row was updated.
INSERT INTO inventory_reservations
(
reservation_id,
product_id,
order_id,
reserved_quantity,
reservation_status
)
VALUES
(
:reservation_id,
:product_id,
:order_id,
:quantity,
'active'
);
COMMIT;
Combining the availability condition with the update prevents a separate read-before-write race from overselling the available quantity.
Explicit Row Locking
Some workflows read rows with update-intent locks before changing them.
BEGIN;
SELECT
available_quantity
FROM inventory
WHERE product_id = :product_id
FOR UPDATE;
UPDATE inventory
SET available_quantity =
available_quantity - :quantity
WHERE product_id = :product_id;
COMMIT;
Locking syntax and behaviour vary between database engines. Locks should be held only for the shortest practical transaction duration.
Optimistic Concurrency
Optimistic concurrency assumes conflicts are uncommon and verifies that the resource has not changed before committing an update.
UPDATE customers
SET
display_name = :display_name,
row_version = row_version + 1
WHERE customer_id = :customer_id
AND row_version = :expected_version;
If no row is updated, another transaction may have changed the customer after the application read it.
Read customer:
version = 7
Submit update:
WHERE version = 7
Database currently contains:
version = 8
Result:
No row updated.
Return or resolve concurrency conflict.
Pessimistic vs Optimistic Concurrency
| Approach | Method | Consideration |
|---|---|---|
| Pessimistic | Locks contested data before modification | Can increase blocking and deadlock risk |
| Optimistic | Detects whether data changed at update time | Conflicting operations must fail or retry |
Keep Transactions Short
Long transactions can retain connections, locks, row versions, log space, and other database resources.
Begin transaction
Read order
Wait for user input
Call payment provider
Generate PDF
Send email
Update database
Commit
Perform safe preparation outside transaction
Begin transaction
Reread required current state
Validate conditions
Apply database changes
Insert outbox event
Commit
Perform asynchronous follow-up
Any condition checked before the transaction can become stale. Reread or revalidate critical state inside the transaction before committing.
External Calls inside Transactions
A database transaction should not normally remain open while waiting for a remote service.
Database transaction opened
|
v
Remote payment call begins
|
v
Network delay or timeout
|
v
Database connection and locks remain held
|
v
Contention and timeout risk increase
A remote system also cannot normally be rolled back merely because the local database transaction rolls back.
Transactional Outbox
The transactional outbox pattern stores a business change and a message record in the same local database transaction.
BEGIN;
INSERT INTO orders
(
order_id,
customer_id,
order_status
)
VALUES
(
:order_id,
:customer_id,
'confirmed'
);
INSERT INTO outbox_messages
(
message_id,
event_type,
aggregate_id,
payload,
created_at
)
VALUES
(
:message_id,
'OrderConfirmed',
:order_id,
:payload,
CURRENT_TIMESTAMP
);
COMMIT;
A separate publisher sends committed outbox messages. The publisher and message consumers must handle repeated delivery safely.
Transactions and Idempotency
A transaction prevents partial changes during one execution. An idempotency key prevents a retry from creating a second successful execution of the same logical operation.
BEGIN;
INSERT INTO idempotency_records
(
tenant_id,
idempotency_key,
operation_name,
processing_status
)
VALUES
(
:tenant_id,
:idempotency_key,
'CreateOrder',
'IN_PROGRESS'
);
INSERT INTO orders
(
order_id,
tenant_id,
customer_id,
order_status
)
VALUES
(
:order_id,
:tenant_id,
:customer_id,
'pending'
);
UPDATE idempotency_records
SET
processing_status = 'COMPLETED',
resource_id = :order_id
WHERE tenant_id = :tenant_id
AND idempotency_key = :idempotency_key;
COMMIT;
A unique constraint on the scoped idempotency key must provide atomic ownership when concurrent retries arrive.
Deadlocks
A deadlock occurs when transactions form a circular waiting dependency.
Transaction A:
Locks Account 1.
Waits for Account 2.
Transaction B:
Locks Account 2.
Waits for Account 1.
Result:
Circular wait.
The database can select one transaction as the deadlock victim and roll it back.
Deadlock-reduction Practices
- Access shared rows and tables in a consistent order
- Keep transactions short
- Use selective predicates and suitable indexes
- Avoid user input and remote calls inside transactions
- Update only required rows
- Implement bounded retries for safe operations
Transaction Retries
Deadlocks, serialization conflicts, and selected transient failures can require retrying the complete transaction.
Transaction fails
|
v
Roll back completely
|
v
Classify the failure
|
+--> Permanent:
| return controlled failure
|
+--> Transient:
verify retry safety
apply backoff and jitter
start a new transaction
reread current data
retry within deadline
Do not retry from the middle of a failed transaction. Start a new transaction and repeat the complete safe unit of work.
Retry rule: A retried transaction must reread current state because the data and applicable business conditions can change between attempts.
PHP Transaction Example
<?php
declare(strict_types=1);
function enrollLearner(
PDO $pdo,
int $enrollmentId,
int $learnerId,
int $courseId
): void {
$pdo->beginTransaction();
try {
$reserveSeat =
$pdo->prepare(
'
UPDATE courses
SET available_seats =
available_seats - 1
WHERE course_id = :course_id
AND course_status = :status
AND available_seats > 0
'
);
$reserveSeat->execute([
'course_id' => $courseId,
'status' => 'published'
]);
if ($reserveSeat->rowCount() !== 1) {
throw new RuntimeException(
'The course is unavailable or full.'
);
}
$insertEnrollment =
$pdo->prepare(
'
INSERT INTO enrollments
(
enrollment_id,
learner_id,
course_id,
enrollment_status,
completion_percentage,
enrolled_at
)
VALUES
(
:enrollment_id,
:learner_id,
:course_id,
:status,
0,
CURRENT_TIMESTAMP
)
'
);
$insertEnrollment->execute([
'enrollment_id' => $enrollmentId,
'learner_id' => $learnerId,
'course_id' => $courseId,
'status' => 'active'
]);
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
}
A production implementation also needs authentication, authorization, idempotency, duplicate-enrollment handling, safe error mapping, an appropriate isolation level, and a bounded retry policy.
Transaction Helper Pattern
<?php
declare(strict_types=1);
function executeInTransaction(
PDO $pdo,
callable $operation
): mixed {
$pdo->beginTransaction();
try {
$result =
$operation(
$pdo
);
$pdo->commit();
return $result;
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
}
The helper centralizes basic commit and rollback handling. It does not automatically provide retry safety, idempotency, isolation selection, or correct business boundaries.
Transactions in Service Layers
Controller
Validates request format
|
v
Application service
Defines business transaction boundary
|
v
Repositories
Execute SQL using the same transaction
|
v
Database
Commits or rolls back the unit of work
Repository methods participating in one business operation should use the same database connection and transaction context. Independently opening new connections can unintentionally place statements in separate transactions.
Connections and Transaction Scope
A database transaction belongs to a database session or connection. Every statement intended to participate in the transaction must execute through the correct connection.
Connection A:
Begin transaction and insert order.
Connection B:
Insert order lines and commit.
Connection A:
Roll back.
Result:
Order lines can remain without
the intended order transaction.
Connection A:
Begin transaction.
Insert order.
Insert order lines.
Reserve inventory.
Commit.
Testing Transactions
Transaction testing should include failure injection and concurrency rather than only successful execution.
- Run the successful business operation.
- Force a failure after each required statement.
- Verify that no partial committed state remains.
- Trigger primary, foreign, unique, and check violations.
- Verify affected-row handling.
- Run competing operations concurrently.
- Trigger a deadlock or concurrency conflict in an approved environment.
- Verify rollback before retry.
- Verify that retries reread current state.
- Verify idempotency after a lost response.
- Test outbox publication recovery.
- Measure transaction duration and lock waits.
Transaction Observability
Useful transaction metrics include:
- Transaction count
- Commit count
- Rollback count
- Transaction duration
- Deadlock count
- Lock-wait duration
- Concurrency-conflict count
- Retry count
- Retry success rate
- Long-running transaction count
- Connection time spent in a transaction
- Constraint violations by operation
Logs should identify the business operation and trace ID without exposing credentials, sensitive values, or raw database details.
Common Transaction Mistakes
Using One Transaction per Statement
Related statements can commit separately and leave a partial business operation.
Assuming Autocommit Is Disabled
Tools, drivers, and frameworks can commit statements automatically unless an explicit transaction is started.
Committing without Checking Affected Rows
A conditional update can affect no rows even though no SQL exception was raised.
Leaving Transactions Open
Unfinished transactions can retain locks, versions, connections, and log resources.
Waiting for User Input inside a Transaction
Interactive waiting creates unpredictable transaction duration and contention.
Calling Remote Services inside a Transaction
Network delays keep database resources occupied and the remote effect cannot normally be rolled back with the local transaction.
Using Different Connections
Statements on another connection do not automatically join the current transaction.
Continuing after a Transaction Failure
Roll back failed work and start a new transaction according to database and driver requirements.
Retrying from the Middle
A retry should restart the complete safe unit of work and reread current state.
Assuming a Savepoint Is an Independent Commit
The outer transaction can still roll back all work performed before and after the savepoint.
Assuming Transactions Prevent Duplicate Requests
A complete transaction can execute successfully more than once. Retryable operations need idempotency.
Assuming a Local Transaction Covers External Systems
Payment providers, brokers, and independent services require durable workflow, retry, and reconciliation patterns.
Recommended Test Cases
| Test | Expected Evidence |
|---|---|
| Successful transaction | All required changes commit |
| Failure after first statement | No partial committed state remains |
| Failure before commit | All uncommitted changes are rolled back |
| Constraint violation | The invalid operation does not commit |
| Conditional update affects no rows | The operation rolls back or returns the correct conflict |
| Rollback to savepoint | Only work after the savepoint is undone |
| Outer rollback after savepoint | All transaction work is discarded |
| Concurrent final-seat booking | Only one booking succeeds |
| Deadlock victim | The selected transaction is rolled back completely |
| Safe transaction retry | The retry rereads current data and respects idempotency |
| Lost response after commit | The committed outcome can be recovered without duplication |
| Outbox publisher failure | The committed message remains available for later delivery |
Transaction Best Practices
Recommended Practices
- Define transaction boundaries around complete business operations.
- Verify the database and driver autocommit settings.
- Commit only after every required operation succeeds.
- Roll back after every unrecoverable transaction failure.
- Verify affected-row counts for conditional updates.
- Keep transactions short and focused.
- Use the same connection for every statement in one transaction.
- Reread critical current state inside the transaction.
- Use constraints as the final integrity boundary.
- Use atomic conditional updates for contested resources.
- Choose optimistic or pessimistic concurrency deliberately.
- Access shared resources in a consistent order.
- Do not wait for user input inside a transaction.
- Avoid slow remote calls while holding a database transaction open.
- Use savepoints only for valid partial-rollback requirements.
- Start a new transaction after rollback.
- Retry only documented transient transaction failures.
- Combine transactions with idempotency for repeatable API operations.
- Use a transactional outbox for reliable event publication.
- Monitor transaction duration, rollbacks, deadlocks, and lock waits.
Practice Exercise
Implement a transaction that records a quiz submission in your online learning platform.
Requirements
- Verify that the learner is enrolled in the course.
- Verify that the quiz is active.
- Prevent duplicate processing of one submission request.
- Create the quiz-attempt record.
- Insert the submitted answers.
- Calculate and store the score.
- Update the learner's course progress when applicable.
- Create an audit record.
- Create an outbox event.
- Commit all database changes together.
- Roll back after any required failure.
- Use one connection for all repository operations.
- Test two concurrent submissions.
- Test a lost response after commit.
- Measure transaction duration and rollback count.
Suggested Transaction
BEGIN;
INSERT INTO quiz_attempts
(
attempt_id,
learner_id,
quiz_id,
attempt_status,
started_at
)
VALUES
(
:attempt_id,
:learner_id,
:quiz_id,
'submitted',
:started_at
);
INSERT INTO quiz_attempt_answers
(
attempt_id,
question_id,
selected_answer_id,
awarded_score
)
VALUES
(
:attempt_id,
:question_id,
:selected_answer_id,
:awarded_score
);
UPDATE quiz_attempts
SET
total_score = :total_score,
submitted_at = CURRENT_TIMESTAMP
WHERE attempt_id = :attempt_id;
INSERT INTO learning_audit
(
audit_id,
learner_id,
action_name,
entity_id,
occurred_at
)
VALUES
(
:audit_id,
:learner_id,
'quiz_submitted',
:attempt_id,
CURRENT_TIMESTAMP
);
INSERT INTO outbox_messages
(
message_id,
event_type,
aggregate_id,
payload,
created_at
)
VALUES
(
:message_id,
'QuizSubmitted',
:attempt_id,
:payload,
CURRENT_TIMESTAMP
);
COMMIT;
Transaction Review Template
| Review Area | Decision |
|---|---|
| Business boundary | One complete quiz submission |
| Required writes | Attempt, answers, score, audit, and outbox event |
| Concurrency control | Prevent duplicate or conflicting submission |
| Idempotency | One key for one logical submission request |
| Failure behaviour | Roll back the complete transaction |
| External follow-up | Publish the committed outbox event asynchronously |
| Observability | Duration, commit, rollback, retry, and conflict metrics |
Frequently Asked Questions
What is a database transaction?
A database transaction is a sequence of related operations treated as one logical unit of work.
What does COMMIT do?
COMMIT successfully ends the current transaction and makes its changes committed.
What does ROLLBACK do?
ROLLBACK ends the current transaction and discards its uncommitted changes.
What is a savepoint?
A savepoint is a named position inside a transaction to which later work can be rolled back where supported.
Does a savepoint commit earlier work?
No. Earlier work remains part of the open outer transaction and can still be rolled back.
What is autocommit?
Autocommit causes statements to commit automatically when they are not executing inside an explicit transaction.
Why should transactions be short?
Short transactions reduce lock duration, connection use, deadlock risk, row-version retention, and transaction-log pressure.
Should remote API calls occur inside a database transaction?
Normally no. Remote calls can be slow or uncertain and cannot generally be rolled back by the local database.
Can a transaction be retried?
Selected transient failures can be retried when the complete operation is safe, bounded, and protected by an appropriate idempotency contract.
Does a transaction prevent duplicate requests?
No. A transaction protects one execution. Idempotency prevents repeated executions from creating duplicate effects.
Can statements on separate connections share one transaction?
Not automatically. Ordinary local transactions belong to a particular database connection or session.
What comes after transactions?
The next topic is isolation, followed by B-tree and composite indexes.
Key Takeaway
A database transaction groups related reads and writes into one logical unit of work. Start an explicit transaction when several operations must succeed together, validate every required condition, verify affected-row counts, commit only after complete success, and roll back after failure. Keep transactions short, use one connection, control concurrent updates, restart the complete safe operation after transient conflicts, combine transactions with idempotency, and use durable outbox or workflow patterns when work crosses database or service boundaries.