HOME > ARTICLES > API DESIGN PATTERNS FOR DISTRIBUTED SYSTEMS

API DESIGN PATTERNS FOR DISTRIBUTED SYSTEMS

[2023.09.18]  12_MIN_READ

Introduction

Designing APIs for distributed systems requires thinking beyond the happy path. Network partitions, partial failures, and eventual consistency are not edge cases — they’re the norm.

Idempotency Keys

Make state-mutating operations safe to retry:

async function createOrder(
  data: OrderData,
  idempotencyKey: string
): Promise<Order> {
  const existing = await db.orders.findByIdempotencyKey(idempotencyKey);
  if (existing) return existing;

  return db.orders.create({ ...data, idempotencyKey });
}

The Outbox Pattern

Ensure database writes and message publication are atomic:

BEGIN;
  INSERT INTO orders (id, status) VALUES ($1, 'pending');
  INSERT INTO outbox (event_type, payload) VALUES ('order.created', $2);
COMMIT;

A separate process polls the outbox and publishes events, marking them as sent.

Pagination Strategies

Offset pagination breaks under concurrent inserts. Use cursor-based pagination:

type PageCursor = {
  id: string;
  createdAt: Date;
};

async function getOrders(cursor?: PageCursor, limit = 20) {
  return db.orders
    .where(cursor ? { createdAt: { lt: cursor.createdAt } } : {})
    .orderBy('createdAt', 'desc')
    .limit(limit);
}

Circuit Breakers

Prevent cascade failures when a downstream service degrades:

const breaker = new CircuitBreaker(callPaymentService, {
  threshold: 5,      // failures before opening
  timeout: 10_000,   // ms before trying again
  resetTimeout: 30_000,
});

Conclusion

These patterns address the fundamental challenges of distributed systems: partial failures, ordering guarantees, and operational safety. Each adds complexity — apply them where the failure modes justify it.

< /articles