# Stripe: Design de API e Chaves de Idempotência

Uma análise técnica aprofundada de como a Stripe resolveu o problema de retentativas seguras em APIs de pagamento usando chaves de idempotência, armazenamento de estado de requisição e rate limiting — decisões de design que se tornaram referência para a indústria.

- URL: https://fernando.moretes.com/studies/stripe-api-idempotency

- Markdown: https://fernando.moretes.com/studies/stripe-api-idempotency/study.md?lang=pt

- Type: Teardown

- Company: Stripe

- Domain: API/Pagamentos

- Date: 2017-02-22

- Tags: api-design, idempotency, payments, stripe, distributed-systems, rate-limiting, reliability, fintech

- Reading time: 8 min

---

Toda API de pagamento enfrenta um problema fundamental: redes falham, timeouts acontecem, e o cliente nunca sabe se a cobrança foi processada ou não. A Stripe construiu uma solução elegante e rigorosa para esse problema — e documentou publicamente. Este teardown reconstrói a arquitetura, examina as decisões de design e avalia onde as escolhas fazem sentido e onde eu faria diferente.

## Ficha Técnica

- **Empresa:** Stripe
- **Domínio:** API de Pagamentos / Infraestrutura Financeira
- **Publicação do post original:** 2018 (Stripe Engineering Blog)
- **Stack principal:** Ruby, API REST, Redis (estado de idempotência), PostgreSQL, HTTPS/TLS
- **Escala:** Centenas de bilhões de dólares processados anualmente; milhões de requisições/dia de parceiros de integração
- **Problema central:** Garantir que uma requisição de pagamento executada mais de uma vez produza exatamente o mesmo efeito que executada uma vez
- **Mecanismo chave:** Chave de idempotência enviada pelo cliente no header HTTP, persistida no servidor com resultado associado

## O Problema: Dinheiro em Trânsito e Redes Não Confiáveis

Pagamentos são transações com consequências reais e assimétricas. Se você cobra um cliente duas vezes por um único pedido, você criou um problema de negócio sério — reembolsos, disputa de chargeback, erosão de confiança. Se você não cobra quando deveria, perdeu receita. O problema não é teórico: em sistemas distribuídos, a entrega de uma mensagem tem três estados possíveis — entregue, não entregue, ou **desconhecido**. É esse terceiro estado que mata sistemas de pagamento ingênuos.

O cenário clássico: um cliente faz uma requisição `POST /charges` para criar uma cobrança. A requisição chega ao servidor da Stripe, o processamento começa, a cobrança é criada na rede de cartões — e então a conexão cai antes que a resposta chegue ao cliente. O cliente recebeu um timeout. O que ele deve fazer? Se ele simplesmente retentativa a requisição, ele pode cobrar o usuário duas vezes. Se ele não retenta, pode perder a venda e deixar o usuário sem o serviço que pagou.

Esse problema é amplificado pelo contexto da Stripe: ela é uma plataforma de infraestrutura. Seus clientes são desenvolvedores e empresas construindo produtos em cima da API. Qualquer solução precisa funcionar de forma **confiável e previsível** para milhares de integrações diferentes, muitas escritas por engenheiros que não são especialistas em sistemas distribuídos. A solução não pode depender de o cliente fazer algo sofisticado — ela precisa ser simples de usar corretamente e difícil de usar errado.

## Arquitetura Reconstruída: Fluxo de Idempotência na API Stripe

Fluxo de uma requisição POST /charges com chave de idempotência, incluindo os caminhos de sucesso, duplicata e falha.

### 👤 Client

- API Client (SDK / HTTP) (user)

### 🌐 Edge / Auth

- TLS Termination + Auth (edge)
- Rate Limiter (per key / IP) (security)

### ⚙️ API Layer

- API Server (charge handler) (compute)
- Idempotency Middleware (compute)

### 🗄️ State Store

- Redis (idempotency keys + locks) (data)
- PostgreSQL (charges, events, result payload) (storage)

### 💳 External

- Card Network (Visa/Mastercard) (external)

### 📬 Async

- Job Queue (webhooks / events) (messaging)
- Webhook Delivery (retry w/ backoff) (compute)

### Fluxos

