O motor de idempotência: garantindo a integridade transacional

SchemaBridge Team · 2025-12-22 · Idempotency, Consistency, Transactions

Execução at-least-once, deduplicação na entrada e chaves no destino: onde os duplicados são realmente barrados em um workflow distribuído, e qual parte é sua responsabilidade.

O pesadelo da cobrança em dobro: por que sistemas distribuídos odeiam retries

Em nosso mundo de redes instáveis e recursos de nuvem efêmeros, falhas não são apenas frequentes; são um estado permanente. Todo engenheiro sênior já viveu o pesadelo da "ação órfã", um cenário que começa como uma pequena falha de rede e termina em corrupção de dados ou em um passivo financeiro. É o cenário clássico de falha de sistemas distribuídos:

1. Requisição: seu orquestrador envia uma requisição de "cobrança" a uma API de pagamentos ou de "envio" a uma transportadora.

2. Sucesso: a API de terceiros processa a requisição com sucesso, cobra o cartão do cliente ou imprime a etiqueta de envio.

3. Partição: ocorre uma breve falha de rede no caminho de volta. A resposta da API — a confirmação vital — nunca chega à sua máquina worker.

4. Retry: seu worker, ao ver um timeout, assume, seguindo a prática padrão, que o processo falhou. Ele segue sua política de retry e envia a requisição novamente.

5. Duplicado: como a API não recebeu uma identidade única para essa intenção específica, ela processa a requisição de novo. Ela a vê como uma nova transação. O cliente é cobrado duas vezes, ou duas etiquetas de envio são geradas para o mesmo pedido.

Isso não é uma falha da lógica do código; é uma falha de identidade. Sem uma forma de identificar unicamente uma intenção específica através do tempo e do espaço, seu sistema aposta com os dados e o dinheiro dos seus clientes toda vez que tenta reconectar.

Transações distribuídas morreram: viva a idempotência

Em uma arquitetura monolítica, contamos com o Two-Phase Commit (2PC) ou com locks distribuídos globais para garantir a consistência. Essas ferramentas nos permitem tratar várias operações como uma única unidade de verdade. Mas em um mundo fragmentado de APIs SaaS, workers serverless e microsserviços poliglotas, transações globais são uma fantasia. Elas não escalam, introduzem uma latência enorme e a maioria dos provedores terceiros não as suporta. Elas exigem um "bloqueio" de recursos impossível de alcançar através das fronteiras entre organizações.

O único caminho viável para a consistência distribuída é a idempotência. Matematicamente, uma operação é idempotente se pode ser aplicada várias vezes sem alterar o resultado além da primeira aplicação. Em termos algébricos: f(x) = f(f(x)). Em termos de engenharia, significa que seu sistema pode falhar e tentar novamente quantas vezes for preciso, e o resultado final será sempre o correto.

O contrato honesto: at-least-once, com salvaguardas

Sejamos precisos sobre o que um orquestrador pode e não pode prometer. O SchemaBridge entrega trabalho at-least-once. Um worker pode cair depois que a chamada HTTP teve sucesso, mas antes de registrar o resultado; uma mensagem pode ser reentregue; um lease de claim pode expirar e ser assumido por outro. Em cada um desses casos, o comportamento seguro é executar a etapa novamente em vez de perdê-la — o que significa que uma etapa pode, raramente, ser executada duas vezes.

O que a plataforma faz é tornar esse "raramente" o mais raro possível, em duas camadas:

Camada 1: deduplicação de triggers de entrada

Cada disparo de um entry point recebe um ID de execução e, por padrão, esse ID é derivado do conteúdo: um SHA-256 determinístico sobre o workspace, o vertex de entry point e o payload da requisição. A mesma entrega de webhook chegando duas vezes resolve para a mesma execução em vez de iniciar uma segunda — exatamente o que você quer quando um remetente tenta de novo. Você controla a identidade com três alavancas:

| Alavanca | Onde | Use quando |

| :--- | :--- | :--- |

| idempotencyFields | Configuração do vertex de entry point | Você quer deduplicar por um identificador de negócio (ex.: order.id) em vez do corpo inteiro |

| clientIdempotencyId | Parâmetro de query na chamada de trigger | O chamador é dono da chave de deduplicação |

