What you'll learn
Transactions make a group of writes succeed or fail as one unit, while concurrency determines what overlapping requests can observe. Define invariants first, use one checked-out client, and choose locking deliberately.
By the end of this lesson, you'll be able to:
- Wrap multi-step invariants in a transaction
- Use one client from BEGIN through COMMIT
- Prevent lost updates
- Retry only safe transient failures
Core mental model
Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.
| Concept | What it means | Decision rule |
|---|---|---|
| Atomicity | All operations commit or all roll back | Use it when partial success violates an invariant |
| Isolation | How concurrent transactions observe one another | Choose the weakest level that protects the use case |
| Optimistic concurrency | A version predicate rejects stale writes | Use when conflicts are uncommon and retries are acceptable |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Define the transaction boundary boundary: inputs, outputs, invariants, ownership, and expected failures.
- Design the data or message contract before choosing implementation details.
- Implement the smallest correct path with dependencies passed explicitly.
- Add validation, failure translation, cleanup, and concurrency behavior.
- Verify the boundary with realistic data and at least one adversarial case.
- Measure or observe the behavior before optimizing or extracting abstractions.
Keep the feedback loop short
Guided code lab
Run work on one client
A transaction belongs to one connection; rollback happens before release.
export async function inTransaction(work) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const value = await work(client);
await client.query('COMMIT');
return value;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}Reject a stale update
The version predicate exposes concurrent modification instead of silently overwriting it.
UPDATE tasks
SET title = $1, version = version + 1
WHERE id = $2 AND owner_id = $3 AND version = $4
RETURNING id, title, version;Production practice
Contract
The service names the invariant; the transaction owns every read and write needed to preserve it.
Verification
Run two concurrent clients against one row and prove final state, conflict response, rollback, and retry limits.
Operations
Keep transactions short, avoid remote calls inside them, observe deadlocks, and cap retries with jitter.
Common failure mode
Independent workshop
Implement an atomic task transfer with a per-owner quota.
Your finished workshop must include:
- Explicit invariant
- Single-client helper
- Locking or version strategy
- Conflict response
- Concurrent test
- Bounded retry
Definition of done
Recap & quick check
Key takeaways
- Transactions protect invariants
- One transaction uses one connection
- Isolation is a product decision
- Conflicts must be visible
- Retries need safe boundaries
Quick check
1. Where must transaction queries run?
2. What detects a stale optimistic write?
3. What should stay outside a transaction?
Next: Repositories, Migrations & Data Access