Files
llm-wiki/raw/company-tech-blogs/brandur-stripe-idempotency-keys.md
T

107 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "company-tech-blog / Brandur Leach — Implementing Stripe-like Idempotency Keys (Client-Generated Key vs Server-Assigned Resource ID)"
source_type: company-tech-blog
url: https://brandur.org/idempotency-keys
archive_url:
related_branches: [feature-resource-identifier-contract]
related_projects: [ca-skeleton]
tags: [company-tech-blog, ca-skeleton, api-design, idempotency, resource-identifier, public-id-separation]
created: 2026-05-31
---
# Brandur Leach — Implementing Stripe-like Idempotency Keys (Client-Generated Key vs Server-Assigned Resource ID)
> Layer: `raw/company-tech-blogs/` — 전 Stripe 엔지니어 Brandur Leach 의 개인 기술 블로그. Stripe 내부 idempotency 구현 패턴을 일반화한 글. **Stripe 공식 문서 아님** — `engineering-blog` 등급 적용. best practice 단정 금지. 가장 널리 인용되는 idempotency key 구현 레퍼런스.
>
> **이 파일의 초점**: `feature-resource-identifier-contract` 의 D4 (ID 생성 책임) · D14 (Idempotency-Key vs Resource ID 구분) · D11 (Public ID vs Internal Sequence 분리) 결정 정당화. Postgres/DB 구현 상세(locked_at, atomic phase 등)는 [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]] 에 별도 보관.
## Parent / 활용 branch (필수)
| Branch | 이 자료가 정당화하는 결정 |
|---|---|
| [[raw/branch-notes/feature-resource-identifier-contract]] | D4 (ID 생성 책임): `Idempotency-Key`*client-generated*, resource ID 는 *server-assigned* — 두 책임의 원천이 다름을 원문으로 뒷받침. D14 (Idempotency-Key vs Resource ID): 명시적 분리 + 수명주기 차이 + 형식 무관 조합 허용. D11 (Public ID vs Internal Sequence): Stripe 가 external-only (단일 public ID) 패턴을 쓰고 idempotency key 를 별도 레이어로 두는 사례 |
## 출처 / Source
- 원본 URL: https://brandur.org/idempotency-keys
- 아카이브 URL: (미수집)
- 저자 / 조직: Brandur Leach (전 Stripe 엔지니어, 개인 기술 블로그 brandur.org)
- 발행일: 본문 명시 없음 (2017~2018 추정)
- 마지막 확인일: 2026-05-31
## 왜 저장했는지 / Why archived
`Idempotency-Key`*client-generated* 임을 원문으로 확인하고, server-assigned resource ID 와의 명시적 분리를 `feature-resource-identifier-contract` (D4/D14/D11) 의 근거로 삼기 위해 보관. 기존 `idempotency-brandur-stripe-postgres.md` 가 DB/Postgres 구현에 초점을 두는 반면, 본 파일은 **ID 생성 책임의 주체(client vs server) 와 수명주기 분리**에 초점.
## 핵심 인용 / Key quotes (verbatim, 3~5개)
> [§HTTP header example] "A common way to transmit an idempotency key is through an HTTP header:"
> (원문 코드 예시: `Idempotency-Key: 0ccb7813-e63d-4377-93c5-476cb93038f3`)
> — line 4 in fetched text
> [§Key definition] "An idempotency key is a unique value that's generated by a client and sent to an API along with a request."
> — line 12 in fetched text
> [§Key format hint] "something with good randomness like a UUID"
> — line 14 in fetched text
> [§Key TTL] "Keys are not meant to be used as a permanent request archive but rather as a mechanism for ensuring near-term correctness. Servers should recycle them out of the system beyond a horizon where they won't be of much use say 24 hours or so."
> — line 16 in fetched text
> [§Fingerprint / params mismatch] "Programs sending multiple requests with different parameters but the same idempotency key is a bug."
> — line 20 in fetched text
## Claims Extracted / 추출된 주장
> 이 자료가 **직접 말하는 것만** claim 으로 분리한다. Stripe 공식 문서 아님 — `engineering-blog` strength 이상으로 격상 금지.
| Claim ID | Claim (이 자료가 직접 말하는 것) | Evidence quote | Strength | Applies to | Does not prove |
|---|---|---|---|---|---|
| BRANDUR-IDEMP-C8 | `Idempotency-Key`*client* 가 생성해서 API 요청과 함께 전송하는 unique value 임 | [§Key definition] "An idempotency key is a unique value that's generated by a client and sent to an API along with a request." | `engineering-blog` | Idempotency-Key HTTP header 의 생성 책임이 client 에 있음 (D4 근거) | server 가 Idempotency-Key 를 생성하면 안 된다는 규범적 금지 규칙 (이 블로그는 권고 사례이지 표준이 아님) |
| BRANDUR-IDEMP-C9 | Idempotency-Key 의 포맷은 "UUID 처럼 난수성이 높은 것" 을 권장 | [§Key format hint] "something with good randomness like a UUID" | `engineering-blog` | key 포맷 선택 가이드 (D4 / D14 보조) | UUID v4 만 허용된다는 뜻이 아님. ULID / NanoID 등 다른 포맷도 동등하게 사용 가능 |
| BRANDUR-IDEMP-C10 | Idempotency-Key 의 TTL 은 영구 보관이 아닌 단기 정확성 보장 용도이며, 24시간 정도가 적절 | [§Key TTL] "Keys are not meant to be used as a permanent request archive but rather as a mechanism for ensuring near-term correctness. Servers should recycle them out of the system beyond a horizon where they won't be of much use say 24 hours or so." | `engineering-blog` | idempotency key TTL 정책 설계 (D14 의 수명주기 차이 근거) | resource ID 의 수명주기 (persistent, 영구) 와의 차이를 *명시적으로* 비교하지는 않음 — 대조 추론은 wiki/concepts 에서 |
| BRANDUR-IDEMP-C11 | Idempotency-Key 는 HTTP header 로 전송하는 것이 일반적 패턴 | [§HTTP header example] "A common way to transmit an idempotency key is through an HTTP header" (코드 예시: `Idempotency-Key: 0ccb7813-e63d-4377-93c5-476cb93038f3`) | `engineering-blog` | HTTP API 에서 idempotency key 전달 방식 (D14 분리 근거) | 이것이 유일한 전송 방법이라는 뜻은 아님 (query param / body 전달도 기술적으로 가능) |
| BRANDUR-IDEMP-C12 | 동일 key + 다른 request params 요청은 client 측 버그로 명시 — 서버는 이를 거부해야 함 | [§Fingerprint / params mismatch] "Programs sending multiple requests with different parameters but the same idempotency key is a bug." | `engineering-blog` | request fingerprint 비교 정책 (D14 보조 — idempotency key 와 request 내용의 결합 의미) | 거부 시 HTTP status code (409 vs 422) 는 이 인용에 없음 (Brandur 는 409 사용, IETF draft 는 422 권고) |
### Strength 허용값 (참고)
- `engineering-blog` — 개인/팀 블로그의 엔지니어링 해설 (본 자료의 등급)
## Usage Boundaries / 적용 경계
- **이 자료가 직접 증명하는 것**:
- `BRANDUR-IDEMP-C8`: Idempotency-Key 가 client-generated 임 (D4 의 "Idempotency-Key 는 client 생성" 근거)
- `BRANDUR-IDEMP-C9`: UUID 같은 난수 포맷 권장 (D4/D14 포맷 가이드)
- `BRANDUR-IDEMP-C10`: Idempotency-Key TTL 이 단기(~24h)이고 영구 보관이 아님 (D14 의 수명주기 차이)
- `BRANDUR-IDEMP-C11`: Idempotency-Key 가 HTTP header 로 전달됨 — resource ID 는 response body / URL path 에 위치 (D14 분리의 물리적 근거)
- `BRANDUR-IDEMP-C12`: 같은 key + 다른 params = client bug — fingerprint 검사 의무 (D14 보조)
- **이 자료가 증명하지 않는 것**:
- Stripe 의 resource ID 와 idempotency key 를 *명시적으로 대조* 한 서술은 없음 — 원문은 idempotency key 만 집중 서술. resource ID 의 분리는 구조적 추론.
- D11 (Public ID vs Internal Sequence): 원문은 Stripe 가 single public UUID 만 쓴다고 명시하지 않음 — Stripe 공식 API docs 로 보강 필요.
- HTTP status 409 vs 422 의 표준 적합성 — IETF draft 별도 확인 필요.
- 이 자료는 `engineering-blog` 등급 — "Stripe 공식 best practice" 로 표현 금지.
- **내 프로젝트에 적용하려면 추가 확인이 필요한 것**:
- ca-skeleton 의 `Idempotency-Key` 24h TTL 이 Brandur 의 "~24 hours or so" 와 일치하는지 (ca-tmpl 결정에서는 24h 채택 — 이 인용이 direct support 가능).
- Idempotency-Key 포맷 (UUID v4 권장) 과 resource ID 포맷 (ULID/UUID v7 — D1 결정) 의 *다른 형식 조합* 허용 여부 — 이 블로그는 조합에 제약을 두지 않음 (UNSUPPORTED_DECISION 여지 없음).
- `feature-resource-identifier-contract` D11 (external-only vs dual) 에 대한 Stripe 사례 뒷받침은 Stripe 공식 API doc 별도 보강 권고.
## 메모 / Notes
- `BRANDUR-IDEMP-C8~C12` 는 기존 `idempotency-brandur-stripe-postgres.md``C1~C7` 과 Claim ID 연번 충돌 없이 설계됨 (같은 PREFIX 의 다른 raw file 이므로 연번 구분 필요 — 이 파일의 claims 은 C8 부터).
- D14 (Idempotency-Key vs Resource ID 구분) 에서 이 자료가 직접적인 *대조* 서술은 제공하지 않음. 그러나 `C8` (client-generated) + `C10` (TTL ~24h) + `C11` (HTTP header 전달) 을 조합하면 resource ID (server-assigned, persistent, URL path) 와의 대조 추론이 가능 — 이 추론은 wiki/concepts 또는 branch decision note 에서만 서술.
- 포맷 조합 자유도 (`C9`): idempotency key 는 UUID v4, resource ID 는 ULID 의 조합이 이 블로그의 내용과 충돌하지 않음.
- Claim C8 이 D4 의 핵심 direct evidence. "client-generated" 한 단어가 ID 생성 책임 결정의 분기점.
## Related / 관련
- 같은 URL 의 다른 초점 raw (DB/Postgres 구현):
- [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]] — locked_at, atomic phase, recovery point, reaper 72h, scope (user_id, key) 등 구현 상세 (C1~C7)
- 같은 주제 다른 raw:
- [[raw/company-tech-blogs/idempotency-toss-payments-techblog]] — Toss Payments 4-tuple scope + 15일 TTL + 409 in-flight
- [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]] — Redis vs DB 저장소 trade-off
- 관련 branch:
- [[raw/branch-notes/feature-resource-identifier-contract]] — D4/D14/D11 결정 (이 자료의 primary consumer)
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] — Idempotency-Key 운영 계약 SSOT
- [[raw/branch-notes/feature-api-contract-baseline]] — fingerprint mismatch 응답 코드 정책
- 인용한 wiki 요약: (미작성)