Condições de corrida em apps CRUD podem causar pedidos duplicados e totais errados. Aprenda pontos comuns de colisão e correções com restrições de banco, bloqueios e proteções na interface.

Uma condição de corrida acontece quando duas (ou mais) requisições atualizam os mesmos dados quase ao mesmo tempo, e o resultado final passa a depender do timing. Cada requisição, isoladamente, parece correta. Juntas, produzem um resultado errado.
Um exemplo simples: duas pessoas clicam em Salvar no mesmo registro de cliente em menos de um segundo. Uma altera o e‑mail, a outra altera o telefone. Se ambas enviam o registro inteiro, a segunda gravação pode sobrescrever a primeira, e uma das mudanças desaparece sem erro.
Você vê isso mais em apps rápidos porque os usuários disparam mais ações por minuto. Também explode em momentos de pico: promoções relâmpago, fechamento de mês, uma grande campanha de e‑mail ou qualquer momento em que um acúmulo de requisições atinge as mesmas linhas.
Os usuários raramente relatam “uma condição de corrida”. Eles relatam sintomas: pedidos ou comentários duplicados, atualizações perdidas ("salvei, mas voltou atrás"), totais estranhos (estoque fica negativo, contadores retrocedem) ou status que mudam inesperadamente (aprovado e depois volta para pendente).
Retentativas pioram. Pessoas dão duplo clique, atualizam a página após resposta lenta, enviam de duas abas ou lidam com redes instáveis que fazem o navegador ou app reenviar. Se o servidor trata cada requisição como uma gravação nova, você pode acabar com dois creates, dois pagamentos ou duas mudanças de status que deveriam ocorrer apenas uma vez.
A maioria dos apps CRUD parece simples: lê uma linha, muda um campo, salva. O problema é que seu app não controla o timing. O banco de dados, a rede, retries, trabalhos em background e o comportamento do usuário se sobrepõem.
Um gatilho comum é duas pessoas editando o mesmo registro. Ambas carregam os mesmos valores “atuais”, fazem mudanças válidas e o último salvamento sobrescreve o primeiro silenciosamente. Ninguém fez nada errado, mas uma atualização se perde.
Também acontece com uma só pessoa. Um duplo clique no botão Salvar, tocar repetidamente, ou uma conexão lenta que leva alguém a apertar Enviar de novo pode disparar a mesma gravação duas vezes. Se o endpoint não for idempotente, você pode criar duplicatas, cobrar duas vezes ou avançar um status duas etapas.
O uso moderno adiciona mais sobreposição. Várias abas ou dispositivos na mesma conta podem disparar atualizações conflitantes. Jobs em background (e‑mails, cobrança, sincronização, limpeza) podem tocar as mesmas linhas de requisições web. Retries automáticos no cliente, no load balancer ou no executor de jobs podem repetir uma requisição que já teve sucesso.
Se você está entregando recursos rápido, o mesmo registro frequentemente é atualizado de lugares que ninguém lembra. Se você usa um construtor guiado por chat como Koder.ai, o app pode crescer ainda mais rápido — então vale tratar concorrência como comportamento normal, não como caso de borda.
Condições de corrida raramente aparecem em demos de “criar um registro”. Elas aparecem onde duas requisições tocam a mesma fonte de verdade quase no mesmo momento. Conhecer os pontos quentes ajuda a projetar gravações seguras desde o início.
Qualquer coisa que pareça “só soma 1” pode quebrar sob carga: likes, contadores de visualização, totais, números de fatura, números de bilhete. O padrão arriscado é ler o valor, somar e escrever de volta. Duas requisições podem ler o mesmo valor inicial e sobrescrever uma à outra.
Workflows como Rascunho -> Submetido -> Aprovado -> Pago parecem diretos, mas colisões são comuns. O problema começa quando duas ações são possíveis ao mesmo tempo (aprovar e editar, cancelar e pagar). Sem proteções, você pode ter um registro que pula etapas, volta atrás ou mostra estados diferentes em tabelas distintas.
Trate mudanças de status como um contrato: permita apenas o próximo passo válido e recuse qualquer outra tentativa.
Assentos restantes, contagens de estoque, horários de atendimento e campos de “capacidade restante” criam o clássico problema de oversell. Dois compradores finalizam a compra ao mesmo tempo, ambos veem disponibilidade e ambos conseguem. Se o banco de dados não for o juiz final, você venderá mais do que tem.
Algumas regras são absolutas: um e‑mail por conta, uma assinatura ativa por usuário, um carrinho aberto por usuário. Essas regras frequentemente falham quando você checa antes ("existe algum?") e depois insere. Sob concorrência, ambas as requisições podem passar na checagem.
Se você gera fluxos CRUD rapidamente (por exemplo, conversando com Koder.ai), anote esses pontos quentes cedo e suporte‑os com restrições e gravações seguras, não apenas checagens na UI.
Muitas condições de corrida começam com algo chato: a mesma ação é enviada duas vezes. Usuários dão duplo clique. A rede está lenta então clicam novamente. Um telefone registra dois toques. Às vezes não é intencional: a página recarrega depois de um POST e o navegador oferece reenviar o formulário.
Quando isso acontece, o backend pode executar dois creates ou updates em paralelo. Se ambos tiverem sucesso, você tem duplicatas, totais errados ou uma mudança de status que roda duas vezes (por exemplo, aprovar duas vezes). Parece aleatório porque depende do timing.
A abordagem mais segura é defesa em profundidade. Conserte a UI, mas assuma que a UI vai falhar.
Mudanças práticas que você pode aplicar à maioria dos fluxos de gravação:
Exemplo: um usuário toca “Pagar fatura” duas vezes no celular. A UI deve bloquear o segundo toque. O servidor também deve rejeitar a segunda requisição ao ver a mesma chave de idempotência, retornando o resultado original em vez de cobrar duas vezes.
Campos de status parecem simples até duas coisas tentarem mudá‑los ao mesmo tempo. Um usuário clica Aprovar enquanto um job automático marca o mesmo registro como Expirado, ou dois membros da equipe trabalham o mesmo item em abas diferentes. Ambas as atualizações podem ter sucesso, mas o status final depende do timing, não das suas regras.
Trate status como uma pequena máquina de estados. Mantenha uma tabela curta de movimentos permitidos (por exemplo: Rascunho -> Submetido -> Aprovado, e Submetido -> Rejeitado). Então cada gravação checa: “Esse movimento é permitido a partir do status atual?” Se não, rejeite em vez de sobrescrever silenciosamente.
Locking otimista ajuda a detectar atualizações obsoletas sem bloquear outros usuários. Adicione um número de versão (ou updated_at) e exija que ele bata ao salvar. Se outra pessoa mudou a linha depois que você a carregou, sua atualização afetará zero linhas e você pode mostrar uma mensagem clara como “Este item mudou, atualize e tente novamente.”
Um padrão simples para atualizações de status é:
Além disso, mantenha mudanças de status centralizadas. Se atualizações estiverem espalhadas entre telas, jobs e webhooks, você vai perder regras. Coloque‑as atrás de uma única função ou endpoint que aplique as mesmas checagens de transição sempre.
O bug de contador mais comum parece inofensivo: o app lê um valor, soma 1 e grava de volta. Sob carga, duas requisições podem ler o mesmo número e ambas gravar o mesmo novo número, então um incremento some. Isso é fácil de perder porque “normalmente funciona” em testes.
Se um valor só é incrementado ou decrementado, deixe o banco fazer isso em uma única instrução. Assim o banco aplica as mudanças de forma segura mesmo quando muitas requisições chegam ao mesmo tempo.
UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;
A mesma ideia vale para inventário, contadores de visualização, contadores de retry e qualquer coisa que possa ser expressa como “novo = antigo + delta”.
Totais costumam errar quando você armazena um número derivado (order_total, account_balance, project_hours) e depois o atualiza de vários lugares. Se você puder calcular o total a partir das linhas de origem (itens do pedido, lançamentos contábeis), evita uma classe inteira de bugs de drift.
Quando for necessário armazenar um total por performance, trate‑o como uma gravação crítica. Mantenha as atualizações das linhas de origem e do total armazenado na mesma transação. Garanta que apenas um escritor possa atualizar o mesmo total de cada vez (locking, updates guardados ou um único caminho proprietário). Adicione constraints que impeçam valores impossíveis (por exemplo, estoque negativo). Depois, reconcilie ocasionalmente com uma verificação em background que recompute e marque divergências.
Um exemplo concreto: dois usuários adicionam itens ao mesmo carrinho ao mesmo tempo. Se cada requisição ler cart_total, somar o preço do item e gravar de volta, uma adição pode desaparecer. Se você atualizar os itens do carrinho e o total do carrinho juntos em uma transação, o total permanece correto mesmo sob cliques paralelos pesados.
Se você quer menos condições de corrida, comece pelo banco. Código de app pode retryar, expirar ou rodar duas vezes. Uma restrição no banco é o portão final que fica correto mesmo quando duas requisições batem ao mesmo tempo.
Constraints de unicidade evitam duplicatas que “nunca deveriam acontecer” mas acontecem: endereços de e‑mail, números de pedido, IDs de fatura ou uma regra de “uma assinatura ativa por usuário”. Quando duas inscrições chegam juntas, o banco aceita uma linha e rejeita a outra.
Chaves estrangeiras impedem referências quebradas. Sem elas, uma requisição pode deletar um pai enquanto outra cria um filho apontando para nada, deixando órfãos difíceis de limpar depois.
Check constraints mantêm valores em um intervalo seguro e impõem regras simples de estado. Por exemplo, quantity >= 0, rating entre 1 e 5, ou status limitado a um conjunto permitido.
Trate falhas de constraint como resultados esperados, não como “erros de servidor”. Capture violações de unicidade, foreign key e check, retorne uma mensagem clara como “Esse e‑mail já está em uso” e registre detalhes para debugging sem vazar internos.
Exemplo: duas pessoas clicam “Criar pedido” duas vezes durante lag. Com uma constraint única em (user_id, cart_id), você não obtém dois pedidos. Obtém um pedido e uma rejeição limpa e explicável.
Algumas gravações não são uma única instrução. Você lê uma linha, checa uma regra, atualiza um status e talvez insere um log de auditoria. Se duas requisições fizerem isso ao mesmo tempo, ambas podem passar na checagem e ambas gravar. Esse é o padrão clássico de falha.
Envolva a gravação de múltiplos passos em uma transação de banco para que todos os passos aconteçam juntos ou nenhum aconteça. Mais importante, a transação te dá um lugar para controlar quem pode mudar os mesmos dados ao mesmo tempo.
Quando só um ator pode editar um registro por vez, use um bloqueio em nível de linha. Por exemplo: bloqueie a linha do pedido, confirme que ainda está em “pendente”, então altere para “aprovado” e escreva a entrada de auditoria. A segunda requisição aguardará, então rechecá o estado e parará.
Escolha com base em quão frequentes são as colisões:
Mantenha o tempo de lock curto. Faça o mínimo possível enquanto o lock estiver ativo: nada de chamadas a APIs externas, trabalho de arquivo lento ou grandes loops. Se você está montando fluxos em uma ferramenta como Koder.ai, mantenha a transação apenas para os passos de banco de dados e faça o resto após o commit.
Escolha um fluxo que possa perder dinheiro ou confiança quando colidir. Um comum é: criar um pedido, reservar estoque e então marcar o pedido como confirmado.
Escreva os passos exatos que seu código faz hoje, em ordem. Seja específico sobre o que é lido, o que é escrito e o que significa “sucesso”. Colisões se escondem na lacuna entre uma leitura e uma gravação posterior.
Um caminho de endurecimento que funciona na maioria das stacks:
Adicione um teste que prove a correção. Rode duas requisições ao mesmo tempo contra o mesmo produto e quantidade. Afirme que exatamente um pedido foi confirmado e o outro falhou de forma controlada (sem estoque negativo, sem linhas de reserva duplicadas).
Se você gera apps rapidamente (incluindo com plataformas como Koder.ai), essa checklist ainda vale para os poucos caminhos de escrita que importam.
Uma das maiores causas de condições de corrida é confiar na UI. Botões desabilitados e checagens no cliente ajudam, mas usuários podem dar duplo clique, atualizar, abrir duas abas ou reenviar uma requisição de uma conexão instável. Se o servidor não é idempotente, duplicatas entram.
Outro bug silencioso: você captura um erro do banco (como violação de unicidade) mas continua o fluxo como se nada tivesse acontecido. Isso frequentemente vira “create falhou, mas ainda enviamos o e‑mail” ou “pagamento falhou, mas ainda marcamos o pedido como pago”. Uma vez que efeitos colaterais aconteceram, é difícil desfazer.
Transações longas também são uma armadilha. Se você mantém uma transação aberta enquanto chama e‑mail, pagamentos ou APIs de terceiros, você segura locks por mais tempo do que o necessário. Isso aumenta esperas, timeouts e a chance de requisições se bloquearem.
Misturar jobs de background e ações do usuário sem uma única fonte da verdade cria estado em split‑brain. Um job retrya e atualiza uma linha enquanto um usuário a está editando, e agora ambos acham que foram os últimos a escrever.
Algumas “correções” que na verdade não consertam:
Se você está construindo com uma ferramenta chat‑to‑app como Koder.ai, as mesmas regras se aplicam: peça constraints do lado servidor e limites transacionais claros, não só proteções visuais mais bonitas.
Condições de corrida aparecem geralmente só sob tráfego real. Um passe pré‑release pode pegar os pontos de colisão mais comuns sem uma grande reescrita.
Comece pelo banco. Se algo precisa ser único (e‑mails, números de fatura, uma assinatura ativa por usuário), faça disso uma constraint única real, não uma checagem no app. Depois, garanta que seu código espere que a constraint às vezes falhe e retorne uma resposta clara e segura.
Em seguida, olhe para o estado. Toda mudança de status (Rascunho -> Submetido -> Aprovado) deve ser validada contra um conjunto explícito de transições permitidas. Se duas requisições tentarem mover o mesmo registro, a segunda deve ser rejeitada ou virar no‑op, não criar um estado intermediário.
Uma checklist prática pré‑release:
Se você constrói fluxos em Koder.ai, trate isso como critérios de aceitação: o app gerado deve falhar de forma segura sob repetições e concorrência, não apenas passar no happy path.
Dois funcionários abrem a mesma solicitação de compra. Ambos clicam Aprovar dentro de alguns segundos. Ambas as requisições chegam ao servidor.
O que pode dar errado é bagunçado: a solicitação é “aprovada” duas vezes, duas notificações saem e totais ligados a aprovações (orçamento usado, contagem diária de aprovações) podem aumentar em 2. As duas atualizações são válidas isoladamente, mas colidem.
Aqui está um plano de correção que funciona bem com um banco estilo PostgreSQL.
Adicione uma regra que garanta que só exista um registro de aprovação por solicitação. Por exemplo, armazene aprovações em uma tabela separada e imponha uma UNIQUE em request_id. Agora o segundo insert falha mesmo que o código do app tenha um bug.
Ao aprovar, faça a transição inteira em uma transação:
Se o segundo funcionário chegar atrasado, ele verá 0 linhas atualizadas ou um erro de unique constraint. De qualquer forma, só uma mudança vence.
Depois da correção, a primeira pessoa vê Aprovado e recebe a confirmação normal. A segunda vê uma mensagem amigável como: “Esta solicitação já foi aprovada por outra pessoa. Atualize para ver o status mais recente.” Sem repetição de notificações, sem falhas silenciosas.
Se você gera um fluxo CRUD em uma plataforma como Koder.ai (backend em Go com PostgreSQL), você pode embutir essas checagens na ação de aprovar uma vez e reaplicar o padrão para outras ações de “apenas um vencedor”.
Condições de corrida são mais fáceis de consertar quando você as trata como rotina repetível, não como uma caça ao bug única. Foque nos poucos caminhos de escrita que importam e deixe‑os aborrecidamente corretos antes de polir qualquer outra coisa.
Comece nomeando seus principais pontos de colisão. Em muitos apps CRUD é o mesmo trio: contadores (likes, inventário, saldos), mudanças de status (Rascunho -> Submetido -> Aprovado) e double submits (duplo clique, retries, redes lentas).
Uma rotina que funciona:
Se você está construindo no Koder.ai, o Planning Mode é um lugar prático para mapear cada fluxo de escrita em passos e regras antes de gerar mudanças em Go e PostgreSQL. Snapshots e rollback também são úteis quando você lança novas constraints ou comportamento de locks e quer um caminho rápido de volta caso apareça um caso de borda.
Com o tempo, isso vira hábito: toda nova feature de escrita recebe uma constraint, um plano transacional e um teste de concorrência. É assim que condições de corrida em apps CRUD deixam de ser surpresas.