Database Transactions: Keep Related Writes Consistent
Database transactions group related operations so they can succeed or fail together. They are essential when a partial update would leave data inconsistent, such as reserving inventory while creating an order. A transaction is more than a pair of begin and commit calls: its isolation level, locking behavior, and duration affect correctness under concurrent requests. Start by identifying an invariant that must always hold, then design the transaction around preserving that invariant.
database transactions: a practical workflow
The sequence is Begin → Related writes → Validate → Commit or rollback. The sections below explain what each decision changes, which failure cases deserve attention, and how to verify the result in a small project. Use the example as a starting point, then adapt the interfaces and error policy to your own application.
Define the invariant before writing queries

For a transfer, the total balance across the two accounts should remain unchanged and neither account should violate the allowed balance rule. For inventory, confirmed reservations must not exceed stock. Write these rules in plain language before choosing SQL statements. A transaction provides atomicity for its database operations, but it cannot repair an incorrect calculation. Put integrity constraints in the database where possible, so alternative writers cannot bypass the rule by forgetting an application-level check.
Protect read-modify-write sequences
Two requests can both read the same stock count and decide that the final item is available. Simply wrapping each request in a transaction may not prevent that race at the default isolation level. Use an atomic conditional update, suitable row locking, or a stronger isolation policy based on the database behavior. An update that decrements stock only when stock is positive can make the check and modification one operation. Verify the affected-row count before creating the corresponding reservation.
Keep external side effects outside the atomicity claim

A database rollback cannot unsend an email or reverse an arbitrary HTTP request. If a transaction commits and a process crashes before sending an event, the database and downstream system can disagree. An outbox pattern stores an event alongside the business change in one transaction, then a separate worker delivers it. Delivery can happen more than once, so consumers still need an idempotent handling strategy. State clearly which operations your transaction covers and which need separate coordination.
Handle conflicts and minimize transaction duration
Deadlocks and serialization failures can occur even in a correct design. Retry the whole transaction when the database reports a retryable conflict, using a bounded policy and fresh reads. Do not retry every database exception indiscriminately. Keep transactions short: avoid waiting for user input or slow external services while holding locks. Use a consistent lock order when touching several records. Monitor lock waits and transaction duration so contention appears as a measurable issue rather than intermittent unexplained errors.
Example and implementation notes
BEGIN;
UPDATE inventory
SET available = available - 1
WHERE product_id = 42 AND available > 0;
-- Application must verify exactly one row changed.
-- If zero rows changed, ROLLBACK and report out of stock.
INSERT INTO reservations (product_id, customer_id) VALUES (42, 7);
COMMIT;
This example illustrates the core contract rather than a complete production application. Connect it to your real inputs, authentication rules, and failure-handling paths before using it. Check the expected output first, then change one input at a time to verify the assumptions. Keep those checks alongside the implementation so future changes do not silently weaken the behavior.

Verification checklist
- Inject a failure between related writes and confirm rollback.
- Run concurrent requests against the last available item.
- Verify constraints protect writes from every application path.
- Test bounded retries for database-reported transaction conflicts.
Practice exercise
Create inventory and reservation tables in a disposable database. Seed one item and send two reservation requests concurrently. The accepted result is one successful reservation and one out-of-stock outcome. Then fail the insert after the stock update and confirm the stock remains available after rollback. Include the row-count check in your application code. Finally, explain how a confirmation email would be delivered without claiming that the database can roll it back.

Review the result
After completing the exercise, describe the successful path and one failure path in plain language. Record the input, expected result, actual result, and the evidence you used to compare them. A passing demonstration should remain repeatable after a dependency update or a refactor. If you cannot reproduce the result, reduce the example until the source of uncertainty is visible. Keep the smallest useful regression check rather than depending on memory.
References and related reading
Consult the official reference for database transactions for exact platform behavior. Continue with a practical debugging workflow and unit testing basics to turn these decisions into repeatable checks.
Discussion
Join the conversation