Sitemap
Abhishek Frances

Abhishek Frances

Senior Software Engineer building scalable web apps, microservices, and distributed systems. Writing about React, Go, Kafka, gRPC, and system design.

Concurrency Control in Modern Systems: From Race Conditions to Distributed Locking

Understanding how production systems prevent duplicate operations, maintain consistency, and handle concurrent requests at scale.

21 min readJun 7, 2026

--

Press enter or click to view image in full size

What Happens When Two Users Try to Book the Last Seat at the Same Time?

Every engineer has built a booking system at some point — whether it’s for movie tickets, flights, hotel rooms, event registrations, or e-commerce inventory.

At first glance, the problem seems simple:

  1. Check if the seat is available.
  2. If available, mark it as booked.
  3. Return success to the user.

But what happens when two users try to book the same seat at exactly the same time?

This seemingly simple scenario introduces one of the most important concepts in distributed systems and system design: Concurrency Control.

In this article, we’ll explore why this problem occurs, the risks it creates, and the different strategies production systems use to solve it.

The Problem

Imagine there is only one seat left.

Two users, User A and User B, click the “Book Now” button at exactly the same moment.

The booking flow looks like this:

User A -----> Application Server
|
v
Check Seat
|
v
Available = Yes
User B -----> Application Server
|
v
Check Seat
|
v
Available = Yes

Both users see the seat as available, both proceed with the booking.

The result?

Seat Count = 1
Successful Bookings = 2

This is known as a Race Condition.

Why Does This Problem Exist?

Modern systems are inherently concurrent, thousands of users interact with the same resources simultaneously.

The database, application servers, caches, and message queues process requests independently.

Because requests arrive at nearly the same time, there is no guarantee about which request will execute first.

This uncertainty creates race conditions.

What Is a Race Condition?

A race condition occurs when multiple operations attempt to access and modify the same resource simultaneously.

The final outcome depends on the timing of those operations.

In our booking example:

  • User A reads the seat status.
  • User B reads the seat status.
  • User A updates the seat.
  • User B updates the seat.

Both requests succeed because neither was aware of the other’s update.

Timeline Visualization

Time 0 
Seat Available

Time 1
User A reads seat

Time 1
User B reads seat

Time 2
User A books seat

Time 3
User B books seat

Result:
Double Booking

Understanding Transactions

Before we discuss race conditions, locking strategies, and concurrency problems, it’s important to understand one of the most fundamental concepts in database systems: the transaction.

A transaction is a group of operations that are executed as a single unit of work.

The database guarantees that either all operations succeed together or none of them are applied.

Think of a transaction as a contract between your application and the database:

Complete everything successfully, or undo everything.

Why Do We Need Transactions?

Let’s consider a simple banking example.

Suppose Francis wants to transfer ₹1,000 from Account A to Account B.

The operation involves two separate database updates:

UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 'A';
UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 'B';

Now imagine the system crashes after the first update.

The result would be:

Account A = ₹9,000
Account B = ₹5,000

The money has disappeared.

Account A was debited, but Account B never received the funds.

This creates inconsistent data.

Solving the Problem with Transactions

Instead of executing the updates independently, we wrap them inside a transaction.

BEGIN TRANSACTION;

UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 'A';

UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 'B';

COMMIT;

If both operations succeed:

Transaction Committed

The changes become permanent.

If anything fails:

ROLLBACK;

The database returns to its original state.

Account A = ₹10,000
Account B = ₹5,000

No money is lost.

ACID Properties

Transactions are built on four fundamental guarantees known as ACID.

Atomicity

A transaction succeeds completely or fails completely.

All Operations Succeed
OR
All Operations Roll Back

There is no partial success.

Consistency

A transaction moves the database from one valid state to another valid state.

Business rules remain intact.

Example:

Total Money Before Transfer
=
Total Money After Transfer

Isolation

Concurrent transactions should not interfere with one another.

Each transaction behaves as if it were running independently.

We’ll explore this concept in much more detail later when discussing concurrency problems.

Durability

Once a transaction is committed, the data remains stored permanently.

