Pular para o conteúdo
tech

O que é Idempotência e Por que Seu Sistema Precisa Disso

·7 min de leitura·8 visualizações

Você já clicou duas vezes no botão de pagar porque a tela travou? E recebeu duas cobranças?

Isso é um problema de idempotência — ou melhor, da falta dela.

Idempotência é uma daquelas palavras que parecem acadêmicas demais pra importar no dia a dia. Mas ela está por trás de praticamente todo sistema confiável que você usa. E se você desenvolve software que lida com dinheiro, filas, webhooks ou qualquer coisa que possa ser executada mais de uma vez — você precisa entender isso.

A definição sem enrolação

Uma operação é idempotente quando executá-la uma vez ou dez vezes produz o mesmo resultado.

Exemplo mais simples do mundo:

SET user.email = "billy@email.com"

Executar isso 1 vez ou 100 vezes dá no mesmo. O email vai ser billy@email.com. Isso é idempotente.

Agora compare com:

UPDATE conta SET saldo = saldo + 100

Executar 1 vez: saldo +100. Executar 2 vezes: saldo +200. Cada execução muda o estado. Isso não é idempotente.

Por que isso importa na vida real

Na teoria, cada request HTTP acontece exatamente uma vez. Na prática, o mundo é caótico:

  • O usuário clica duas vezes no botão
  • A rede cai no meio da request e o client faz retry
  • O load balancer reenvia a request porque achou que deu timeout
  • O webhook do Stripe é disparado duas vezes
  • O worker da fila processa o mesmo job duas vezes porque o ACK se perdeu

Em todos esses cenários, sua operação vai ser executada mais de uma vez. Se ela não for idempotente, você tem um problema.

Cenário real: cobrança duplicada

O usuário clica em "Pagar R$49,90". A request vai pro backend, o pagamento é processado, mas a resposta demora. O frontend não recebe confirmação. O usuário clica de novo.

Sem idempotência: duas cobranças de R$49,90. O usuário reclama, você reembolsa manualmente, perde confiança.

Com idempotência: a segunda request é identificada como duplicata. O sistema retorna o resultado da primeira execução. Uma cobrança, zero problema.

Os verbos HTTP e idempotência

O protocolo HTTP já foi pensado com isso em mente:

VerboIdempotente?Por quê
GETSimSó lê dados, não muda nada
PUTSimSubstitui o recurso inteiro — repetir dá no mesmo
DELETESimDeletar algo que já foi deletado = mesmo estado final
POSTNãoCada chamada pode criar um novo recurso
PATCHDependeSe é "set campo X = Y", sim. Se é "incrementa X", não

O POST é o problemático. E é justamente o verbo que usamos pra criar pedidos, processar pagamentos, enviar mensagens. Tudo que tem efeito colateral real.

Como tornar operações idempotentes

Existem três padrões principais:

1. Idempotency Key

O client gera um identificador único (UUID) e envia junto com a request. O servidor armazena esse ID e o resultado. Se a mesma key aparecer de novo, retorna o resultado salvo sem executar nada.

// No controller Laravel
public function processPayment(Request $request)
{
    $key = $request->header("Idempotency-Key");

    $cached = Cache::get("idempotency:{$key}");
    if ($cached) {
        return response()->json($cached);
    }

    $result = $this->paymentService->charge($request->all());

    Cache::put("idempotency:{$key}", $result, now()->addHours(24));

    return response()->json($result);
}

É exatamente assim que o Stripe funciona. Toda request de pagamento aceita um header Idempotency-Key. Se você mandar a mesma key duas vezes, o Stripe retorna o resultado da primeira execução.

2. Operações naturalmente idempotentes

Às vezes, basta redesenhar a operação:

-- Não idempotente
UPDATE conta SET saldo = saldo + 100;

-- Idempotente (com referência única)
INSERT INTO transacoes (id, conta_id, valor)
VALUES ("txn_abc123", 42, 100)
ON DUPLICATE KEY UPDATE id = id;  -- noop se  existe

Em vez de "incrementar saldo", você "registra uma transação com ID único". Se a transação já existe, nada acontece. O saldo é calculado a partir das transações — sempre correto, não importa quantas vezes você tente inserir.

3. Detecção de duplicatas

Pra webhooks e eventos, use um registro do que já foi processado:

