Como lidar com requisições duplicadas no back-end?

Um problema bem comum no desenvolvimento de software é lidar com requisições duplicadas.

Às vezes a gente pensa que, quando o usuário clica em um botão, aquela ação vai acontecer apenas uma vez. Mas, na prática, nem sempre é assim.

Um exemplo comum: tela de pagamento

Imagine uma tela de pagamento.

O usuário clica no botão de pagar, a requisição demora um pouco para responder e ele clica novamente.

Ou então a internet oscila, o front-end tenta enviar a requisição de novo, ou uma integração externa envia o mesmo webhook mais de uma vez.

Nesses cenários, o back-end pode acabar recebendo a mesma intenção mais de uma vez.

Se o sistema não estiver preparado, uma única tentativa de pagamento pode virar duas cobranças. Uma criação de pedido pode virar dois pedidos. Uma movimentação financeira pode ser registrada mais de uma vez.

Onde entra a idempotência?

É aqui que entra a idempotência.

De forma simples, idempotência é a ideia de permitir que uma mesma operação seja chamada mais de uma vez sem causar efeitos duplicados.

Ou seja: se a requisição já foi processada antes, o sistema precisa reconhecer isso e evitar processar tudo novamente.

Exemplo simples com Node.js

Um exemplo simples em Node.js seria:

const crypto = require("crypto");
const processedKeys = new Map();
 
app.post("/payments", (req, res) => {
  const key = req.headers["idempotency-key"];
 
  if (!key) {
    return res.status(400).json({
      error: "Idempotency-Key is required",
    });
  }
 
  if (processedKeys.has(key)) {
    return res.status(200).json(processedKeys.get(key));
  }
 
  const payment = {
    id: crypto.randomUUID(),
    amount: req.body.amount,
    status: "created",
  };
 
  processedKeys.set(key, payment);
 
  return res.status(201).json(payment);
});

O que esse código está fazendo?

Nesse exemplo, o cliente envia uma chave chamada Idempotency-Key.

Essa chave funciona como uma identificação única daquela tentativa de operação.

Na primeira vez que a requisição chega, o pagamento é criado normalmente e o resultado é armazenado.

Se a mesma requisição chegar novamente com a mesma chave, o sistema não cria outro pagamento. Ele apenas retorna o resultado que já tinha sido gerado antes.

Por que Map não é suficiente em produção?

É claro que esse exemplo usa Map apenas para facilitar o entendimento.

Em um ambiente real, manter isso apenas em memória não seria uma boa ideia, porque os dados seriam perdidos se a aplicação reiniciasse.

Além disso, em aplicações com várias instâncias rodando ao mesmo tempo, cada instância teria sua própria memória. Isso significa que uma instância poderia conhecer uma chave de idempotência, enquanto outra não.

Redis, banco de dados ou os dois?

Na prática, faria mais sentido usar Redis com TTL, banco de dados com uma constraint única, ou até os dois juntos, dependendo da criticidade da operação.

Redis

Redis pode ser útil quando você quer guardar a chave por um tempo limitado, como alguns minutos ou horas.

Ele costuma fazer sentido para controles temporários, retries rápidos e cenários em que a chave não precisa existir para sempre.

Banco de dados

Já o banco de dados costuma ser mais interessante quando você precisa garantir consistência em operações críticas, como pagamentos, pedidos ou movimentações financeiras.

Nesse caso, uma constraint única pode impedir que a mesma operação seja registrada mais de uma vez.

Redis + banco

Em alguns fluxos mais sensíveis, usar Redis e banco juntos pode ser uma abordagem ainda mais segura.

O Redis ajuda a evitar reprocessamentos rápidos e repetidos.

O banco continua sendo a fonte final de verdade para garantir que uma operação crítica não seja duplicada.

Conclusão

No fim das contas, idempotência não é só sobre evitar que o usuário clique duas vezes em um botão.

É sobre aceitar que falhas acontecem.

Requisições podem ser reenviadas. Webhooks podem chegar duplicados. Jobs podem ser reprocessados. A rede pode falhar em momentos estranhos.

Um back-end bem preparado precisa continuar correto mesmo quando essas coisas acontecem.

E quando falamos de Redis como parte dessa solução, vale entender melhor como ele pode ser usado além de um simples cache temporário. Se quiser se aprofundar nisso, confira o artigo sobre estratégias de cache no back-end.