Skip to content

Dealing With Contention

The Problem

Contention occurs when multiple processes/threads/users try to access or modify the same resource simultaneously, leading to:

  • Race conditions
  • Data corruption
  • Lost updates
  • Deadlocks
  • Performance degradation

Common Scenarios: - Two users editing the same document - Multiple requests updating inventory count - Concurrent bank transfers from same account - High-traffic ticket booking (concerts, flights) - Flash sales with limited inventory - Counter increments (likes, views)


Options & Trade-offs

1. Pessimistic Locking

Philosophy: "Assume conflicts will happen, prevent them upfront"

Pessimistic Locking

Implementation Types:

Database Row-Level Locks

-- Exclusive lock (write)
SELECT * FROM inventory
WHERE product_id = 123
FOR UPDATE;

-- Shared lock (read)
SELECT * FROM inventory
WHERE product_id = 123
FOR SHARE;

-- With timeout (PostgreSQL)
SET lock_timeout = '5s';
SELECT * FROM inventory WHERE id = 123 FOR UPDATE;

Distributed Locks (Redis)

// Using Redisson
RLock lock = redisson.getLock("inventory:123");
try {
    // Wait up to 10 seconds, lock expires after 30 seconds
    if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
        try {
            // Critical section
            updateInventory();
        } finally {
            lock.unlock();
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Redlock Algorithm (Multiple Redis Nodes)

1. Get current time
2. Try to acquire lock on N/2+1 nodes
3. If successful on majority within timeout → lock acquired
4. If failed → release all locks, retry
Pros Cons
Guarantees consistency Reduces concurrency/throughput
Simple mental model Risk of deadlocks
Prevents conflicts entirely Lock contention under high load
Good for high-conflict scenarios Increased latency
Single point of failure (lock server)

When to use: - Financial transactions - Inventory with high contention - Any operation that MUST NOT have conflicts - Short-duration operations


2. Optimistic Locking

Philosophy: "Assume conflicts are rare, detect them when they happen"

Optimistic Locking

Implementation Types:

Version Numbers

-- Read with version
SELECT id, quantity, version FROM inventory WHERE id = 123;
-- Returns: id=123, quantity=10, version=5

-- Update with version check
UPDATE inventory
SET quantity = 9, version = version + 1
WHERE id = 123 AND version = 5;

-- If affected_rows = 0 → conflict, retry

Timestamps

UPDATE inventory
SET quantity = 9, updated_at = NOW()
WHERE id = 123 AND updated_at = '2024-01-15 10:30:00';

ETags (HTTP)

GET /api/document/123
Response:
ETag: "abc123"

PUT /api/document/123
If-Match: "abc123"
Content-Type: application/json
{"content": "updated"}

Response (if changed): 412 Precondition Failed
Response (if success): 200 OK, ETag: "def456"

JPA/Hibernate

@Entity
public class Inventory {
    @Id
    private Long id;

    @Version
    private Long version;

    private Integer quantity;
}

// Throws OptimisticLockException on conflict
Pros Cons
High concurrency Retry overhead on conflicts
No lock contention Complex retry logic
No deadlocks May starve under high contention
Good for read-heavy workloads Not suitable for long transactions
Works well in distributed systems

When to use: - Read-heavy workloads - Low conflict probability - User-facing edits (documents, profiles) - APIs with ETag support


3. Atomic Operations

Philosophy: "Make the operation indivisible at the lowest level"

Database Atomic Updates:

-- Instead of: SELECT then UPDATE
-- Do: Single atomic UPDATE
UPDATE inventory
SET quantity = quantity - 1
WHERE id = 123 AND quantity > 0;

-- Check affected_rows to know if successful

Redis Atomic Operations:

# Atomic increment
INCR page_views:article:123

# Atomic decrement with check
EVAL "
    local current = redis.call('GET', KEYS[1])
    if tonumber(current) > 0 then
        return redis.call('DECR', KEYS[1])
    else
        return -1
    end
" 1 inventory:product:123

Compare-And-Swap (CAS):

AtomicInteger counter = new AtomicInteger(0);

// Atomic increment
counter.incrementAndGet();

// Compare and swap
int expected = 5;
int newValue = 6;
boolean success = counter.compareAndSet(expected, newValue);

Pros Cons
Highest performance Limited to simple operations
No lock overhead Can't span multiple resources
Database handles concurrency Business logic must fit pattern
Simple implementation

When to use: - Counters (views, likes) - Simple inventory decrements - Rate limiting - Any single-field update


4. Queue-Based Serialization

Philosophy: "Serialize all writes through a single queue"

Queue Serialization

Implementation:

// Partition by resource ID for ordering
@KafkaListener(topics = "inventory-updates")
public void processUpdate(InventoryUpdate update) {
    // All updates for same product processed serially
    inventoryService.update(update);
}

// Producer partitions by product ID
kafkaTemplate.send("inventory-updates",
    productId.toString(), // partition key
    update);

Pros Cons
Guaranteed ordering Added latency (async)
No conflicts possible Queue becomes bottleneck
Can batch operations Complexity of queue infrastructure
Natural audit log Error handling complexity
Not suitable for sync operations

When to use: - Order processing - Event sourcing - When eventual consistency is OK - High-write scenarios that can be async


5. Sharding / Partitioning

Philosophy: "Divide resources so they don't contend"

Sharding

Strategies: - Hash-based: shard = hash(id) % num_shards - Range-based: A-H → Shard1, I-P → Shard2 - Geo-based: US → Shard1, EU → Shard2 - Tenant-based: Customer1 → Shard1

Pros Cons
Linear scalability Cross-shard operations complex
Reduced contention per shard Rebalancing challenges
Data locality Hotspots possible
Increased operational complexity

When to use: - Large-scale systems - Multi-tenant applications - When data naturally partitions (users, regions)


6. CRDTs (Conflict-free Replicated Data Types)

Philosophy: "Design data structures that automatically merge without conflicts"

Types: - G-Counter: Grow-only counter - PN-Counter: Positive-Negative counter - G-Set: Grow-only set - LWW-Register: Last-Writer-Wins register - OR-Set: Observed-Remove set

G-Counter Example:

Node A: {A: 3, B: 0, C: 0} → total = 3
Node B: {A: 0, B: 5, C: 0} → total = 5
Node C: {A: 0, B: 0, C: 2} → total = 2

Merged: {A: 3, B: 5, C: 2} → total = 10

Implementation:

public class GCounter {
    private Map<String, Long> counts = new HashMap<>();
    private String nodeId;

    public void increment() {
        counts.merge(nodeId, 1L, Long::sum);
    }

    public long value() {
        return counts.values().stream().mapToLong(Long::longValue).sum();
    }

    public void merge(GCounter other) {
        other.counts.forEach((key, value) ->
            counts.merge(key, value, Math::max));
    }
}

Pros Cons
No conflicts by design Limited operation types
Works offline Can grow unbounded (tombstones)
Eventually consistent Complex to implement correctly
Great for distributed systems May not match business requirements

When to use: - Collaborative editing - Distributed counters - Shopping carts - Offline-first applications


7. Application-Level Techniques

Idempotent Operations

// Instead of: inventory -= 1
// Use idempotent: set inventory to specific value
@Transactional
public void processOrder(String orderId, int productId) {
    if (orderAlreadyProcessed(orderId)) {
        return; // Idempotent
    }
    // Process order
    markOrderProcessed(orderId);
}

Read-Your-Writes

// After write, read from primary
// Avoids reading stale data from replica
public Order createOrder(OrderRequest request) {
    Order order = orderRepository.save(request);
    // Force read from primary
    return orderRepository.findByIdFromPrimary(order.getId());
}

Lease-Based Locks

// Lock with automatic expiration
String lockValue = UUID.randomUUID().toString();
boolean acquired = redis.set("lock:resource", lockValue, "NX", "EX", 30);

if (acquired) {
    try {
        // Do work, extend lease if needed
        redis.expire("lock:resource", 30);
    } finally {
        // Only release if we still own it
        String script = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
        """;
        redis.eval(script, "lock:resource", lockValue);
    }
}

Comparison Matrix

Approach Consistency Throughput Complexity Best For
Pessimistic Lock Strong Low Medium Financial, inventory
Optimistic Lock Strong High Medium Low-conflict updates
Atomic Operations Strong Very High Low Counters, simple updates
Queue Serialization Eventual High High Event processing
Sharding Strong (per shard) Very High High Scale-out
CRDTs Eventual Very High Very High Distributed, offline

Decision Tree

Decision Tree


Real-World Examples

Flash Sale (High Contention)

Strategy: Queue + Pessimistic Lock + Sharding

1. Requests hit rate limiter
2. Valid requests queued by product ID
3. Workers process with DB locks
4. Inventory sharded by product category

Collaborative Document Editing

Strategy: CRDTs + Operational Transform

1. Each keystroke = operation
2. Operations merged using OT/CRDT
3. Conflicts auto-resolved
4. Periodic snapshots for efficiency

Bank Account Transfer

Strategy: Pessimistic Lock + Two-Phase Commit

1. Lock source account
2. Lock destination account (ordered by ID to prevent deadlock)
3. Verify sufficient balance
4. Debit source, credit destination
5. Release locks

Social Media Likes Counter

Strategy: Atomic Operations + Eventual Consistency

1. Increment in Redis (atomic)
2. Async sync to database
3. Display approximate count
4. Exact count calculated periodically

Anti-Patterns to Avoid

  1. Lock and forget - Always have timeouts
  2. Nested locks - Deadlock risk
  3. Long-held locks - Blocks others
  4. Ignoring failures - Silent corruption
  5. Over-locking - Performance killer
  6. SELECT then UPDATE - Race condition

Key Takeaways

  1. Understand your conflict rate - Optimistic for low, pessimistic for high
  2. Atomic operations first - Simplest solution if possible
  3. Timeouts always - Prevent indefinite waits
  4. Order lock acquisition - Prevent deadlocks
  5. Consider eventual consistency - Often acceptable, much simpler
  6. Shard when possible - Best way to eliminate contention