Apps Artificial Intelligence Cloud Computing CSS Cybersecurity Data Science Database Go JavaScript Linux Python Rust Software Engineering Web Development

Transaction Isolation and Safe Database Retries

4 min read .
Transaction Isolation and Safe Database Retries

Database transactions make groups of reads and writes atomic, but atomicity alone does not answer what concurrent transactions are allowed to observe. That is the job of isolation.

The practical challenge appears when correct transactions conflict. Strong isolation can intentionally abort one transaction rather than allow an invalid interleaving. Applications need to distinguish those retryable concurrency failures from ordinary errors.

Isolation protects invariants, not just statements

Consider two concurrent requests that reserve the last available item.

Each transaction:

  1. reads the available quantity;
  2. verifies that it is greater than zero;
  3. decrements it;
  4. commits.

If both transactions can read the same old value and both commit their updates, the business invariant can be violated even though every individual SQL statement succeeds.

The exact behavior depends on the database engine, isolation level, query shape, and locking strategy. The important lesson is that correctness should be reasoned about at the transaction level.

Know the anomalies you are preventing

Isolation levels are often explained through phenomena such as dirty reads, non-repeatable reads, phantom reads, and serialization anomalies.

Those labels are useful, but application design should start with a concrete invariant:

  • inventory must not become negative;
  • two users must not claim the same unique allocation;
  • an account transfer must preserve total value;
  • a state transition must start from an expected current state.

Then choose an isolation or locking strategy that protects that invariant for the database you actually run.

Vendor documentation matters because implementations of similarly named isolation levels can differ.

Strong isolation can reject valid-looking work

Under serializable isolation, the database attempts to make committed transactions equivalent to some serial execution.

When concurrent transactions cannot safely be ordered, one may be aborted with a serialization failure. That failure is not necessarily a database outage or malformed query. It can be the mechanism that preserves correctness.

The application can often retry the whole transaction.

The word whole is important. Retrying only the failed final statement may reuse reads that are no longer valid.

Put the transaction in a retryable function

A useful structure is:

for attempt in bounded_attempts:
    begin transaction

    read all state needed for the decision
    validate invariants
    write changes

    try commit
    if commit succeeds:
        return success

    rollback
    if error is not a documented retryable conflict:
        return error

    wait with small randomized backoff

return retry_exhausted

The database client or driver should expose enough error information to classify retryable conflicts according to the chosen database.

Do not classify errors by brittle substring matching when the driver provides structured error codes.

Keep external side effects outside the retry loop

A transaction may run more than once. Anything inside the retryable function must therefore tolerate repetition.

This is unsafe:

begin transaction
charge payment provider
update order row
commit

If the database aborts after the payment succeeds, retrying can charge the customer again.

Prefer recording durable intent in the database, committing it, then processing the external side effect through an idempotent workflow such as an outbox-backed worker.

The database transaction can make internal state atomic. It cannot automatically roll back an email, HTTP request, or payment sent to another system.

Make retries bounded and observable

Retries are a correctness technique, not a way to hide unlimited contention.

Use:

  • a small maximum attempt count;
  • jittered backoff where appropriate;
  • a request-level deadline;
  • metrics for retry count and exhausted retries.

If conflicts become frequent, investigate the workload. High retry rates can indicate transactions that are too large, hot rows, missing indexes, an inefficient access pattern, or a data model that concentrates contention.

Keep transactions short

Long-running transactions increase the time during which concurrent work can conflict and may retain locks or old row versions longer than expected.

Do not perform slow network calls inside a transaction. Avoid waiting for user input or large CPU-heavy processing while a transaction is open.

A good pattern is:

  1. perform expensive preparation outside the transaction;
  2. open the transaction;
  3. re-read the authoritative state needed for correctness;
  4. validate and write;
  5. commit;
  6. perform post-commit work.

The re-read is important because state may have changed while preparation happened.

Optimistic checks can complement isolation

Sometimes an application can express its invariant through a conditional update:

UPDATE jobs
SET status = 'running'
WHERE id = :id
  AND status = 'queued';

Then check the affected-row count. If zero rows changed, another actor may have already moved the state.

This pattern is simple for state transitions and compare-and-swap behavior. It does not replace transaction isolation for more complex invariants involving multiple records.

Common pitfalls

Retrying every database error

Syntax errors, constraint violations, authentication failures, and permanent application errors do not become correct through repetition.

Retrying only the last statement

A serialization conflict can invalidate the transaction’s earlier reads and decisions.

Performing irreversible work inside the retried transaction

External side effects can happen multiple times.

Assuming a level name has identical behavior everywhere

Read the transaction and locking documentation for the database engine and version you deploy.

Hiding persistent contention with many retries

More attempts may increase load and make the hot spot worse.

Build retries around invariants

Safe transaction retries require two pieces to work together: the database must signal when a concurrent execution cannot be accepted, and the application must be able to repeat the transaction without duplicating external effects.

Define the invariant first, choose the database behavior that protects it, keep transactions small, retry only documented transient conflicts, and measure how often retries happen. This makes concurrency failures a controlled part of the design instead of a production surprise.

Related Posts

chevron-up