Even if the database server crashes immediately afterward, the committed changes are preserved.

Transaction Lifecycle

A transaction typically follows this flow:

BEGIN TRANSACTION
↓
Read Data
↓
Modify Data
↓
Validate Rules
↓
COMMIT

If an error occurs:

BEGIN TRANSACTION
↓
Read Data
↓
Modify Data
↓
ERROR
↓
ROLLBACK

Revisiting Our Seat Booking Example

Suppose only one seat is available.

A booking transaction might look like this:

BEGIN TRANSACTION;

SELECT seat_status
FROM seats
WHERE seat_id = 101;

UPDATE seats
SET seat_status = 'BOOKED'
WHERE seat_id = 101;

COMMIT;

At first glance, this appears to solve the problem.

The booking operation either succeeds or fails.

However, a new challenge emerges when multiple transactions execute simultaneously.

What happens when two users attempt to book the same seat at the same time?

This is where concurrency control becomes important and where transactions alone may no longer be sufficient.

Why Transactions Alone Are Not Enough

When engineers first learn about transactions, it’s common to assume that wrapping operations inside a transaction automatically solves concurrency problems.

BEGIN TRANSACTION;

UPDATE seats
SET status = 'BOOKED'
WHERE id = 101;

COMMIT;

At first glance, this seems sufficient.

However, transactions are not a silver bullet.

The level of protection provided by a transaction depends on the database’s Isolation Level, which determines how concurrent transactions interact with one another.

To understand why concurrency remains challenging, let’s explore three common anomalies that can occur when multiple transactions access the same data simultaneously.

Dirty Reads

A dirty read occurs when one transaction reads data that has been modified by another transaction but has not yet been committed.

Imagine a banking system where an account balance is ₹10,000.

Transaction A begins processing a withdrawal and temporarily updates the balance to ₹5,000.

Before Transaction A commits, Transaction B reads the updated balance and sees ₹5,000.

Now suppose Transaction A fails and rolls back.

The balance returns to ₹10,000, but Transaction B has already made decisions based on information that never officially existed.

This is known as a dirty read.

In high-stakes systems such as banking, payments, or financial reporting, dirty reads can lead to incorrect calculations and inconsistent business decisions.

Non-Repeatable Reads

A non-repeatable read occurs when the same transaction reads the same row twice and receives different results.

Consider a flight reservation system.

Transaction A checks the number of available seats and finds five seats remaining.

While Transaction A is still running, Transaction B successfully books a seat and commits the change.

When Transaction A checks again, it now sees only four seats available.

The same query, executed within the same transaction, returns different results because another transaction modified the underlying data.

For reporting systems, inventory management, and financial calculations, this behavior can produce unexpected and unreliable outcomes.

Phantom Reads

A phantom read occurs when a transaction executes the same query twice and discovers new rows that were inserted by another committed transaction.

Imagine a ticketing platform generating a report of all bookings for a specific event.

The first query returns 100 bookings.

While the report is still running, another transaction inserts a new booking and commits.

When the report executes the same query again, it now returns 101 bookings.

No existing rows changed, but a completely new row appeared.

From the perspective of the original transaction, the new record appears like a phantom.

Phantom reads commonly occur in booking systems, analytics platforms, inventory management solutions, and reporting workflows.

Isolation Levels: The Trade-Off Between Consistency and Performance

To manage these concurrency issues, databases provide multiple isolation levels. Each level offers a different balance between performance and consistency.

At one end of the spectrum, lower isolation levels provide better performance but allow more concurrency-related anomalies.

At the other end, the Serializable isolation level offers the strongest consistency guarantees by making transactions behave as though they execute one after another. The trade-off is reduced throughput and increased locking overhead.

Bringing It Back to the Seat Booking Problem

Let’s revisit our original scenario.

Two users attempt to reserve the last available seat at exactly the same time.

Even if both booking requests run inside transactions, the outcome depends on how those transactions interact with one another and which isolation level is being used.

This is why large-scale systems rarely rely on transactions alone.