| forceNewExecution | Apenas execuções de teste e a chamada de execução em streaming (ativo por padrão nelas) | Não disponível em triggers de produção — nelas, use idempotencyFields ou um clientIdempotencyId único por chamada |

{
  "type": "ENTRY_POINT",
  "config": {
    "idempotencyFields": "order.id"
  }
}

O outro lado é uma armadilha real: se duas execuções legítimas puderem carregar payloads idênticos byte a byte ("rode o relatório noturno com corpo vazio"), elas serão fundidas em uma só, a menos que você desative isso. Triggers agendados (cron) são a exceção — cada disparo recebe um novo ID aleatório, porque um agendamento nunca deve ser deduplicado. E saiba que a deduplicação explícita é uma consulta do tipo ler-e-depois-escrever, não uma restrição de unicidade no banco de dados, então dois triggers verdadeiramente concorrentes com a mesma chave podem ambos passar.

Camada 2: salvaguardas no nível da etapa

Dentro de uma execução, uma etapa que já está COMPLETED não é executada de novo, um worker precisa reivindicar atomicamente uma etapa (DISPATCHED → EXECUTING) antes de executá-la, as transições terminais são protegidas, e uma época de claim isola um worker cujo lease foi assumido por outro. Essas salvaguardas barram os caminhos comuns de duplicação — mas nenhuma delas consegue entrar em uma API de terceiros e desfazer uma chamada que já aconteceu.

A camada 3 é sua: a chave no destino

Essa última lacuna é o motivo pelo qual o próprio contrato de comportamento da plataforma diz claramente: torne o efeito colateral idempotente no destino e não conte com o orquestrador para executá-lo exatamente uma vez.

A boa notícia é que a maioria das APIs sérias (pagamentos, envio, mensageria) aceita uma chave de idempotência, e uma etapa Gateway pode enviar uma derivada dos dados da execução. No SchemaBridge, o valor de um header pode ser uma expressão sobre a entrada da etapa, de modo que a chave é calculada a partir de um identificador de negócio estável — nunca de um timestamp ou de um valor aleatório:

{
  "type": "GATEWAY",
  "config": {
    "method": "POST",
    "url": "https://api.payments.example/v1/charges",
    "headers": {
      "Idempotency-Key": "= 'charge-' & payload.order.id",
      "Authorization": "Bearer {{ secrets.PAYMENTS_KEY }}"
    }
  }
}

Como a chave depende apenas do pedido, uma nova execução desta etapa — após uma reentrega, um Retry feito por um operador ou um retry automático por sobrecarga — envia a mesma chave, e o destino retorna o primeiro resultado em vez de cobrar de novo. Uma ressalva: Start over (uma repetição do workflow inteiro) cria uma nova execução, mas se sua chave for derivada de dados de negócio e não do ID da execução, ela permanece estável mesmo nesse caso. É exatamente por isso que identificadores de negócio são chaves melhores do que identificadores do motor.

Lidando com APIs legadas: o padrão "Read-Verify-Write"

Infelizmente, muitos sistemas legados e provedores SaaS de nicho não suportam chaves de idempotência. Eles não têm um header Idempotency-Key. Para esses endpoints "não idempotentes", você pode compor uma sequência Read-Verify-Write a partir de etapas comuns:

1. Etapa verificadora (Read): uma etapa Gateway primeiro consulta o sistema de destino para ver se o registro já existe (ex.: GET /orders?external_id=123, usando o mesmo identificador de negócio que você teria usado como chave).

2. Ramificação condicional: um edge condicional verifica a resposta. Se o pedido existe, a execução segue pelo caminho de "já feito". Se não, ela prossegue.

3. Etapa de ação (Write): somente se a verificadora não encontrar nada é que a execução avança para a escrita propriamente dita (POST /orders).

Conheça a janela

O Read-Verify-Write estreita a janela de duplicação; ele não a fecha. Se um worker cair depois da escrita, mas antes de registrar o sucesso, a etapa de escrita é executada novamente por conta própria — ela não volta para verificar de novo primeiro. Então, para um destino sem nenhum suporte a idempotência, procure também uma restrição de unicidade natural do lado do destino (um external_id que ele rejeite em caso de conflito) e trate uma resposta de conflito como "já feito". Por padrão, um 4xx faz uma etapa Gateway falhar; definir clientErrorMode como complete transforma uma resposta de erro do cliente em dados que sua próxima etapa pode usar para rotear.