public function handleWebhook(Request $request)
{
    $eventId = $request->input("event_id");

    if (ProcessedEvent::where("event_id", $eventId)->exists()) {
        return response()->json(["status" => "already_processed"]);
    }

    DB::transaction(function () use ($request, $eventId) {
        // Processa o evento
        $this->processEvent($request->all());

        // Marca como processado
        ProcessedEvent::create(["event_id" => $eventId]);
    });

    return response()->json(["status" => "processed"]);
}

O segredo é que a marcação e o processamento acontecem na mesma transação. Se o processamento falha, a marcação também falha — e o retry vai funcionar corretamente.

Filas e jobs: o campo minado

Se você usa filas (Redis, SQS, RabbitMQ), idempotência não é opcional — é obrigatória.

Por quê? Porque filas garantem at-least-once delivery, não exactly-once. Seu job vai ser executado mais de uma vez em algum momento. Pode ser por timeout, por crash do worker, por rebalanceamento de partições.

No Laravel, isso é especialmente relevante com Horizon:

class ProcessPaymentJob implements ShouldQueue
{
    public function __construct(
        private string $transactionId,
        private int $userId,
        private int $amount
    ) {}

    public function handle()
    {
        // Guard de idempotência
        if (Payment::where("transaction_id", $this->transactionId)->exists()) {
            return; // Já processado, sai silenciosamente
        }

        // Processa o pagamento
        Payment::create([
            "transaction_id" => $this->transactionId,
            "user_id" => $this->userId,
            "amount" => $this->amount,
        ]);
    }
}

O transaction_id é a chave. Se o job rodar duas vezes com o mesmo ID, a segunda execução é um noop.

Onde a maioria erra

1. Confiar que "não vai acontecer"

Vai. Em produção, com carga real, tudo que pode ser executado duas vezes será executado duas vezes. Lei de Murphy aplicada a sistemas distribuídos.

2. Idempotência só no endpoint, não no domínio

De nada adianta ter uma idempotency key no controller se o service por baixo faz saldo += valor sem verificação. A idempotência precisa descer até a camada que muda estado.

3. Cache sem TTL

Se você usa idempotency key com cache, defina um TTL razoável (24-48h). Senão, sua tabela de keys cresce infinitamente.

4. Ignorar o problema em webhooks

Webhooks de terceiros (Stripe, Asaas, PagSeguro) vão enviar o mesmo evento mais de uma vez. A documentação deles avisa. Se você não trata, vai processar o mesmo pagamento, a mesma notificação, o mesmo cancelamento duas vezes.

Idempotência no mundo real: como uso no HubNews

No HubNews, o pipeline de notícias processa centenas de artigos por dia. Cada artigo passa por RSS → Curadoria → Escrita → FactCheck → Tradução → Imagem → Publicação.

Se qualquer etapa falha e entra em retry, o sistema precisa lidar com isso sem duplicar artigos, sem gerar duas imagens, sem publicar a mesma notícia duas vezes nas redes sociais.

A solução: cada artigo tem um hash único baseado no conteúdo original do RSS. Se o pipeline tenta processar o mesmo hash, ele detecta a duplicata via embedding similarity (threshold 0.82) e faz merge em vez de criar um novo registro.

Nas redes sociais, cada post tem um ID de referência. O sistema verifica se aquele artigo já foi publicado no Twitter/LinkedIn/Instagram antes de disparar. Se já foi, pula. Simples, robusto, idempotente.

Checklist rápido

Antes de colocar qualquer endpoint em produção, pergunte:

  • O que acontece se essa request for enviada duas vezes?
  • Meus jobs de fila são seguros pra retry?
  • Webhooks de terceiros estão protegidos contra duplicatas?
  • Operações financeiras têm idempotency key ou transaction ID?
  • A detecção de duplicata e o processamento estão na mesma transação?

Se a resposta pra qualquer uma dessas for "não sei" — você tem trabalho a fazer.

O takeaway

Idempotência não é um conceito teórico de ciência da computação. É uma propriedade de design que separa sistemas que funcionam em condições ideais de sistemas que funcionam no mundo real.

Redes falham. Usuários clicam duas vezes. Workers crasham. Webhooks repetem. A pergunta não é se sua operação vai ser executada mais de uma vez — é quando.

Sistemas idempotentes tratam o retry como um cenário normal, não como uma exceção. E isso é a diferença entre um sistema que dá problema toda semana e um que roda em paz.

Quer aplicar isso no seu projeto?

Mentoria e consultoria em carreira, código e produtos digitais.

Falar com Billy
Billy

Billy

Full Stack Dev & Empreendedor Solo

Construindo produtos com código e IA. Criador do HubNews e Sistema Reino.

Compartilhar:XLinkedInWhatsApp