Production systems typically combine multiple techniques such as:

  • Transactions
  • Row-Level Locking
  • Optimistic Locking
  • Distributed Locking
  • Idempotency Keys
  • Event Queues

Together, these mechanisms ensure that systems remain consistent, reliable, and scalable even when thousands of users are competing for the same resource simultaneously.

Why Is This Dangerous?

The impact goes far beyond seat booking.

Race conditions can cause:

  • Double bookings
  • Duplicate payments
  • Inventory overselling
  • Incorrect account balances
  • Duplicate coupon redemptions
  • Data corruption

In production systems, these issues can directly affect revenue and customer trust.

Solution 1: Pessimistic Locking

The simplest approach is to lock the resource before updating it.

The database prevents other transactions from modifying the same row until the lock is released.

Example:

SELECT *
FROM seats
WHERE id = 101
FOR UPDATE;

Flow:

User A acquires lock
|
v
Updates seat
|
v
Commits transaction
User B waits

Advantages

  • Strong consistency
  • Easy to reason about
  • Prevents concurrent updates

Disadvantages

  • Reduced throughput
  • Increased waiting time
  • Can create lock contention under heavy traffic

Solution 2: Optimistic Locking

Instead of locking resources, optimistic locking assumes conflicts are rare.

Each record contains a version number.

Example:

Seat:
id = 101
status = Available
version = 5

Update query:

UPDATE seats
SET version = version + 1
WHERE id = 101
AND version = 5;

If another transaction updates the row first, the version changes and the update fails.

Advantages

  • Better performance
  • Higher scalability
  • Minimal blocking

Disadvantages

  • Requires retry mechanisms
  • Not ideal for extremely high contention

Solution 3: Distributed Locking

In modern microservice architectures, applications run across multiple servers.

A database lock alone may not be sufficient.

Systems often use distributed lock managers such as Redis-based locks.

Flow:

User A
|
Acquire Distributed Lock
|
Process Request
|
Release Lock
User B waits

Advantages

  • Works across multiple application instances
  • Useful in distributed systems

Disadvantages

  • Additional infrastructure
  • Increased complexity
  • Requires careful timeout handling

Solution 4: Idempotency

Users frequently:

  • Refresh pages
  • Retry requests
  • Double-click buttons

Without protection, the same request may be processed multiple times.

Idempotency solves this problem.

Example:

Request:
Idempotency-Key: abc123

If the same key arrives again:

Return Existing Result
Do Not Process Again

Advantages

  • Prevents duplicate operations
  • Essential for payment systems
  • Improves user experience

Disadvantages

  • Does not solve resource contention by itself

Queues and Event-Driven Systems

So far, we’ve discussed locking strategies that prevent multiple users from modifying the same resource simultaneously.

While locking is effective, it isn’t always the best solution for high-traffic systems.

Imagine a flash sale where 100,000 users attempt to purchase the last few items within seconds.

Even the most carefully designed locking strategy can become a bottleneck.

This is where queues and event-driven architectures become powerful alternatives.

The Problem with Direct Processing

In a traditional synchronous flow, every request immediately interacts with the database.

User Request
↓
Application Server
↓
Database

When thousands of requests arrive simultaneously:

  • Database contention increases
  • Locks become more frequent
  • Response times grow
  • Throughput decreases

As traffic scales, the database often becomes the system’s bottleneck.

Introducing a Queue

Instead of processing requests immediately, the system can place requests into a queue.

User Request
↓
API Gateway
↓
Message Queue
↓
Booking Processor
↓
Database

The queue acts as a buffer between incoming traffic and backend processing.

Rather than allowing thousands of users to compete for the same resource simultaneously, requests are processed in a controlled and predictable manner.

How This Solves Concurrency Problems

Let’s revisit our seat-booking example.

Assume only one seat remains.

Two users submit booking requests at the same time.

Instead of both requests immediately attempting to update the database, they enter a queue.

Queue
Position 1 → User A
Position 2 → User B

The booking processor consumes requests sequentially.

Process User A
↓
Seat Available
↓
Book Seat
↓
Commit

Next:

Process User B
↓
Seat Not Available
↓
Reject Request

