February 18, 202610 min read

API Integration Patterns for Production Systems

Battle-tested patterns for building reliable API integrations: retry strategies, circuit breakers, idempotency, and rate limiting.

BackendArchitectureAPI Design

Building API integrations that work in production — not just in Postman — requires patterns for handling the real world: flaky networks, rate limits, schema changes, and partial failures.

Retry with Exponential Backoff

Network errors and 5xx responses are inevitable. A retry strategy with exponential backoff prevents overwhelming a recovering service while maximizing your chances of success.

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url, options)
      if (res.ok) return res
      if (res.status < 500) throw new Error(`Client error: ${res.status}`)
    } catch (err) {
      if (attempt === maxRetries) throw err
    }
    const delay = Math.min(1000 * 2 ** attempt, 30000)
    const jitter = delay * (0.5 + Math.random() * 0.5)
    await new Promise((r) => setTimeout(r, jitter))
  }
}

Circuit Breaker

When an upstream service is down, continuing to send requests wastes resources and slows down your entire system. A circuit breaker short-circuits failed requests after a threshold.

class CircuitBreaker {
  failures = 0; lastFailure = 0; state = "closed"
  constructor(private threshold = 5, private resetMs = 60000) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.lastFailure > this.resetMs)
        this.state = "half-open"
      else throw new Error("Circuit is open")
    }
    try {
      const result = await fn()
      this.failures = 0; this.state = "closed"
      return result
    } catch (err) {
      this.failures++; this.lastFailure = Date.now()
      if (this.failures >= this.threshold) this.state = "open"
      throw err
    }
  }
}

Idempotency Keys

For mutating operations (payments, order creation), an idempotency key ensures that retried requests don't duplicate side effects — the server reuses the original response.

const idempotencyKey = crypto.randomUUID()

const response = await fetch("/api/payments", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify({ amount: 4999, currency: "USD" }),
})

Rate Limiting (Client Side)

Respect the API's rate limits before they enforce them. A token bucket algorithm gives you smooth, predictable request pacing.

class RateLimiter {
  private tokens: number
  private lastRefill: number
  constructor(
    private maxTokens: number,
    private refillRate: number // tokens per second
  ) {
    this.tokens = maxTokens
    this.lastRefill = Date.now()
  }
  async acquire(): Promise<void> {
    this.refill()
    if (this.tokens < 1) {
      const waitMs = (1 / this.refillRate) * 1000
      await new Promise((r) => setTimeout(r, waitMs))
      this.refill()
    }
    this.tokens--
  }
  private refill() {
    const now = Date.now()
    const elapsed = (now - this.lastRefill) / 1000
    this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate)
    this.lastRefill = now
  }
}

Wrapping Up

These patterns aren't optional for production systems. Retries handle transient failures, circuit breakers prevent cascade failures, idempotency keys prevent duplicates, and rate limiting keeps you within bounds. Layer them together for integrations that survive the real world.

Found this article useful? Share it with your team or explore more developer resources below.