- client -> tls: POST /charges
Idempotency-Key: uuid
- tls -> ratelimit: autenticado
- ratelimit -> idem_check: dentro do limite
- idem_check -> redis: busca chave
(GET idem_key)
- redis -> idem_check: HIT → retorna
resultado cacheado
- idem_check -> api: MISS → processa
nova requisição
- api -> redis: SET lock
(atomic)
- api -> postgres: persiste charge
+ idem record
- api -> cardnet: autorização
cartão
- cardnet -> api: aprovado/negado
- api -> postgres: atualiza resultado
+ status final
- api -> redis: SET resultado
(TTL 24h)
- api -> queue: enfileira evento
charge.succeeded
- queue -> webhook: entrega async
- webhook -> client: POST webhook
(retry backoff)
- ratelimit -> client: 429 Too Many
Requests

## Como Funciona: A Mecânica das Chaves de Idempotência

A solução da Stripe é elegante em sua simplicidade aparente, mas há profundidade considerável nos detalhes de implementação.

**O contrato básico**: o cliente gera um UUID único por *intenção de operação* e o envia no header `Idempotency-Key`. Se a mesma chave for enviada novamente dentro de uma janela de 24 horas, o servidor retorna exatamente o mesmo resultado da primeira execução — sem reprocessar, sem cobrar novamente. A chave é vinculada ao par `(api_key, idempotency_key)`, não apenas à chave de idempotência isolada, o que evita colisões entre contas diferentes.

**O fluxo no servidor**: ao receber uma requisição, o middleware de idempotência faz um `GET` atômico no Redis pela chave composta. Se encontrar um resultado completo, retorna imediatamente com o HTTP status original e o payload original — o cliente não consegue distinguir se foi a primeira execução ou uma retentativa. Se não encontrar nada, adquire um lock distribuído (via `SET NX` no Redis) para garantir que requisições concorrentes com a mesma chave não processem em paralelo — isso é crítico para evitar race conditions onde dois processos simultâneos tentam criar a mesma cobrança.

**Estados intermediários**: o sistema precisa lidar com o caso em que uma requisição está *em processamento* quando uma segunda chega com a mesma chave. A Stripe retorna um `409 Conflict` nesse caso, sinalizando ao cliente que a operação original ainda está em andamento. Isso é semanticamente correto e evita que o cliente interprete o 409 como uma falha de negócio.

**Persistência do resultado**: quando o processamento completa — com sucesso ou com erro de negócio (cartão recusado, por exemplo) — o resultado é persistido tanto no PostgreSQL (como parte do registro da cobrança) quanto no Redis com TTL de 24 horas. Erros de negócio também são idempotentes: se a primeira tentativa resultou em `card_declined`, a segunda tentativa com a mesma chave retorna o mesmo `card_declined` sem tentar novamente na rede de cartões. Isso é uma decisão de design importante e não óbvia — discutirei no callout.

**Retentativas com backoff exponencial**: o SDK oficial da Stripe implementa retentativas automáticas com backoff exponencial e jitter para requisições que retornam 5xx ou timeout de rede. A chave de idempotência é gerada uma vez e reutilizada em todas as retentativas da mesma operação. Isso transforma o problema de "como garantir exatamente uma execução" em "como garantir pelo menos uma execução com resultado idempotente" — uma mudança de perspectiva fundamental.

## Rate Limiting: A Camada de Proteção que Completa o Quadro

Idempotência resolve o problema de segurança das retentativas, mas não resolve o problema de volume. Um cliente com um bug pode enviar milhares de requisições por segundo — com ou sem chaves de idempotência corretas. O rate limiting é a camada que protege a infraestrutura e garante fairness entre clientes.

A Stripe implementa rate limiting em múltiplas dimensões: por chave de API (conta), por endpoint, e globalmente. A granularidade por endpoint é importante: um endpoint de leitura como `GET /charges` tem limites mais generosos que um endpoint de escrita como `POST /charges`, porque o custo de processamento e o risco de efeitos colaterais são diferentes.

O mecanismo técnico mais comum para rate limiting em alta escala é o **token bucket** ou **sliding window counter** no Redis. A Stripe não documenta publicamente qual algoritmo usa, mas o comportamento observável — limite de 100 requisições por segundo por chave de API para a maioria dos endpoints em produção — é consistente com um sliding window. O header de resposta `Retry-After` em respostas 429 é um sinal de design importante: ele diz ao cliente *quando* pode tentar novamente, transformando um erro em informação acionável.

Um detalhe crítico na interação entre idempotência e rate limiting: uma requisição bloqueada por rate limit **não consome a chave de idempotência**. Isso é semanticamente correto — o cliente foi impedido de tentar, então a chave permanece disponível para quando ele puder tentar novamente. Se o rate limiter consumisse a chave, o cliente ficaria preso: bloqueado de tentar e com a chave "queimada" para aquela operação.