Because requests are processed one at a time, race conditions are effectively eliminated.

Why Large-Scale Systems Prefer Queues

Queues provide several advantages beyond concurrency control.

Traffic Smoothing

User traffic is often unpredictable.

For example:

Normal Traffic
1,000 Requests / Minute
Flash Sale
100,000 Requests / Minute

Without a queue, backend services may become overwhelmed.

A queue absorbs sudden spikes and allows workers to process requests at a sustainable rate.

Improved Reliability

Suppose the booking service temporarily becomes unavailable.

Without a queue:

Request
↓
Service Failure
↓
Request Lost

With a queue:

Request
↓
Queue
↓
Temporary Failure
↓
Retry Processing

Requests remain safely stored until they can be processed.

Better Scalability

As demand grows, additional consumers can be added.

Queue
↓
Worker 1
Worker 2
Worker 3
Worker 4

This allows systems to scale horizontally without changing business logic. Many modern distributed systems use dedicated messaging platforms.

Common choices include (Popular Technologies):

  • Apache Kafka
  • RabbitMQ
  • Amazon SQS

These platforms provide durability, retry mechanisms, ordering guarantees, and fault tolerance.

Event-Driven Architecture

Queues often serve as the foundation of event-driven systems.

Instead of tightly coupling services together:

Booking Service
↓
Payment Service
↓
Notification Service

Systems communicate through events.

    Booking Created Event
↓
┌────────────┼────────────┐
↓ ↓ ↓
Payment Notification Analytics

Each service reacts independently.

Benefits include:

  • Better scalability
  • Reduced coupling
  • Improved fault tolerance
  • Easier system evolution

Real-World Example: E-Commerce Flash Sales

Consider an online store selling a limited-edition product.

Only 500 units are available.

At launch, 50,000 users attempt to purchase simultaneously.

Without a queue:

  • Database locks increase dramatically
  • Requests compete aggressively
  • System performance degrades

With a queue:

Purchase Request
↓
Queue
↓
Inventory Processor
↓
Inventory Update

Requests are processed in order, inventory remains consistent, and the user experience becomes far more predictable.

Trade-Offs

Like every architectural decision, queues introduce trade-offs.

Advantages

  • Reduced race conditions
  • Improved scalability
  • Better fault tolerance
  • Traffic spike protection
  • Increased throughput

Disadvantages

  • Increased architectural complexity
  • Eventual consistency
  • More infrastructure to manage
  • Additional monitoring requirements

Locking ensures that multiple users cannot modify the same resource simultaneously.

Queues take a different approach.

Instead of preventing concurrency, they control it.

This distinction is why many high-scale systems combine queues with locking strategies, transactions, and idempotency mechanisms to achieve both consistency and scalability.

Why Queues Work So Well

The primary benefit of a queue is that it converts an uncontrolled concurrency problem into a predictable processing pipeline.

Instead of thousands of requests competing simultaneously:

100,000 Requests
↓
Database

the workload becomes:

100,000 Requests
↓
Queue
↓
Controlled Processing
↓
Database

This dramatically reduces contention and improves stability.

Handling Traffic Spikes

One of the biggest challenges in distributed systems is sudden traffic bursts.

Consider a limited-time sale.

Normal Traffic
1,000 Requests / Minute
Flash Sale
100,000 Requests / Minute

Without a queue, backend services may become overwhelmed.

A queue absorbs the spike and allows the system to process requests at a sustainable rate.

This approach is commonly known as traffic smoothing.

Improving Reliability

Queues also improve fault tolerance.

Suppose the booking service temporarily crashes.

Without a queue:

Request
↓
Service Failure
↓
Request Lost

With a queue:

Request
↓
Queue
↓
Service Failure
↓
Retry Processing

The request remains safely stored until processing resumes, this significantly increases system reliability.

Understanding Eventual Consistency

Throughout this article, we’ve discussed consistency as one of the most important challenges in distributed systems.

In traditional database systems, we often expect changes to become visible immediately after a transaction commits.

However, as systems scale across multiple services, databases, and geographical regions, maintaining immediate consistency becomes increasingly expensive.

