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.