A Stripe também documenta a prática de **idempotência em webhooks**: o sistema de entrega de webhooks pode entregar o mesmo evento mais de uma vez (pelo menos uma vez de entrega), e os clientes são orientados a usar o `event.id` como chave de idempotência no processamento. Isso fecha o loop: a API é idempotente de entrada, e o sistema de notificação é idempotente de saída.

## Matriz de Decisões: Trade-offs Centrais do Design

### Idempotência gerenciada pelo servidor (abordagem Stripe)

**Pros**
- Funciona com qualquer cliente HTTP — sem lógica especial necessária além de gerar e reutilizar um UUID
- Resultado determinístico independente do número de retentativas
- Protege contra bugs no cliente (loop de retentativas, reconexões agressivas)

**Cons**
- Requer armazenamento de estado no servidor (Redis + PostgreSQL) — custo operacional e de latência
- TTL de 24h cria uma janela de inconsistência se o cliente perder o UUID original
- Erros de negócio idempotentes podem surpreender desenvolvedores (card_declined não é retriável com mesma chave)

**Verdict:** Escolha correta para uma plataforma de infraestrutura com milhares de integradores de diferentes níveis de sofisticação

### Idempotência gerenciada pelo cliente (design alternativo)

**Pros**
- Sem estado adicional no servidor
- Mais flexível para clientes sofisticados que querem controle total

**Cons**
- Requer que cada integrador implemente lógica de deduplicação corretamente — fonte de bugs em produção
- Impossível de auditar ou debugar do lado do servidor
- Não escala como padrão de plataforma

**Verdict:** Inadequado para uma API pública de pagamentos — transfere complexidade para onde ela causa mais dano

### Operações totalmente idempotentes por design (ex: PUT semântico)

**Pros**
- Sem necessidade de chave de idempotência — a operação em si é segura para repetir
- Modelo mental mais simples para operações de configuração (ex: atualizar dados de cliente)

**Cons**
- Impossível para criação de cobranças — cada cobrança é uma nova intenção financeira, não uma atualização de estado
- Requer modelagem de domínio diferente (recursos como estado vs. comandos como eventos)

**Verdict:** Complementar, não substituto — adequado para endpoints de recurso, não para endpoints de transação

## Leitura pelo AWS Well-Architected Framework

- **security**: **Forte.** A chave de idempotência é vinculada à chave de API, evitando que um ator mal-intencionado reutilize chaves de outro cliente. TLS obrigatório em todas as chamadas. O rate limiting por chave de API funciona como controle de abuso além de proteção de infraestrutura. Um ponto de atenção: chaves de idempotência são enviadas em headers HTTP — se logadas sem cuidado, podem vazar em sistemas de observabilidade. A Stripe não documenta explicitamente como trata isso.
- **reliability**: **Excelente — é o pilar central do design.** O sistema transforma falhas de rede em eventos recuperáveis sem risco de duplicação. A combinação de lock distribuído (Redis NX) + persistência de resultado (PostgreSQL) garante exactly-once semantics do ponto de vista do efeito de negócio, mesmo que a rede seja não confiável. O 409 Conflict para requisições concorrentes é uma escolha de confiabilidade consciente — prefere rejeitar a processar em paralelo.
- **performance**: **Bom com trade-offs conscientes.** O Redis como cache de resultado de idempotência adiciona uma ida de rede extra no caminho feliz (lookup antes de processar), mas o custo é justificado pelo benefício. Para requisições repetidas (retentativas), o Redis short-circuit evita toda a cadeia de processamento — incluindo a chamada à rede de cartões, que é a operação mais cara. O TTL de 24h é um balanço entre cobertura de janela de retry e uso de memória.
- **cost**: **Eficiente para o problema resolvido.** O custo incremental do Redis para armazenar chaves de idempotência é marginal comparado ao custo de processar cobranças duplicadas — tanto em infraestrutura quanto em consequências de negócio (chargebacks, suporte). O design evita reprocessamento desnecessário na rede de cartões, que tem custo por transação.
- **sustainability**: **Positivo.** Ao evitar reprocessamento desnecessário — especialmente chamadas à rede de cartões — o design reduz computação desperdiçada. O short-circuit no Redis para retentativas é mais eficiente energeticamente que reprocessar a pilha completa.

## A Decisão Não Óbvia: Erros de Negócio São Idempotentes