This is where the concept of Eventual Consistency comes into play.

What Is Eventual Consistency?

Eventual Consistency is a consistency model where updates do not become visible across the entire system immediately.

Instead, all parts of the system gradually converge toward the same state over time.

In simple terms:

The system may temporarily show different versions of the data, but given enough time and no further updates, all copies will eventually become consistent.

A Simple Example

Imagine an e-commerce platform.

A customer purchases the last available product.

The Inventory Service updates the stock count immediately.

Inventory Service
↓
Stock = 0

However, other services may not be updated instantly.

Inventory Service     → Updated
Search Service → Not Updated Yet
Recommendation Engine → Not Updated Yet
Analytics Service → Not Updated Yet

For a brief period:

Search Results
↓
Still Shows Product Available

This temporary inconsistency is expected.

After background synchronization occurs:

Inventory Service     → Updated
Search Service → Updated
Recommendation Engine → Updated
Analytics Service → Updated

all services eventually reach the same state.

Why Not Make Everything Consistent Immediately?

A common question is:

Why don’t we simply update every service at the same time?

The answer is scalability.

Consider a global application with:

  • Multiple microservices
  • Multiple databases
  • Multiple regions
  • Millions of users

Waiting for every component to update before responding to a user would significantly increase latency and reduce system availability.

Get Abhishek Frances’s stories in your inbox

Join Medium for free to get updates from this writer.

Eventual consistency allows systems to remain responsive while updates propagate asynchronously.

Eventual Consistency in Event-Driven Systems

Consider a booking platform.

A successful booking generates an event.

Booking Confirmed
↓
Booking Created Event

That event is consumed by multiple services.

          Booking Event
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
Payment Notification Analytics

These services process the event independently.

As a result, updates do not occur simultaneously.

For example:

Booking Service      → Updated
Notification Service → Processing
Analytics Service → Processing

For a short period, different services may reflect different states.

Eventually, all services become synchronized.

A Practical Question: What If Eventual Consistency Isn’t Acceptable?

While discussing eventual consistency, a natural question arises:

If eventual consistency can lead to temporarily stale data, should we still use it when building an Inventory Management System for an ERP?

The short answer is:

Not for inventory updates themselves.

Inventory is typically considered a source of truth.

If a product has only one unit remaining, the system cannot afford a situation where two users see the same inventory count and successfully place orders.

For example:

Current Inventory = 1
User A places an order
User B places an order
Both services read:
Inventory = 1

If inventory updates rely purely on eventual consistency:

User A → Order Success
User B → Order Success
Inventory Sync Happens LaterResult:
Inventory = -1

The system has oversold inventory.

For most ERP systems, this is unacceptable.

Does This Mean We Should Avoid Eventual Consistency Entirely?

Not necessarily.

A common mistake is assuming that the entire system must follow a single consistency model.

In practice, modern systems often combine both strong consistency and eventual consistency.

The key is identifying which data requires immediate accuracy and which data can tolerate delays.

Strong Consistency for Inventory Operations

Inventory updates should typically be protected using transactions and locking mechanisms.

A simplified flow might look like:

Order Request
↓
Database Transaction
↓
Row-Level Lock
↓
Reduce Inventory
↓
Commit Transaction

This guarantees that inventory cannot become negative due to concurrent updates.

Eventual Consistency for Supporting Systems

Once the inventory update has been committed successfully, the system can publish an event.

Inventory Updated
↓
Inventory Event

Other services can process this event asynchronously.

       Inventory Event
┌────────────┼────────────────┬────────────┐
↓ ↓ ↓ ↓
Reports Analytics Notifications Dashboards

These systems may update a few seconds later without affecting business correctness.

The Hybrid Approach

This pattern is commonly used in ERP, e-commerce, and warehouse management systems.

Inventory Update
↓
Strong Consistency
↓
Event Published
↓
Eventual Consistency

The source-of-truth data remains accurate, while downstream systems benefit from the scalability and flexibility of asynchronous processing.

The Real Design Question

The decision is rarely:

