Quick answer
A database transaction groups multiple operations into one unit of work. The database either commits the whole transaction or rolls it back so partial changes are not left behind.
Transactions are essential when backend code must keep related data consistent, such as creating an order and reserving inventory, transferring money, or updating a workflow state.
Why transactions matter
Consider an order workflow:
insert order
insert order items
reserve inventory
write payment record
If the system inserts the order but fails before inserting the order items, the database may contain incomplete data. A transaction lets the backend say: either all of these changes succeed, or none of them remain.
That property makes transactions one of the core tools for reliable backend systems.
Commit and rollback
A transaction usually has three phases:
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;
If something fails before COMMIT, the transaction can be rolled back:
ROLLBACK;
After rollback, the database should not keep the partial changes from that transaction.
ACID
ACID describes important transaction properties:
| Property | Meaning |
|---|---|
| Atomicity | All operations in the transaction commit, or none do. |
| Consistency | The transaction moves the database from one valid state to another. |
| Isolation | Concurrent transactions should not corrupt each other. |
| Durability | Once committed, data should survive crashes according to database guarantees. |
ACID is a useful vocabulary, but backend developers still need to understand isolation levels and application-level invariants.
Isolation
Isolation controls what one transaction can see from another transaction.
Weak isolation may allow anomalies such as:
- Dirty reads: reading uncommitted data.
- Non-repeatable reads: reading the same row twice and seeing different values.
- Phantom reads: re-running a query and seeing new matching rows.
- Lost updates: one update overwrites another without detecting it.
Stronger isolation reduces anomalies but can increase locking, waiting, and retry needs.
A common transfer example
A money transfer must subtract from one account and add to another. These two changes belong together.
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 'acct_a'
AND balance >= 100;
UPDATE accounts
SET balance = balance + 100
WHERE id = 'acct_b';
INSERT INTO ledger_entries (from_account, to_account, amount)
VALUES ('acct_a', 'acct_b', 100);
COMMIT;
Real financial systems need more safeguards than this short example, but the core idea is the same: related changes should be committed consistently.
Transactions and application code
Backend code should keep transactions focused.
Do inside a transaction:
- Read rows needed for the decision.
- Write related database changes.
- Insert audit or ledger records.
- Check constraints close to the data.
Avoid doing slow external work inside a transaction:
- Calling third-party APIs.
- Sending email.
- Waiting on message brokers.
- Rendering reports.
- Performing long CPU-heavy work.
Long transactions can hold locks and increase contention. Commit the database state first, then enqueue follow-up work when possible.
Transactions and queues
A common backend problem is updating the database and publishing a message.
commit order -> publish order.created
If the database commit succeeds but message publishing fails, downstream systems may never hear about the order. If the message publishes before the commit and the transaction rolls back, consumers may see an event for data that does not exist.
The transactional outbox pattern is a common solution: write the business change and an outbox row in the same database transaction, then a separate worker publishes the outbox message.
Read Message Queues Explained for the queue side of this design.
Transactions and optimistic locking
Transactions do not automatically solve every concurrency problem at the API level.
If two users edit the same record based on stale reads, one can still overwrite the other unless the backend uses version checks, constraints, or stronger locking.
Optimistic Locking Explained covers the version-column pattern for detecting stale updates.
Common mistakes
The first mistake is assuming every group of repository calls automatically shares one transaction. In many frameworks, you must explicitly define the transaction boundary.
The second mistake is keeping transactions open while calling external services. That increases lock time and makes failure recovery harder.
The third mistake is doing check-then-write logic without a constraint or atomic update. Concurrent requests can both pass the check.
The fourth mistake is ignoring retry behavior. Some transaction failures are safe to retry, while others require user-visible conflict handling.
Practical backend checklist
When designing a transactional workflow:
- Define the exact invariant that must remain true.
- Put related writes in one transaction when they share one database.
- Use constraints for uniqueness and referential integrity.
- Keep the transaction short.
- Avoid external calls inside the transaction.
- Decide how to handle conflicts and deadlocks.
- Test partial failure paths.
Related reading
Read Optimistic Locking Explained for stale update protection and Idempotency in APIs Explained for retry-safe side effects. Read Database Migration Rollback Explained for production schema recovery planning and Database Connection Pooling Explained for how long transactions create pool pressure. Use the SQL Formatter when reviewing multi-statement examples and debugging transaction scripts.