Arquitetura real · Sênior
Stripe: APIs previsíveis com chaves de idempotência
Arquitetura real em produção. Pontos centrais: Operações Idempotentes, Fundamentos de Design de API e Padrões de Design para Nuvem.
Resposta curta
Arquitetura real em produção. Pontos centrais: Operações Idempotentes, Fundamentos de Design de API e Padrões de Design para Nuvem. Toda chamada entre dois nós atravessa uma rede, e redes falham de formas exóticas, mesmo as que são estatisticamente confiáveis. Entre um cliente e um servidor há três falhas distintas: a conexão inicial pode nem se estabelecer; a chamada pode cair…
01 · CONTEXTO
A resposta que não chega
Toda chamada entre dois nós atravessa uma rede, e redes falham de formas exóticas, mesmo as que são estatisticamente confiáveis. Entre um cliente e um servidor há três falhas distintas: a conexão inicial pode nem se estabelecer; a chamada pode cair no meio, enquanto o servidor cumpre a operação, deixando o trabalho em limbo; ou a chamada pode dar certo e a conexão cair antes de o servidor avisar o cliente.
Só a primeira falha é inofensiva: o cliente sabe que nada aconteceu e pode repetir. Nas outras duas o sucesso da operação é ambíguo do ponto de vista do cliente, que não sabe se repetir é seguro. Isso vale para qualquer sistema distribuído, e o artigo lembra que até a API do Stripe com um único outro servidor chamando-a já é um.
O cliente envia o pedido de cobrança ao servidor da API.
O servidor executa a operação e grava o efeito no banco: a cobrança de fato aconteceu.
A conexão cai antes de a resposta chegar. Do ponto de vista do cliente, nada indica se a cobrança foi feita.
Se o cliente repetir sem nenhuma proteção, o servidor executa de novo e a cobrança acontece em dobro.
A saída mais simples é projetar o servidor para que repetir seja sempre seguro. Um endpoint com essa propriedade se diz idempotente: pode ser chamado qualquer número de vezes com os efeitos colaterais ocorrendo uma só vez. Diante de qualquer erro, o cliente repete até ter sucesso verificável, e o estado dele converge com o do servidor.
02 · IDEMPOTENTE POR NATUREZA
Quando repetir já é seguro
O artigo usa como exemplo a API de um provedor de DNS hipotético que cria um subdomínio com um PUT. Toda a informação necessária para criar o registro vai na chamada, então o cliente pode invocá-la quantas vezes quiser; se o servidor percebe que é duplicata porque o domínio já existe, ignora o pedido e responde com sucesso. Pela semântica HTTP, PUT e DELETE são idempotentes, e o PUT cria ou substitui o recurso inteiro pelo conteúdo do corpo.
A documentação do Stripe aplica o mesmo raciocínio. Na API v1, GET e DELETE são idempotentes por definição: repeti-los não repete os efeitos da operação original, e mandar chave de idempotência neles não muda nada. Já as requisições POST, que mudam estado, aceitam a chave.
| Tipo de operação | Exemplo | Chave de idempotência? |
|---|---|---|
| PUT com o estado completo | Criar um registro DNS com todos os campos | Dispensável, repetir já é seguro |
| GET e DELETE | Ler ou apagar um recurso | Sem efeito, são idempotentes por definição |
| POST que muda estado | Cobrar um cliente | Necessária, repetir cobraria de novo |
03 · A CHAVE DE IDEMPOTÊNCIA
Exatamente uma vez, mesmo com repetição
E quando a operação precisa ocorrer uma única vez? O exemplo do artigo é um endpoint de cobrança: chamá-lo duas vezes por acidente cobraria o cliente em dobro. A chave de idempotência resolve isso. Ao fazer o pedido, o cliente gera um ID único que identifica só aquela operação e o envia junto do corpo normal; o servidor correlaciona o ID com o estado da requisição do seu lado. Se o cliente notar uma falha, repete com o mesmo ID, e cabe ao servidor decidir o que fazer.
| Falha | O que o servidor faz na repetição |
|---|---|
| Falha ao conectar | Vê o ID pela primeira vez e processa normalmente |
| Falha no meio da operação | Se um banco ACID desfez tudo, repete por inteiro; senão recupera o estado e continua |
| Falha na resposta | Responde com o resultado em cache da operação que deu certo |
Repare que o artigo não prescreve um único comportamento: o que acontece no caso do meio depende da implementação, e é por isso que a escolha de guardar o estado de cada requisição importa tanto.
O cliente gera uma chave única para a operação e a envia junto do corpo normal do pedido.
A camada de idempotência consulta o registro. A chave nunca foi vista, então o pedido segue para execução.
A operação roda e o servidor guarda no registro o status e o corpo da resposta, depois que a execução do endpoint começa (regra da API v1).
A resposta se perde no caminho e o cliente fica sem saber o resultado.
O cliente repete com a mesma chave. A camada encontra a resposta salva e a devolve, sem chamar a operação de novo.
Se a repetição chegasse com parâmetros diferentes dos originais, a camada recusaria com erro em vez de executar.
Quem gera a chave é o cliente. Como gerá-la fica por conta dele, mas a documentação sugere UUIDs v4 ou outra string aleatória com entropia suficiente para evitar colisões. As chaves têm até 255 caracteres e não devem carregar dados sensíveis, como e-mails ou identificadores pessoais. Uma segunda estratégia é derivar a chave de um objeto ligado ao usuário, como o ID de um carrinho de compras, o que protege de forma direta contra envio duplo.
04 · O QUE O SERVIDOR DECIDE
Executar, devolver ou recusar
Para cada repetição o servidor precisa escolher entre devolver o que já tem, recusar ou executar de verdade. A documentação do Stripe define os casos da API v1, que o quadro abaixo reúne.
| Situação da repetição | O que acontece |
|---|---|
| Mesma chave e mesmos parâmetros, depois de a execução começar | Devolve o status e o corpo salvos da primeira requisição |
| Mesma chave com parâmetros diferentes | Erro, para evitar uso indevido da chave |
| Parâmetros inválidos ou conflito com outra requisição em execução | Nada é salvo, e a requisição pode ser repetida |
| Conflito com outra requisição, possivelmente pela mesma chave | Resposta com status 409, conflito |
| Chave já removida e reutilizada | Pode executar uma nova requisição |
O cliente consegue distinguir uma resposta repetida pelo servidor procurando o cabeçalho Idempotent-Replayed com valor verdadeiro.
05 · CICLO DE VIDA DA CHAVE
Janela de repetição e reconciliação
A chave não vive para sempre. O Stripe retém as chaves por pelo menos 24 horas e pode removê-las depois disso, e reutilizar uma chave removida pode executar uma nova requisição. Por isso a orientação é limitar as repetições de uma operação não resolvida às primeiras 24 horas depois da requisição original, sempre com a mesma chave e os mesmos parâmetros.
Se o resultado continua desconhecido depois dessa janela, não se repete nem com a chave antiga nem com uma nova. Em vez disso, busca-se o objeto relevante, inspeciona-se a requisição no painel e usam-se eventos de webhook para reconciliar o resultado.
Outra regra prática: se as repetições atravessam chamadas separadas do SDK ou reinícios de processo, a aplicação deve gerar e persistir a chave da operação e fornecê-la a cada tentativa.
Antes da primeira tentativa, a aplicação gera a chave da operação e a persiste.
O pedido fica sem resposta. Enquanto a janela permite, a aplicação repete com a mesma chave e os mesmos parâmetros.
Se o processo reinicia no meio, a aplicação lê a chave persistida em vez de criar outra.
Passada a janela sem resultado conhecido, a aplicação para de repetir, nem com a chave antiga nem com uma nova.
A reconciliação busca o objeto e lê os eventos de webhook, casando o que aconteceu com os registros locais.
06 · REPETIR COM EDUCAÇÃO
Recuo exponencial, jitter e as três regras
Repetir também tem custo. Se o servidor está num incidente com indisponibilidade real, as repetições não passam e podem agravar a degradação. Por isso se recomenda um recuo exponencial: o cliente espera pouco após a primeira falha e, conforme a operação continua falhando, espera um tempo proporcional a 2^n, em que n é o número de falhas. Mesmo assim, se muitos clientes falharam quase ao mesmo tempo, os calendários de repetição podem se alinhar e atingir o servidor juntos, o chamado problema do thundering herd. A correção é somar um jitter aleatório à espera de cada cliente.
A biblioteca Ruby do Stripe faz isso sozinha: repete automaticamente em caso de falha, com a mesma chave de idempotência, tempos de espera crescentes e jitter.
| Cabeçalho Stripe-Should-Retry | O que o cliente faz |
|---|---|
| Verdadeiro | Repete usando recuo exponencial, para não sobrecarregar a API |
| Falso | Não repete |
| Ausente | Decide pelo código de status e outras informações da resposta |
Perguntas frequentes
O que é Stripe: APIs previsíveis com chaves de idempotência?
Arquitetura real em produção. Pontos centrais: Operações Idempotentes, Fundamentos de Design de API e Padrões de Design para Nuvem.
O que o design de Stripe: APIs previsíveis com chaves de idempotência cobre?
Ele está organizado nestas seções: A resposta que não chega, Quando repetir já é seguro, Exatamente uma vez, mesmo com repetição, Executar, devolver ou recusar, Janela de repetição e reconciliação e Recuo exponencial, jitter e as três regras.
Quanto tempo leva Stripe: APIs previsíveis com chaves de idempotência e qual é o nível?
Leva cerca de 17 minutos para ler e tem nível Sênior.
Agora projete você mesmo
Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.