Should I use strong consistency or eventual consistency?

The better question is:

Which parts of my system absolutely require immediate correctness, and which parts can tolerate temporary inconsistency?

For an ERP inventory system, inventory quantities, stock reservations, and order allocations usually require strong consistency.

Reports, dashboards, notifications, and analytics can often operate under eventual consistency.

Understanding this distinction is one of the most important architectural decisions when designing large-scale business systems.

Strong Consistency vs Eventual Consistency

Neither approach is universally better.

The correct choice depends on business requirements.

What Big Systems Actually do

They separate:

Critical Data vs Non-Critical Data

When Is Eventual Consistency Acceptable?

Eventual consistency works well when temporary stale data is acceptable. Non-Critical Data can tolerate delays.

Examples:

  • Social media feeds
  • Product recommendations
  • Analytics dashboards
  • Notification systems
  • Search indexing

In these systems, a delay of a few seconds is usually not a business problem.

When Is Eventual Consistency Not Acceptable?

Some systems require immediate accuracy, Critical Data must always be correct.

Examples:

  • Banking transactions
  • Payment processing
  • Inventory reservation
  • Ticket booking
  • Financial trading systems

In these scenarios, even a brief inconsistency can cause financial loss or incorrect business outcomes.

Key Takeaway

Eventual consistency is not a flaw in distributed systems.

It is a deliberate architectural trade-off.

By accepting temporary inconsistencies, systems gain better scalability, availability, and performance.

The most important question is not:

Is eventual consistency good or bad?

The real question is:

Can my business tolerate temporary inconsistencies while the system converges to the correct state?

Understanding that trade-off is one of the fundamental skills required for designing modern distributed systems.

Choosing the Right Strategy

Press enter or click to view image in full size

At this point, we’ve explored several techniques for handling concurrent requests:

  • Transactions
  • Pessimistic Locking
  • Optimistic Locking
  • Distributed Locking
  • Idempotency
  • Queues and Event-Driven Processing

A common question arises:

Which approach should I use?

The answer is that there is no universally correct solution.

Every concurrency strategy involves trade-offs between consistency, performance, scalability, complexity, and user experience.

The right choice depends entirely on the problem you’re trying to solve.

Start with the Business Requirement

Before selecting a technical solution, it’s important to understand the business impact of inconsistency.

Ask yourself:

  • Can duplicate operations occur?
  • Can users tolerate temporary inconsistency?
  • Is data accuracy more important than performance?
  • How frequently will conflicts occur?
  • What happens if two requests succeed simultaneously?

The answers to these questions will often determine the architecture more than the technology itself.

When to Use Pessimistic Locking

Pessimistic locking assumes conflicts are likely.

The system locks a resource before allowing modifications.

Transaction A
↓
Acquire Lock
↓
Update Resource
↓
Release Lock
Transaction B
↓
Wait

This approach works best when:

  • Data consistency is critical
  • Conflicts occur frequently
  • Throughput requirements are moderate

Examples:

  • Banking systems
  • Financial transactions
  • Critical inventory updates

Advantages:

  • Strong consistency
  • Easy to understand
  • Prevents conflicting updates

Trade-offs:

  • Reduced concurrency
  • Increased waiting time
  • Lower scalability under heavy traffic

When to Use Optimistic Locking

Optimistic locking assumes conflicts are relatively rare.

Instead of locking data, the system detects conflicts during updates.

Read Resource
↓
Check Version
↓
Update If Version Matches
↓
Success or Retry

This approach works best when:

  • Read operations greatly outnumber writes
  • Contention is relatively low
  • High scalability is required

Examples:

  • E-commerce catalogs
  • User profile updates
  • Content management systems

Advantages:

  • High throughput
  • Better scalability
  • Minimal locking overhead

Trade-offs:

  • Requires retry logic
  • Increased application complexity
  • Poor fit for heavily contested resources

When to Use Distributed Locking

Traditional database locks only work within a database.

Modern systems often run across multiple servers and services.

In these environments, coordination must occur across the entire distributed system.

Server A
↓
Acquire Distributed Lock
Server B
↓
Wait for Lock

