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

120 lines
11 KiB
Markdown

---
title: Brandur — Implementing Stripe-like Idempotency Keys in Postgres
source_type: company-tech-blog
url: https://brandur.org/idempotency-keys
archive_url:
status: raw
confidence: high
tags: [ca-idempotency, postgres, db-storage, atomic-phases, recovery-points, stripe]
related_projects: [ca-skeleton-operational-contract]
related_branches: [feature-rate-limit-idempotency-contract, feature-api-contract-baseline]
created: 2026-05-22
last_reviewed: 2026-05-27
---
# Brandur — Implementing Stripe-like Idempotency Keys in Postgres
> Layer: `raw/company-tech-blogs/` — 전 Stripe 엔지니어 개인 블로그 (engineering-blog 등급). Stripe 내부 구현 패턴을 일반화한 글로 Postgres 기반 idempotency 구현의 reference. **Stripe 공식 문서 아님 — best practice 단정 금지.**
> 검증된 요약은 `/ingest` 후 `wiki/concepts/`에 별도 작성.
## Parent / 활용 branch (필수)
| Branch | 이 자료가 정당화하는 결정 |
|---|---|
| [[raw/branch-notes/feature-rate-limit-idempotency-contract]] | DB table 기반 저장 + `locked_at` lock + reaper 의 reference 구현. ca-tmpl 의 200ms in-flight wait + 24h TTL 결정의 비교 base |
| [[raw/branch-notes/feature-api-contract-baseline]] | API contract surface 에 `Idempotency-Key` 의 fingerprint mismatch 정책 (Brandur 409, IETF 422) 비교 근거 |
| [[raw/project-notes/ca-skeleton-operational-contract]] | §13. API Contract Surface (Idempotency-Key) + §18. Control Plane Contract (Rate Limit/Idempotency) 의 DB-based 구현 reference |
## 컨텍스트
ca-tmpl 이 **"DB table 기반 저장 + 200ms in-flight wait"** 를 채택한 직접적 근거가 되는 구현 패턴. Redis 기반 저장(대안 6) vs DB 기반 저장 비교에 결정적 자료. atomic phase / recovery_point 모델은 단순 dedup 을 넘어 부분 실행 후 retry 복구까지 다룬다.
## 출처 / Source
- 원본 URL: https://brandur.org/idempotency-keys
- 아카이브 URL: (미수집)
- 저자 / 조직: Brandur Leach (전 Stripe 엔지니어, 개인 블로그)
- 발행일: 본문 명시 없음 (2017~2018 추정)
- 마지막 확인일: 2026-05-27
## 핵심 인용 / Key quotes (verbatim)
> [§Schema — locked_at] "locked_at: A field that indicates whether this idempotency key is actively being worked."
> [§Schema — params] "params: The input parameters of the request. This is stored mostly so that we can error if the user sends two requests with the same idempotency key but with different parameters."
> [§Unique constraint] "We've made `idempotency_key` unique, but across `(user_id, idempotency_key)` so that it's possible to have the same idempotency key for different requests as long as it's across different user accounts."
> [§Mismatched params] "Programs sending multiple requests with different parameters but the same idempotency key is a bug."
> [§Lock acquisition] "Only acquire a lock if the key is unlocked or its lock has expired because the original request was long enough ago."
> [§Reaper] "I'd suggest a threshold of about 72 hours so that even if a bug is deployed on Friday that errors a large number of valid requests, an app could still keep a record."
> [§Atomic phases] "An atomic phase is a set of local state mutations that occur in transactions between foreign state mutations. We say that they're atomic because we can use an ACID-compliant database to guarantee either all occur, or none."
## Claims Extracted / 추출된 주장
| Claim ID | Claim (이 자료가 직접 말하는 것) | Evidence quote | Strength | Applies to | Does not prove |
|---|---|---|---|---|---|
| BRANDUR-IDEMP-C1 | idempotency_keys 테이블에 `locked_at` 컬럼을 두어 키가 active 처리 중인지 표시 | [§Schema — locked_at] "locked_at: A field that indicates whether this idempotency key is actively being worked." | `engineering-blog` | Postgres 기반 idempotency 구현 | row-level FOR UPDATE lock 대신 컬럼 lock 을 쓰는 이유 (가시성, stale lock 정리)는 본 인용 범위 밖 |
| BRANDUR-IDEMP-C2 | `params` 컬럼에 request 입력을 저장하는 주 목적은 동일 키 + 다른 파라미터 요청을 error 로 반환하기 위함 | [§Schema — params] "params: The input parameters of the request. This is stored mostly so that we can error if the user sends two requests with the same idempotency key but with different parameters." | `engineering-blog` | DB-based fingerprint mismatch 정책 | mismatch 시 정확한 status code (409 vs 422) 는 본 인용에 없음 — Brandur 본문 다른 곳에서 409 언급 |
| BRANDUR-IDEMP-C3 | unique 제약은 `(user_id, idempotency_key)` 2-tuple — 다른 user 면 같은 키 허용 | [§Unique constraint] "We've made `idempotency_key` unique, but across `(user_id, idempotency_key)` so that it's possible to have the same idempotency key for different requests as long as it's across different user accounts." | `engineering-blog` | per-user scope 의 idempotency | endpoint/method 까지 분리하지 않는 이유는 본 인용에 없음. Stripe 자체의 운영 정책과 다를 수 있음 (Stripe 공식 문서 확인 필요) |
| BRANDUR-IDEMP-C4 | 동일 키로 다른 파라미터 요청은 client 측 버그로 명시 | [§Mismatched params] "Programs sending multiple requests with different parameters but the same idempotency key is a bug." | `engineering-blog` | client retry 정책 설계 | 모든 vendor 가 동일하게 취급한다는 뜻은 아님 (IETF draft 는 422 권고, Toss 는 명시 없음) |
| BRANDUR-IDEMP-C5 | lock 획득 조건은 (a) 해제 상태이거나 (b) 충분히 오래 전 요청이라 lock 이 만료된 경우만 | [§Lock acquisition] "Only acquire a lock if the key is unlocked or its lock has expired because the original request was long enough ago." | `engineering-blog` | `locked_at` 기반 stale lock 회수 메커니즘 | lock 만료 기준 시간 (예: 90초, 5분 등) 의 정확한 값은 인용 범위에 없음 |
| BRANDUR-IDEMP-C6 | reaper 의 keep threshold 권장값은 **약 72시간** — 금요일 버그 배포 대비 | [§Reaper] "I'd suggest a threshold of about 72 hours so that even if a bug is deployed on Friday that errors a large number of valid requests, an app could still keep a record." | `engineering-blog` | DB-based idempotency 의 reaper 운영 | 72시간이 모든 도메인의 표준이라는 뜻은 아님. Toss 15일, Stripe v2 30일, ca-tmpl 24h 등 다양 |
| BRANDUR-IDEMP-C7 | atomic phase = "foreign state mutation 사이에 일어나는 local state mutation 의 집합" 으로 ACID DB 가 all-or-none 을 보장 | [§Atomic phases] "An atomic phase is a set of local state mutations that occur in transactions between foreign state mutations. We say that they're atomic because we can use an ACID-compliant database to guarantee either all occur, or none." | `engineering-blog` | 외부 API 호출이 끼어드는 결제 등 도메인의 recovery 모델 | 모든 비즈니스 로직이 atomic phase 모델에 적합하다는 뜻은 아님. 외부 호출이 없거나 idempotent 한 작업은 과한 설계 |
## Usage Boundaries / 적용 경계
- **이 자료가 직접 증명하는 것**:
- `BRANDUR-IDEMP-C1` ~ `C7`: Postgres 기반 idempotency 구현의 schema, lock 메커니즘, reaper 권장 시간, atomic phase 모델
- **이 자료가 증명하지 않는 것**:
- Stripe 의 실제 internal 구현이 본 글과 동일한지 (저자는 전 Stripe 엔지니어이지만 본 글은 일반화된 패턴, Stripe 공식 문서 아님)
- Redis 기반 구현이 부적절하다는 결론 (본 글은 DB 기반만 다룸 — 비교 결론은 별도 자료 필요)
- 72시간 reaper 가 모든 도메인의 표준 (vendor 별로 24h~30일 다양)
- `locked_at` 컬럼 lock 이 Redlock 등 distributed lock 보다 안전하다는 일반 결론
- **내 프로젝트에 적용하려면 추가 확인이 필요한 것**:
- ca-tmpl 의 3-tuple `(principal, key, useCaseName)` 과 Brandur 의 2-tuple `(user_id, idempotency_key)` 매핑 시 useCaseName 이 endpoint 분리 역할을 충분히 하는지
- ca-tmpl 의 200ms wait 가 Brandur 의 `locked_at` 만료 모델과 호환되는 구현인지 (wait timeout vs lock expiry 별개)
- fingerprint mismatch 시 ca-tmpl 의 422 vs Brandur 의 409 — IETF draft 와 비교한 표준 정합성
## 메모 / Notes
> 검증되지 않은 내 해석은 여기에 두지 말 것 — wiki source-summary 단계에서.
- **key scope (어떤 dimension으로)**: `(user_id, idempotency_key)` — Stripe v1 pair scope 의 구체 구현으로 보임 (Stripe 공식 문서로 corroborate 필요). ca-tmpl 의 `(principal, key, useCaseName)` 는 여기에 endpoint dimension 을 추가한 형태.
- **TTL**: 권장 72시간 (`C6`). ca-tmpl 24h 는 더 짧음.
- **저장소**: Postgres 테이블. Redis 아님. → **결제·상태변경 도메인에서 Redis 보다 DB 가 선호되는 이유의 reference (단 engineering-blog 등급)**.
- **duplicate 처리**:
- 완료된 동일 key → response_code/body 그대로 replay (블로그 본문에서 별도 설명).
- in-flight → `locked_at` 으로 차단 (`C5`). lock 만료 시 재시도 가능.
- **fingerprint (same key, different body)**: `request_params` JSONB 비교 → 다르면 **409 Conflict** (블로그 본문). Brandur 409, IETF/ca-tmpl 422. 코드 차이만 있고 사상은 같음.
- **recovery points**: 단순 dedup 을 넘어 "atomic phase" 모델 (`C7`) 로 **부분 실행 후 retry 복구**까지 다룸. STARTED → RIDE_CREATED → CHARGE_CREATED → FINISHED 같은 상태 머신.
- **장점 (블로그 본문 + 추론)**:
- 트랜잭션과 같은 DB 안에 있어 결제 정합성과 한 단위로 묶임 (Redis 면 별도 정합성 관리 필요).
- atomic phase 로 외부 호출(charge 등) 중간 실패도 안전한 retry 가능.
- 운영 가시성 (SQL 로 키 조회·디버깅).
- **단점 (추론, 미검증)**:
- Redis 대비 처리량/latency 손해.
- 테이블 비대화 → 인덱스/Vacuum 운영 비용. Reaper 필수.
- lock 컬럼 기반이라 connection-level lock 보다 가시성은 좋으나 stale lock 위험 (만료 정책 필수).
- **ca-tmpl 과의 차이**:
- 저장소 선택 (DB) = 일치.
- lock 모델: Brandur `locked_at` 컬럼 = ca-tmpl 200ms wait 의 기반 메커니즘. ca-tmpl 이 wait timeout 을 짧게 잡아 client 친화 + 좀비 lock 위험을 줄임.
- scope: Brandur 2-tuple vs ca-tmpl 3-tuple. ca-tmpl 이 endpoint(useCase) 까지 분리하여 더 안전.
- fingerprint mismatch status: Brandur 409 vs ca-tmpl 422. **IETF draft 는 422 를 권하므로 ca-tmpl 이 더 표준 정합적**.
- TTL: Brandur 72h vs ca-tmpl 24h → ca-tmpl 이 더 짧음 (스토리지·공격면 측면에서 보수적).
## Related / 관련
- 같은 주제 다른 raw:
- [[raw/company-tech-blogs/idempotency-toss-payments-techblog]] — vendor official 비교 (4-tuple, 15일 TTL, 409 in-flight)
- [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]] — Redis vs DB 저장소 trade-off
- 인용하는 branch:
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]]
- [[raw/branch-notes/feature-api-contract-baseline]]
- 인용하는 project:
- [[raw/project-notes/ca-skeleton-operational-contract]] (§13, §18)
- 인용한 wiki 요약: (미작성)