Skip to content

Distributed Coordination

Choosing the right coordination mechanism is critical. This guide helps you decide between Locks, Idempotency, and Database constraints.

The Problem

In distributed systems, concurrent operations can cause:

  • Double processing — Same request processed twice
  • Race conditions — Interleaved operations corrupt state
  • Lost updates — Concurrent writes overwrite each other

Three Solutions

MechanismPreventsUse Case
IdempotencyDuplicate requestsClient retries same operation
LocksConcurrent executionMultiple operations on same resource
DatabaseData inconsistencyFinal safety net

Decision Tree

Idempotency

Purpose: Detect and handle duplicate requests from the same client.

How it works:

  1. Client sends request with idempotency key
  2. Server checks if key was seen before
  3. If yes → return cached response
  4. If no → process and cache response
typescript
@Post('payments')
@Idempotent()
async createPayment(@Body() dto: CreatePaymentDto) {
  // Only executes once per idempotency key
  return this.paymentService.process(dto);
}

Best for:

  • Payment processing
  • Order creation
  • Any operation where client might retry

NOT for:

  • Preventing concurrent access from different users
  • Coordinating between services

Locks

Purpose: Ensure only one operation executes at a time for a resource.

How it works:

  1. Acquire lock on resource identifier
  2. Execute operation
  3. Release lock
typescript
@WithLock({ key: 'order:{0}' })
async processOrder(orderId: string) {
  // Only one execution per orderId at a time
  return this.orderService.process(orderId);
}

Best for:

  • Inventory updates (prevent overselling)
  • Account balance changes
  • Any shared resource modification

NOT for:

  • Detecting duplicate requests (use Idempotency)
  • Long-running operations (lock timeout issues)

Database Constraints

Purpose: Final line of defense for data integrity.

How it works:

  • UNIQUE constraints prevent duplicates
  • Foreign keys ensure referential integrity
  • Optimistic locking detects conflicts
sql
-- Unique constraint
CREATE UNIQUE INDEX idx_payments_idempotency 
ON payments(idempotency_key);

-- Optimistic locking
UPDATE orders 
SET status = 'shipped', version = version + 1
WHERE id = ? AND version = ?;

Best for:

  • Ensuring data integrity regardless of application bugs
  • Handling edge cases that slip through other mechanisms

NOT for:

  • Primary coordination (too late in the flow)
  • Performance-sensitive paths (lock contention)

Combining Mechanisms

For critical operations like payments, use all three:

typescript
@Post('payments')
@Idempotent()  // Layer 1: Detect retries
async createPayment(
  @Body() dto: CreatePaymentDto,
  @Headers('Idempotency-Key') key: string,
) {
  return this.lockService.withLock(
    `payment:${dto.orderId}`,  // Layer 2: Serialize
    async () => {
      // Layer 3: DB constraint on idempotency_key
      return this.paymentService.process(dto, key);
    },
  );
}

Comparison Table

AspectIdempotencyLocksDB Constraints
ScopeSame client requestAny concurrent accessData layer
GranularityRequestResourceRow/Table
Failure modeAllow (configurable)TimeoutError
PerformanceFast (cache lookup)Medium (Redis roundtrip)Varies
Use alone?For retries onlyFor coordination onlyAs safety net

Anti-Patterns

Using locks for idempotency

typescript
// WRONG: Lock doesn't detect retries
@WithLock({ key: 'payment:{0}' })
async createPayment(orderId: string) {
  // Retry will wait for lock, then process again!
}

Using idempotency for coordination

typescript
// WRONG: Idempotency doesn't block concurrent users
@Idempotent()
async updateInventory(productId: string, delta: number) {
  // Two users can still interleave reads/writes!
}

Skipping database constraints

typescript
// WRONG: Application bugs can still cause duplicates
@Idempotent()
@WithLock({ key: 'payment:{0}' })
async createPayment(/*...*/) {
  // No UNIQUE constraint = risk of duplicates
}

Next Steps

Released under the MIT License.