Skip to main content

Concurrency control

The context

In a distributed or concurrent system, multiple transactions might try to update the same resource (e.g., account balance). You need to prevent lost updates and inconsistent reads — without killing performance.

There are two main strategies for that: pessimistic and optimistic control.

Pessimistic control uses explicit locks — safe but blocks others. Optimistic control uses version checks — faster, non-blocking, but needs retries. For example, a ledger service may use pessimistic writes for strict correctness, while some read models and idempotency records use optimistic updates to scale.

Pessimistic concurrency control

"Lock first, then modify."

You explicitly acquire a lock on a resource before writing to it, so nobody else can change it until you're done.

Example (Postgres):

SELECT * FROM accounts WHERE id='A123' FOR UPDATE;
-- now no one else can modify this row until you COMMIT/ROLLBACK
UPDATE accounts SET balance = balance - 100 WHERE id='A123';
COMMIT;

Characteristics:

  • Safe under contention — no conflicting writes.
  • But slower, because concurrent transactions block each other.
  • Great for high-value, low-concurrency operations (like a single ledger write per account).

In Go code, you might wrap it in a transaction and use FOR UPDATE or advisory locks.

Optimistic concurrency control

"Assume no one else will conflict — detect if they did."

You don't lock rows preemptively. Instead, you:

  1. Read a record (including a version or updated_at column).
  2. Make your changes.
  3. Try to write back with a conditional update.
  4. If the row changed in the meantime, your update fails — and you retry.

Example:

UPDATE account_balances
SET balance = balance + 100, version = version + 1
WHERE account_id = 'A123' AND version = 42;

If another process already updated version to 43, this UPDATE affects 0 rows, so you know a conflict happened.

Characteristics:

  • No blocking — great for scalability and distributed writes.
  • You detect conflicts after they occur and retry.
  • Works well when conflicts are rare (hence "optimistic").
  • Common in event-driven systems, microservices, and read models.