Distributed locking is useful when:

  • Multiple application instances exist
  • Services operate independently
  • Shared resources span multiple nodes

Examples:

  • Ride-sharing systems
  • Inventory reservation systems
  • Distributed schedulers

Advantages:

  • Coordinates across services
  • Prevents duplicate processing

Trade-offs:

  • Additional infrastructure
  • Increased operational complexity
  • Lock timeout considerations

When to Use Idempotency

Some systems don’t suffer from resource contention.

Instead, they suffer from duplicate requests.

Consider payment processing.

A user clicks:

Pay Now
Pay Now
Pay Now

Without protection:

Charge 1
Charge 2
Charge 3

With idempotency:

Request ID: ABC123
Already Processed
Return Existing Result

Idempotency is essential when:

  • Network retries are common
  • User actions may be repeated
  • Duplicate execution is unacceptable

Examples:

  • Payment gateways
  • Order processing
  • External API integrations

Advantages:

  • Prevents duplicate operations
  • Improves reliability
  • Simple user experience

Trade-offs:

  • Does not solve resource contention
  • Requires request tracking infrastructure

When to Use Queues

Queues are ideal when systems experience high traffic volumes.

Instead of allowing requests to compete simultaneously, requests are processed in a controlled sequence.

Incoming Requests
↓
Message Queue
↓
Workers
↓
Database

Queues work best when:

  • Traffic spikes are common
  • Throughput is more important than immediate response
  • Asynchronous processing is acceptable

Examples:

  • Flash sales
  • Order processing
  • Notification systems
  • Large-scale booking platforms

Advantages:

  • Excellent scalability
  • Traffic smoothing
  • Improved fault tolerance

Trade-offs:

  • Eventual consistency
  • Increased architectural complexity
  • Additional infrastructure

Combining Strategies in Real Systems

One of the biggest misconceptions in system design is assuming that only one strategy should be used.

Most production systems combine multiple approaches.

For example, a booking platform might use:

API Request
↓
Idempotency Check
↓
Queue
↓
Booking Processor
↓
Database Transaction
↓
Row-Level Lock

Each component solves a different problem:

  • Idempotency prevents duplicate requests
  • Queues handle traffic spikes
  • Transactions ensure atomicity
  • Locks prevent conflicting updates

This layered approach provides both consistency and scalability.

Trade-Off Comparison: Choosing the Right Tool for the Job

Throughout this article, we’ve explored multiple approaches to handling concurrent requests.

At first glance, it may seem that some strategies are superior to others.

In reality, every concurrency-control mechanism solves a specific problem and introduces its own trade-offs.

As engineers, our goal is not to find the “best” solution.

Our goal is to find the most appropriate solution for the system’s requirements.

Comparing the Available Approaches

Consistency vs Scalability

One of the most important trade-offs in distributed systems is the balance between consistency and scalability.

Consider the following simplified spectrum:

Strong Consistency
|
|
Pessimistic Locking
Transactions
Distributed Locking
Optimistic Locking
Queues
|
|
High Scalability

As consistency guarantees increase, throughput often decreases.

As scalability increases, systems may need to tolerate temporary inconsistencies.

Finding the right balance is one of the most important architectural decisions engineers make.

Understanding the Trade-Offs

Transactions

Transactions provide atomicity and ensure that a group of operations either succeeds completely or fails entirely.

Best suited for:

  • Financial operations
  • Order processing
  • Inventory updates

Strengths:

  • Simple to implement
  • Strong consistency guarantees

Limitations:

  • Limited scalability under extreme contention

Decision Matrix

When designing a system, the following can help identify the appropriate approach.

Press enter or click to view image in full size

Concurrency control is not about choosing a single technique.

It is about understanding the trade-offs between consistency, scalability, performance, and complexity. The most successful systems combine multiple strategies, applying each one where it delivers the greatest value.

The mark of a mature system design is not the absence of trade-offs.

It is the deliberate and informed management of those trade-offs.

System Design Interview Perspective

By now, we’ve explored race conditions, transactions, isolation levels, locking strategies, idempotency, queues, and real-world implementations.

