between lock contention and lost atomicity
Transaction scope determines how much work gets bundled inside a single transaction. A scope that is too broad creates lock contention and blocks concurrent access, while a scope that is too narrow loses the guarantee that related changes apply together or fail together. Sizing it correctly follows clear, practical criteria rather than pure intuition.
Table of Contents
- 1. What transaction scope means and why size matters
- 2. Too-broad transactions: lock contention and scaling problems
- 3. Too-narrow transactions: loss of atomicity
- 4. Practical criteria for sizing
- 5. Setting transaction boundaries in application code
- 6. Long-running transactions and their side effects
- 7. Batch processing: chunking transactions
- 8. Considering isolation level and scope together
- 9. Transaction scope compared
- 10. Summary
- 11. FAQ
1. What transaction scope means and why size matters
Transaction scope describes which operations get bundled between BEGIN and COMMIT, and therefore count together as one atomic unit. Every change within that scope either applies completely or is discarded completely, there is no intermediate state that other transactions can observe. This property makes transactions the central tool for data consistency in relational databases.
The size of the transaction scope, however, is not a purely technical detail but a design decision with direct consequences for performance, scalability, and correctness. A scope that is too broad holds locks longer than necessary and blocks concurrent access to the same resources. A scope that is too narrow separates operations that actually belong together, which can leave inconsistent intermediate states in the database when a partial failure occurs. The right transaction scope lies between these two extremes, and finding it requires a clear understanding of the application's business logic.
In practice, the problem often only shows up under load: a scope that is too broad works flawlessly in development with a single user but leads to waiting times, timeouts, or even deadlocks under production load with a hundred concurrent requests. Correctly sizing transaction scope is therefore a core topic for any system that needs to scale beyond a single user.
2. Too-broad transactions: lock contention and scaling problems
A transaction scope that is too broad usually results from developers, for convenience, bundling all operations of a business action into a single transaction, including reads, computations, and even external calls that do not actually need a transactional guarantee. Every row read or written inside that scope can create a lock that is held until COMMIT. The longer and broader the transaction, the more locks accumulate, and the longer they block concurrent access to the same rows or tables.
The classic example: a reporting job opens a transaction, reads tens of thousands of rows for an analysis, and writes a single result back at the end. If this entire process is wrapped in one transaction, the database, depending on isolation level, holds read locks or at least MVCC-relevant snapshot resources for the whole duration, which slows down or blocks concurrent write operations on the same tables. A transaction scope that is too broad also raises the probability of deadlocks, because more resources are held simultaneously over a longer period.
-- WRONG: transaction scope too broad, includes read-only reporting work
-- and an unrelated audit log write in the same unit of work
BEGIN;
SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days'; -- read-only report data
-- ... application computes aggregates in memory, taking seconds ...
UPDATE order_summary SET total = :computed_total WHERE period = :period;
INSERT INTO audit_log (action, details) VALUES ('summary_computed', :details);
COMMIT;
-- Locks on order_summary and audit_log are held for the entire report duration
-- RIGHT: split read-only reporting from the actual write
SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days'; -- no transaction needed
-- ... application computes aggregates outside any transaction ...
BEGIN;
UPDATE order_summary SET total = :computed_total WHERE period = :period;
INSERT INTO audit_log (action, details) VALUES ('summary_computed', :details);
COMMIT;
-- Only the actual write is wrapped, locks are held for milliseconds
3. Too-narrow transactions: loss of atomicity
The other extreme is just as problematic: a transaction scope that is too narrow splits operations that belong together across multiple separate transactions, which loses the atomicity guarantee. If an order consists of two steps, reducing stock and creating the order record, and both steps run in separate transactions, a failure between the two steps can leave stock reduced with no order actually existing. The database itself cannot detect this inconsistent state, because from its perspective both transactions individually succeeded.
A transaction scope that is too narrow often creeps in gradually, for example when a developer refactors an existing function into smaller pieces and overlooks that the original transaction boundary deliberately spanned several steps. It also arises systematically in microservice architectures, where each service manages its own database transaction: a business action that touches several services can no longer be secured by a single local transaction. Here you either need a deliberate widening of the scope within one service, or a cross-cutting pattern such as sagas with compensating actions.
-- WRONG: transaction scope too narrow, splits related writes into two transactions
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = :product_id;
COMMIT;
-- if the application crashes right here, stock is decremented but no order exists
BEGIN;
INSERT INTO orders (product_id, customer_id, status) VALUES (:product_id, :customer_id, 'placed');
COMMIT;
-- RIGHT: both writes belong to the same business action, same transaction scope
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = :product_id AND stock > 0;
INSERT INTO orders (product_id, customer_id, status) VALUES (:product_id, :customer_id, 'placed');
COMMIT;
-- Either both changes apply, or neither does
4. Practical criteria for sizing
The central guiding question for sizing transaction scope is: which changes must, from a business perspective, apply together or fail together? Everything that satisfies this condition belongs in the same scope. Everything that can remain correct independently, even if it happens at a different time or in a separate transaction, does not belong inside. This rule sounds simple but requires a precise understanding of the business invariants an application must guarantee.
A second practical criterion is the expected duration of the contained operations. Anything that potentially takes a long time or depends on external, uncontrollable systems, network round trips, filesystem access, calls to external APIs, generally belongs outside transaction scope. Pure database operations in the millisecond range, on the other hand, are unproblematic as long as they belong together from a business standpoint. A third criterion concerns the number of affected rows: the more rows a transaction locks, the larger the collision surface with other transactions, which is why bulk operations should often be split into smaller chunks, even if that means giving up atomicity across the whole set.
5. Setting transaction boundaries in application code
In most application architectures, transaction scope is not controlled directly in SQL but through a unit-of-work pattern in application code. A unit of work encapsulates a business action, collects all necessary changes, and commits them together at the end. This pattern makes the transaction boundary explicitly visible in code, instead of implicitly defining it through scattered BEGIN/COMMIT calls, and makes it easier to deliberately widen or narrow the scope when needed.
What matters here is that the transaction boundary is defined at the level of business logic, not at the level of technical layers. A data access layer that automatically wraps every single method in its own transaction almost inevitably produces a scope that is too narrow, because the business logic orchestrating several such method calls can no longer control the boundaries. Transaction scope should therefore be opened and closed by the calling business logic, while individual repository methods simply operate within the transaction that is already open.
-- WRONG: each repository call opens and commits its own transaction,
-- the calling business logic cannot control the overall scope
-- (conceptual, transaction boundaries hidden inside each call)
CALL reduce_stock(product_id); -- own transaction, commits immediately
CALL create_order(customer_id); -- own transaction, commits immediately
-- If create_order fails, reduce_stock has already committed independently
-- RIGHT: business logic owns the transaction boundary explicitly
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = :product_id;
INSERT INTO orders (product_id, customer_id) VALUES (:product_id, :customer_id);
COMMIT;
-- Repository operations run inside the transaction the caller controls
6. Long-running transactions and their side effects
Besides lock contention, long-running transactions have further, often underestimated side effects on the database itself. In PostgreSQL, a long open transaction prevents the autovacuum process from cleaning up outdated row versions, because those versions could theoretically still be visible to the running transaction. This leads to table bloat, growing indexes, and declining performance over time. In MySQL/InnoDB, long transactions grow the history list length in the undo log system, which also ties up storage and hurts the performance of subsequent operations.
A transaction scope that is too broad is therefore not only a problem for immediate concurrency but also for long-term database health. Monitoring tools should actively look for transactions that stay open unusually long, so-called idle-in-transaction states, where the application leaves a transaction open while waiting for something else. Such states are almost always a sign of an incorrectly sized scope and should be specifically hunted down and fixed in the codebase.
7. Batch processing: chunking transactions
For bulk operations, such as updating millions of rows during a migration, transaction scope faces a special tradeoff: a single giant transaction guarantees full atomicity but holds locks for a potentially very long duration and can force a complete rollback of millions of rows if a failure occurs mid-run. The established solution is chunking: the total set is split into smaller batches, each batch runs in its own short transaction.
This approach deliberately gives up atomicity across the whole set in favor of shorter lock durations and the ability to resume from the last successful batch after a failure, instead of starting over completely. For practical sizing, this means: the transaction scope of a batch operation should be oriented around the chunk size, typically a few hundred to a few thousand rows depending on row size and system load, not around the total amount of data to be processed.
-- WRONG: one giant transaction for a million-row migration
BEGIN;
UPDATE products SET category_id = new_category_id
FROM category_mapping WHERE products.old_category_id = category_mapping.old_category_id;
COMMIT;
-- Locks held for minutes, no progress checkpoint if it fails midway
-- RIGHT: chunked transactions with checkpoints, each batch commits independently
-- Pseudocode driving loop around a small, bounded transaction per batch
last_id = 0
batch_size = 1000
loop:
BEGIN;
UPDATE products SET category_id = new_category_id
WHERE id > last_id AND id <= last_id + batch_size
AND old_category_id IS NOT NULL;
COMMIT;
last_id = last_id + batch_size
if no_more_rows: break
8. Considering isolation level and scope together
Transaction scope and isolation level influence each other and should not be considered in isolation. A broad scope under SERIALIZABLE creates a significantly higher risk of serialization failures than the same scope under READ COMMITTED, because the database has to maintain stricter consistency guarantees over a longer period under SERIALIZABLE. Anyone who cannot avoid a broad scope for business reasons should therefore check carefully whether the strictest isolation level is actually required, or whether a weaker level with explicit row locks at the critical points is sufficient.
Conversely, a deliberately narrow transaction scope can justify using a stricter isolation level, because the short runtime already keeps conflict risk low. This interplay makes clear that transaction scope and isolation level should be treated as one coherent design problem, not as two independent configuration parameters.
-- Narrow scope with a stricter isolation level, still low conflict risk
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE inventory SET stock = stock - 1 WHERE product_id = :product_id AND stock > 0;
COMMIT;
-- Short, narrow scope keeps the serialization failure probability low
-- Broad scope under SERIALIZABLE, same isolation level, much higher risk
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM inventory WHERE warehouse_id = :warehouse_id; -- large read set
-- ... application processes thousands of rows over several seconds ...
UPDATE inventory SET stock = stock - 1 WHERE product_id = :product_id;
COMMIT;
-- Wide read footprint plus SERIALIZABLE greatly raises conflict probability
9. Transaction scope compared
The following table shows typical scenarios and the recommended transaction scope for each, derived from the business criteria discussed in the previous sections.
| Scenario | Wrong scope | Recommended scope | Reasoning |
|---|---|---|---|
| Order with stock deduction | Two separate transactions | One combined transaction | Both changes must apply together |
| Reporting analysis plus write | Read and write in one transaction | Read outside, only write transactional | Reads need no lock guarantee |
| Million-row migration | One giant transaction | Chunked transactions with checkpoints | Shorter lock times, resumable |
| External payment call | Called inside the transaction | Called outside, DB write afterward | Network latency must not extend locks |
| Cross-microservice action | Implicit assumption of a local transaction | Saga with compensating actions | No shared local transaction possible |
The table makes it clear: the right transaction scope always follows the business question of what must apply together, not technical convenience or however the code happens to be structured.
Mironsoft
Database architecture, transaction design, and performance tuning
Transactions that secure either too much or too little?
We analyze your transaction boundaries, identify scopes that are too broad with lock contention and scopes too narrow with missing atomicity, and bring your unit-of-work architecture onto a clean, business-driven foundation.
Scope audit
Analyzing existing transaction boundaries for lock contention and atomicity gaps
Unit-of-work design
Defining transaction boundaries cleanly at the business logic level
Batch chunking
Splitting bulk operations into performant, resumable transaction batches
10. Summary
Correctly sizing transaction scope is a business design decision, not a technical afterthought. A scope that is too broad creates lock contention, raises deadlock risk, and burdens the database through delayed cleanup of old row versions. A scope that is too narrow separates operations that must apply together or fail together, opening the door to inconsistent intermediate states that are hard to diagnose.
The guiding question always stays the same: which changes must, from a business perspective, belong together atomically? Everything else, especially reads that need no lock guarantee and external calls with uncontrollable latency, belongs outside the transaction boundary. For bulk operations, chunking replaces full atomicity with resumable, short batches. Anyone who consistently sizes transaction scope according to these criteria reduces both performance problems and consistency errors at the same time.
Sizing transaction scope correctly: the essentials at a glance
Guiding question
Which changes must apply together or fail together from a business perspective? Only those belong in the same scope.
Scope too broad
Lock contention, higher deadlock risk, delayed vacuum/purge. External calls and reads do not belong inside.
Scope too narrow
Loss of the atomicity guarantee, inconsistent intermediate states on partial failure between separate transactions.
Bulk operations
Chunk into small, short batch transactions instead of one giant transaction over the entire dataset.