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.