Skip to main content

Command Palette

Search for a command to run...

Concurrency: When Order Is an Assumption

How backend systems break when time has no guarantees

Updated
4 min read
Concurrency: When Order Is an Assumption
S

I'm a passionate backend dev

Up until now, we’ve looked at the backend as a straight line. A request comes in, the server thinks, the database updates, and a response goes out.

But a production backend is never a straight line. It’s thousands of lines happening at the exact same time. This is Concurrency.

Concurrency is the reason your code can work perfectly on your laptop but fail spectacularly the moment two people click a button at the same time.

What Concurrency Actually Means

Concurrency does not mean “many users”.

It means: Multiple things happening without a guaranteed order.

Two requests. Same resource. Overlapping in time.

If you assume an order that does not exist, you get a race condition.

The "Last Seat" Problem

Imagine you’re building a ticket-booking system. There is exactly one seat left on a flight.

  1. User A clicks "Buy." Your code checks the database: Is a seat available? Yes.

  2. User B clicks "Buy" a millisecond later. Your code checks the database: Is a seat available? Yes (because User A’s transaction hasn't finished yet).

  3. User A’s request finishes and marks the seat as sold.

  4. User B’s request finishes and marks the same seat as sold.

You just sold the same seat twice. This is a Race Condition. The "winner" of the race depended on the tiny, unpredictable timing of the network and the CPU.

Why This Problem Doesn’t Show Up Early

Early systems feel safe.

Low traffic. Single instance. Sequential execution.

Requests arrive one after another. State changes look predictable.

Then traffic grows. Instances multiply. Latency fluctuates.

Suddenly, “at the same time” stops being theoretical.

Atomic Operations: The "All or Nothing" Rule

The best way to stop a race is to make sure there is no "middle" to get stuck in. In the backend, we call this Atomicity.

Think of an atomic operation like a bank transfer. You don't want the money to leave Account A and then "wait" in limbo before arriving at Account B. If the power goes out halfway through, the money shouldn't just vanish.

Atomicity ensures that your code acts like a single, instant motion:

  • It either succeeds completely: Both accounts are updated.

  • Or it fails completely: It’s like the button was never pressed.

There is no "in-between." To the rest of the world, the operation is either Done or Not Started.

The "Transaction" Safety Net

Most databases give us a tool to handle this called a Transaction. A transaction wraps your steps (Check seat → Take money → Mark sold) into a single "package."

If any part of that package fails—maybe the payment service is down—the database performs a Rollback. It hits the "undo" button on everything that happened in that package so that your data stays clean and consistent.

Locking: How We Negotiate Space

When two processes want the same data, we have to decide how they "queue up." There are two main ways to think about this:

1. Pessimistic Locking (The "I Don't Trust You" Approach)

The moment User A starts looking at the seat, the database puts a physical lock on that row. User B is forced to wait until User A is completely finished.

  • Pros: Totally safe. No one can overwrite anything.

  • Cons: It’s slow. If everyone is waiting for locks, your high-performance backend turns into a traffic jam.

2. Optimistic Locking (The "Ask Forgiveness" Approach)

You let everyone read the data. But when User A tries to save, the database checks: "Has this changed since you last looked at it?" If it has, User A’s save fails, and they have to try again.

  • Pros: Much faster for high-traffic systems.

  • Cons: You have to write code to handle the "retry" logic when a save fails.

The Distributed Nightmare

Everything we just discussed gets 10x harder when you have multiple servers.

If User A is talking to Server 1 and User B is talking to Server 2, they might be using different caches or different database replicas. Now, "simultaneous" isn't just a timing issue; it's a coordination issue.

This is why tools like Redis Distributed Locks or Zookeeper exist. They act as the "Global Referee" that tells all your servers who is currently allowed to touch a specific piece of data

Summary

  • Concurrency is unavoidable at scale.

  • Race Conditions happen when multiple requests try to change the same state at once.

  • Thinking in Backend means assuming that if something can happen at the same time, it will.

You don't solve concurrency by trying to make things faster; you solve it by creating boundaries (Locks) and indivisible steps (Atomicity).

What Comes Next

In the next one, we’ll talk about idempotency and retries.
Why failures repeat, why systems must tolerate it, and how backend engineers design for “try again” without breaking correctness.

This article is part of the Thinking in Backend series, where we learn backend engineering by understanding how systems behave under pressure, not just how code looks in isolation.