Understanding these concepts is important.

However, in a system design interview, interviewers are rarely testing whether you can recite definitions.

They’re evaluating how you think when presented with a concurrency problem.

Consider the following interview question:

Two users attempt to purchase the last available item at exactly the same time. How would you design the system?

Many candidates immediately jump into implementation details:

  • Use Redis
  • Use Kafka
  • Use Transactions
  • Use Distributed Locks

While these answers may be technically correct, they often skip the most important step:

Understanding the problem before selecting a solution.

The Thought Process Interviewers Expect

When you hear a concurrency-related problem, your reasoning should follow a structured approach.

Identify Shared Resource
↓
Identify Consistency Requirements
↓
Estimate Traffic and Contention
↓
Evaluate Failure Scenarios
↓
Select Appropriate Strategy

Let’s break this down.

Step 1: Identify the Shared Resource

The first question should always be:

What resource is being modified?

Examples:

Concurrency problems only exist because multiple requests compete for the same resource.

Understanding the resource helps narrow the solution space.

Step 2: Understand the Consistency Requirements

Not every system requires the same level of consistency.

Ask:

What happens if two requests succeed simultaneously?

For example:

Movie Booking

Two Users
↓
One Seat

Result:

Double Booking

Unacceptable.

Strong consistency is required.

Analytics Dashboard

Page Views
↓
Temporary Delay

Result:

Slightly Stale Data

Often acceptable.

Eventual consistency may be sufficient.

The stricter the consistency requirement, the stronger the concurrency controls generally need to be.

Step 3: Estimate Traffic and Contention

The next question is:

How often will conflicts occur?

Consider two scenarios.

Scenario A

100 Users
10,000 Products

Conflicts are rare.

Optimistic Locking may be sufficient.

Scenario B

100,000 Users
500 Limited Edition Products

Conflicts are frequent.

Additional controls such as locking, reservations, or queues may be required.

Traffic volume significantly influences architectural decisions.

Step 4: Think About Failure Scenarios

Strong candidates don’t only consider successful requests.

They think about failures.

Ask:

  • What happens if the service crashes?
  • What happens if the database becomes unavailable?
  • What happens if the request is retried?
  • What happens if the user refreshes the page?
  • What happens if network latency causes duplicate submissions?

For example:

User Clicks Pay
↓
Network Timeout
↓
User Clicks Again

Without idempotency:

Charge 1
Charge 2

This type of reasoning demonstrates production experience.

Step 5: Select the Appropriate Strategy

Only after understanding the business requirements should a solution be selected.

Examples:

Strong Consistency →Pessimistic Locking

High Throughput → Optimistic Locking

Multiple Servers → Distributed Locking

Duplicate Requests → Idempotency

Traffic Spikes → Queues

The key is matching the strategy to the problem.

Common Interview Mistakes

Many candidates make the following mistakes:

Jumping Directly to Technology

Use Redis.
Use Kafka.
Use PostgreSQL.

Technology is not the design.

The design is the reasoning behind the technology choice.

Ignoring Trade-Offs

Every solution introduces compromises.

Strong candidates explain both advantages and disadvantages.

Interviewers want to know that you understand the consequences of your decisions.

Focusing Only on Happy Paths

Production systems fail.

Discussing retries, duplicate requests, network failures, and service outages demonstrates real-world engineering experience.

Key Takeaway

Concurrency problems are among the most common topics in system design interviews because they reveal how engineers think about correctness, scalability, and reliability.

The best answers don’t begin with a technology choice.

They begin with understanding:

  • The shared resource
  • The consistency requirements
  • The traffic characteristics
  • The failure scenarios
  • The trade-offs involved

Once those factors are clear, selecting the appropriate concurrency strategy becomes significantly easier.

This structured approach is often what separates senior engineers from developers who simply know the tools.

--

--

Abhishek Frances
Abhishek Frances

Written by Abhishek Frances

0 followers

Senior Software Engineer building scalable web apps, microservices, and distributed systems. Writing about React, Go, Kafka, gRPC, and system design.