2 min read

Idempotency Key API Design

Organizing key scope, TTL, and response reuse policies to safely handle duplicate requests

Idempotency Key API Design thumbnail

Introduction

In functions where duplicate execution is critical, such as payment, ordering, and point accumulation, data consistency problems begin as soon as network retries are allowed. Overlapping client retries, gateway timeouts, and message broker retransmissions can cause the same request to be processed multiple times. This article defines Idempotency-Key as an API contract and summarizes a practical guide for designing storage/TTL/response cache.

Idempotency Key API 설계 커버
Wikimedia Commons 기반 무료 이미지

Problem definition

Redundant processing issues are usually discovered after a major failure, but the cause can be predicted in advance. If you see any of the following signs, you should immediately improve your design:

  • There is no key generation rule to distinguish identical requests, so retries and new requests cannot be distinguished.
  • The success response is not saved, so the results may vary upon retry.
  • Since the key storage TTL is shorter than the business guarantee time, duplicate reflection occurs in late retries.

Idempotency is not a simple key check, but a state machine. Separating IN_PROGRESS, SUCCEEDED, and FAILED_RETRIABLE makes it easier to respond to failures.

Key concepts

perspectiveDesign criteriaVerification points
key generationClient generates UUID per unit operationDuplication rate and conflict rate
save stateHash request body + store response bodyResponse consistency across retries
TTL policyStay above guaranteed business hoursWhether to block late retries
ObservabilityPer-key log/metric collectionDuplicate request detection time

The key is to specify the contract at the API level. If only the server is implemented and the client guide is omitted, the duplication problem will recur.

Code Example 1: Idempotency Repository Interface

export type IdempotencyRecord = {
  key: string;
  requestHash: string;
  status: "IN_PROGRESS" | "SUCCEEDED" | "FAILED_RETRIABLE";
  responseCode?: number;
  responseBody?: string;
  expiresAt: Date;
};

export interface IdempotencyStore {
  begin(record: Omit<IdempotencyRecord, "status">): Promise<"CREATED" | "EXISTS">;
  complete(key: string, responseCode: number, responseBody: string): Promise<void>;
  find(key: string): Promise<IdempotencyRecord | null>;
}

Code example 2: API handler processing routine

export async function createOrderHandler(req: Request) {
  const key = req.headers.get("Idempotency-Key");
  if (!key) return new Response("Missing Idempotency-Key", { status: 400 });

  const body = await req.json();
  const requestHash = await sha256(JSON.stringify(body));
  const started = await idempotencyStore.begin({ key, requestHash, expiresAt: plusHours(24) });

  if (started === "EXISTS") {
    const record = await idempotencyStore.find(key);
    if (!record) return new Response("Conflict", { status: 409 });
    return new Response(record.responseBody, { status: record.responseCode ?? 202 });
  }

  const result = await orderUseCase.execute(body);
  await idempotencyStore.complete(key, 201, JSON.stringify(result));
  return new Response(JSON.stringify(result), { status: 201 });
}

Architecture flow

Mermaid diagram rendering...

Tradeoffs

  • Storage space is increased for response reuse, but it is cheaper than payment/order consistency costs.
  • It is safe to keep the key TTL long, but storage costs and GDPR deletion policies must be considered together.
  • Incorporating request hash verification increases security but increases CPU cost.

Cleanup

Idempotency-Key is a fail-safe in a retry-allowing system. By designing key generation rules, storage states, and response reuse policies together, business consistency can be maintained even in failure situations.

Image source

  • Cover: source link
  • License: CC BY-SA 4.0 / Author: Papapep
  • Note: After downloading the free license image from Wikimedia Commons, it was optimized to JPG at 1600px.

Comments