A armadilha do gerenciamento de chaves: por que a idempotência caseira falha em escala

Muitas equipes de engenharia tentam construir uma "tabela de idempotência" em seu banco de dados principal. Isso cria três problemas críticos que acabam matando a velocidade e a confiabilidade:

1. O gargalo de escrita: cada chamada de API agora exige uma escrita no banco para registrar o token. Sob carga pesada, sua tabela de idempotência vira um ponto de contenção.

2. Complexidade de limpeza, o problema do lixo: chaves de idempotência não são necessárias para sempre. Você precisa de um processo em segundo plano ou de um TTL (Time-To-Live) para remover chaves antigas. Se a limpeza for agressiva demais, você arrisca cobranças em dobro para tarefas lentas e reexecutadas. Se for lenta demais, a tabela cresce sem limite.

3. O descompasso de estado distribuído: o que acontece se a escrita no banco tiver sucesso, mas a chamada de API falhar? Ou se seu worker cair depois da chamada de API, mas antes que o banco possa ser atualizado para "Concluído"? Você acaba com um descompasso que exige intervenção manual para ser resolvido.

A saída não é uma tabela mais inteligente do seu lado; é deixar que o sistema que executa o efeito colateral seja dono da deduplicação. Um destino que respeita uma chave de idempotência já resolveu o armazenamento, a expiração e a janela de falha para sua própria operação — seu trabalho é enviar a ele uma chave estável.

Geração do token no cliente vs. no servidor

Onde o token deve ser gerado?

Em um workflow do SchemaBridge, a origem da intenção são os próprios dados da execução. Derivar a chave ali, no header da etapa Gateway, a mantém estável em todos os caminhos de retry que o motor possa seguir.

Tabela comparativa: onde os duplicados são barrados

| Origem do duplicado | Barrado por | Quem configura |

| :--- | :--- | :--- |

| Remetente reenvia o mesmo webhook | ID de execução derivado do conteúdo / idempotencyFields / clientIdempotencyId | SchemaBridge, com a chave que você escolher |

| Etapa reentregue dentro de uma execução | Salvaguardas no nível da etapa (curto-circuito se concluída, claim atômico, época de claim) | SchemaBridge |

| Etapa reexecutada depois que a chamada já teve sucesso | Chave de idempotência no destino | Você, na etapa Gateway |

| Destino sem suporte a idempotência | Read-Verify-Write mais uma restrição de unicidade no destino | Você |

Dicas de especialista: o checklist de idempotência

1. Nunca use timestamps: sua chave deve se basear nos dados, não no tempo.

2. Delimite suas chaves: prefixe as chaves por operação (charge-, ship-) para que uma chave de "Envio" nunca colida com uma de "Cobrança", mesmo que compartilhem o ID do pedido.

3. Trate conflitos 409: se uma API retornar um 409 (Conflict) para uma chave ou identificador que já viu, seu workflow normalmente deve tratar isso como "já feito" se a entrada for a mesma.

4. Decida sobre payloads repetidos de antemão: se duas execuções genuínas puderem carregar corpos idênticos, defina idempotencyFields ou passe um clientIdempotencyId único por chamada — caso contrário, a segunda execução será descartada pela deduplicação. (forceNewExecution só existe em execuções de teste e na chamada de execução em streaming.)

5. Faça da chave parte do template: se um desenvolvedor precisa lembrar de adicionar uma chave de idempotência, um dia ele vai esquecer. Coloque-a na configuração de Gateway que você reutiliza para essa API.

Conclusão: a identidade é a espinha dorsal da verdade

Em um sistema distribuído, você não pode confiar na rede, não pode confiar no relógio e não pode confiar na resposta. A única coisa em que você pode realmente confiar é a identidade.

O SchemaBridge oferece identidade nas fronteiras que controla — IDs de execução derivados do conteúdo, chaves de deduplicação explícitas e salvaguardas no nível da etapa — e é honesto sobre a fronteira que não controla: o efeito colateral no terceiro. Derive uma chave estável dos seus dados de negócio, envie-a em cada chamada, e o "pesadelo da cobrança em dobro" se torna uma decisão de design que você tomou uma vez, em vez de um incidente que você descobre depois.

Parte da nossa série Building the Bridge. Veja também: Dominando o merge sobre a sincronização de estado entre ramificações paralelas.

Explorar