A decisão mais interessante — e mais debatível — no design da Stripe é que **erros de negócio também são idempotentes**. Se você envia uma requisição com a chave `idem_abc123` e o cartão é recusado (`card_declined`), uma segunda requisição com a mesma chave retorna o mesmo `card_declined` sem tentar novamente na rede de cartões.

A lógica por trás disso é coerente: a chave de idempotência representa uma *intenção específica* de cobrança. Se essa intenção resultou em recusa, a recusa é o resultado correto para aquela intenção. Se o cliente quer tentar novamente — talvez o usuário atualizou o cartão, ou quer tentar um cartão diferente — essa é uma *nova intenção*, que deve ter uma nova chave de idempotência.

Isso tem uma consequência importante para o design de fluxos de pagamento: o cliente precisa entender a diferença entre **erros retriáveis** (5xx, timeout de rede) e **erros não retriáveis** (erros de negócio como `card_declined`, `insufficient_funds`). Para erros retriáveis, reutilize a chave. Para erros de negócio, gere uma nova chave se quiser tentar novamente com condições diferentes.

Essa distinção é poderosa mas requer educação dos integradores. A Stripe investe pesadamente em documentação e SDKs que encapsulam essa lógica — o SDK Python, por exemplo, automaticamente retenta apenas em erros de rede e 5xx, nunca em 4xx. Isso é design de plataforma: a decisão arquitetural correta implementada de forma que o comportamento padrão seja o comportamento seguro.

> **O que eu faria diferente — e o que levaria para qualquer sistema financeiro:** O design da Stripe é sólido e eu endossaria a maioria das escolhas sem hesitação. Mas há três pontos onde eu pensaria diferente:

**1. TTL de idempotência configurável por tipo de operação.** 24 horas é uma escolha razoável para a maioria dos casos, mas em sistemas financeiros com janelas de processamento específicas (ex: liquidação intraday, fechamento de lote), um TTL fixo pode ser muito curto ou muito longo dependendo do contexto. Eu exporia um mecanismo para o cliente declarar a janela de retry esperada — ou internamente, variaria o TTL por tipo de endpoint e criticidade da operação.

**2. Separação explícita entre "lock de processamento" e "cache de resultado".** O Redis serve dois propósitos no design: lock distribuído durante o processamento e cache de resultado após. Misturar esses dois papéis no mesmo store cria acoplamento operacional — uma falha no Redis durante o processamento pode deixar locks órfãos. Em sistemas de alta criticidade, eu usaria stores separados com TTLs e políticas de eviction diferentes, ou usaria o PostgreSQL como fonte de verdade para o estado de idempotência e o Redis apenas como cache de leitura rápida.

**3. Observabilidade de idempotência como cidadão de primeira classe.** O design não documenta explicitamente métricas de idempotência — taxa de hits/misses, distribuição de retentativas por chave, tempo entre primeira tentativa e retentativa bem-sucedida. Em qualquer sistema financeiro que opero, essas métricas são dashboards de primeira linha. Um aumento na taxa de retentativas é um sinal precoce de degradação de rede ou bugs em integrado

## Veredicto

O design de idempotência da Stripe é um caso de estudo de como resolver um problema genuinamente difícil de forma que o resultado pareça óbvio em retrospecto — o sinal mais confiável de bom design de engenharia. A combinação de chaves de idempotência gerenciadas pelo servidor, lock distribuído para concorrência, persistência de resultados incluindo erros de negócio, e SDKs que encapsulam o comportamento correto por padrão cria um sistema que é simultaneamente robusto para a infraestrutura da Stripe e seguro para integradores de todos os níveis de sofisticação.

O que torna este design especialmente valioso como estudo é que ele não é específico para pagamentos. Os mesmos princípios se aplicam a qualquer operação com efeitos colaterais em sistemas distribuídos: envio de e-mail, provisionamento de recursos, transferências financeiras, criação de pedidos. A idempotência não é uma feature de API — é uma propriedade de sistema que precisa ser projetada desde o início.

Os pontos onde eu divergiria — TTL configurável, separação de stores, observabilidade de primeira classe — são refinamentos para contextos de maior criticidade ou maior complexidade operacional. Para uma API pública com m

## Referências

- [Stripe — Designing robust and predictable APIs with idempotency](https://stripe.com/blog/idempotency)

## Fontes do caso

- [Stripe — Designing robust and predictable APIs with idempotency](https://stripe.com/blog/idempotency)
