Bloqueio otimista para evitar atualizações perdidas em apps web
Aprenda como o bloqueio otimista evita atualizações perdidas quando duas abas ou usuários editam o mesmo registro, usando colunas de versão ou ETags e tratamento claro de conflitos.

O problema da atualização perdida em linguagem simples
Uma “atualização perdida” acontece quando duas pessoas (ou duas abas do navegador) editam a mesma coisa, e o segundo salvamento silenciosamente sobrescreve o primeiro. Ninguém vê um erro. Ambos acham que a mudança foi salva. Mas apenas o último salvamento fica no banco de dados.
Um exemplo simples: você abre seu perfil em uma aba e muda o nome exibido de “Sam” para “Samantha”, mas ainda não clica em Salvar. Em outra aba, você altera o e-mail e clica em Salvar. Depois você volta para a primeira aba e clica em Salvar. Se o app usa “last write wins”, essa submissão mais antiga pode sobrescrever a mudança de e-mail mais recente, mesmo que você nunca tenha mexido no campo de e-mail na primeira aba.
Isso muitas vezes passa despercebido porque tudo parece normal. O servidor retorna “200 OK”, a interface mostra um toast de sucesso e a página é atualizada. O bug só aparece depois, quando alguém percebe que uma configuração reverteu, um endereço mudou de volta ou uma atualização administrativa desapareceu. Aí parece aleatório, e os clientes podem acusar o software de “instável”.
Você vai ver isso principalmente em telas CRUD do dia a dia onde as pessoas mantêm uma página aberta por um tempo: perfis e configurações de conta, painéis administrativos (usuários, produtos, permissões), páginas de configuração de time (funções, informações de cobrança), editores de conteúdo (títulos, metadados, descrições) e qualquer formulário de edição que carrega dados uma vez e salva depois.
“Last write wins” é arriscado porque trata todo salvamento como igualmente atual, mesmo quando vem de dados obsoletos. Também cria um problema de confiança: os usuários fizeram a coisa certa, mas o sistema descartou o trabalho deles silenciosamente.
O bloqueio otimista (optimistic locking) é uma forma comum de prevenir isso. Quando você salva, o servidor verifica se o registro mudou desde que você o carregou. Se mudou, o app impede a sobrescrita e pede que você resolva o conflito em vez de fingir que está tudo certo.
Situações comuns que causam sobrescritas silenciosas
Sobrescritas silenciosas acontecem quando o app permite que alguém edite dados com base em um snapshot antigo e depois salva sem checar o que mudou enquanto isso. O resultado é “o último salvamento vence”, mesmo quando esse último é o incorreto.
A causa mais comum é ter duas abas (ou janelas) abertas na mesma página. Você atualiza um registro na Aba A, esquece que a Aba B continua aberta e a Aba B salva depois, colocando valores antigos de volta sem saber. Isso aparece muito em painéis administrativos, dashboards e páginas de “Editar perfil” que ficam abertas por horas.
Também acontece quando duas pessoas diferentes mexem no mesmo registro. Pense numa nota do cliente, num status de pedido ou num endereço. Pessoa 1 muda o telefone, Pessoa 2 muda instruções de entrega, e quem clicar em Salvar por último pode apagar a outra mudança se o app enviar o registro completo em vez de apenas os campos alterados.
Redes lentas ou instáveis pioram a situação. Um salvamento vindo de um trem ou de uma conexão móvel fraca pode chegar atrasado. Se o servidor aceitar como corrente, pode sobrescrever edições mais novas que foram salvas enquanto a primeira requisição estava em trânsito. O modo offline tem o mesmo risco: você pode editar localmente, voltar a ficar online e enviar mudanças que já estão obsoletas.
Retries podem reenviar uma atualização antiga também: o usuário clica duas vezes em Salvar, um job em background re-tenta depois de um timeout usando dados antigos, uma fila reexecuta uma mensagem após um crash, ou uma biblioteca cliente automaticamente reenvia uma requisição que já foi aplicada.
São exatamente esses casos em que o bloqueio otimista compensa. Adicione uma checagem simples de versão (ou um If-Match com ETag) e o servidor pode detectar “você editou uma cópia antiga” e recusar o salvamento em vez de sobrescrever silenciosamente.
O que é bloqueio otimista e como funciona
O bloqueio otimista previne o problema de atualização perdida. Em vez de bloquear pessoas que editam, ele deixa todo mundo editar livremente e só checa por conflitos quando alguém tenta salvar.
É mais fácil entender comparando com bloqueio pessimista. Bloqueio pessimista é como colocar uma placa “não mexa” em um registro enquanto você edita. Evita conflitos, mas cria espera, timeouts e problemas quando alguém “deixa a página aberta”. O bloqueio otimista assume que conflitos são raros, então evita bloqueios e só impede o salvamento quando um conflito realmente acontece.
Uma “versão” é um pequeno dado que muda sempre que uma linha ou documento muda. Em uma linha de banco de dados, costuma ser uma coluna inteira como version que começa em 1 e incrementa a cada atualização. Em APIs, também pode ser um ETag, que é uma impressão digital do estado atual.
O fluxo básico é:
- Ao carregar um registro, você também lê sua versão atual.
- Ao salvar, você envia de volta a versão que viu originalmente.
- A atualização só ocorre se a versão armazenada ainda corresponder.
- Se corresponder, o registro é atualizado e a versão é incrementada.
- Se não corresponder, o salvamento é rejeitado como conflito.
Esse descompasso é o ponto inteiro. O sistema está dizendo: “Alguém (ou outra aba) mudou isso depois que você carregou.” O app pode então mostrar uma mensagem clara e oferecer um próximo passo seguro, como recarregar, revisar diferenças ou copiar suas alterações antes de tentar novamente. A sobrescrita nunca acontece silenciosamente.
Isso se encaixa na maioria dos apps CRUD porque a maioria dos usuários não está editando exatamente o mesmo registro ao mesmo tempo. Você mantém a UI responsiva (sem locks mantidos enquanto alguém pensa) e ainda protege os dados.
Um exemplo mental rápido: você abre um formulário de perfil em duas abas. A Aba A salva primeiro, elevando a versão de 3 para 4. A Aba B tenta salvar com versão 3. O banco (ou API) recusa, e a Aba B precisa atualizar ou mesclar. Essa pequena checagem de versão transforma perda de dados oculta em um conflito visível e solucionável.
Passo a passo: abordagem com coluna de versão
Uma coluna de versão é a maneira mais simples de parar sobrescritas silenciosas em um app CRUD. Armazene um número em cada linha, envie-o ao cliente quando ele ler o registro e exija que o cliente o envie de volta ao salvar. Se o número mudou desde que carregaram a página, rejeite a atualização.
1) Adicione um campo de versão à tabela
Adicione uma coluna inteira, frequentemente chamada version, começando em 1. (Usar updated_at pode funcionar como backup, mas timestamps podem complicar com fuso horário e edições muito rápidas.)
ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
2) Transporte a versão pela sua API
Faça a versão acompanhar o registro de ponta a ponta. Na leitura, inclua version na resposta da API para que a UI possa armazená-la. No formulário, mantenha essa versão no estado (mesmo que esteja oculta). Na atualização, exija que o cliente envie a última versão vista (o corpo da requisição é a forma mais simples).
Agora o servidor pode dizer se o cliente está salvando uma cópia antiga.
3) Atualize somente se a versão ainda corresponder
Esse é o núcleo do bloqueio otimista: atualize a linha apenas quando id e version corresponderem, então incremente a versão.
Um padrão comum é uma query atômica:
UPDATE documents
SET title = ?, body = ?, version = version + 1
WHERE id = ? AND version = ?;
Se a query atualizar 0 linhas, a versão não correspondeu. Retorne um conflito (frequentemente HTTP 409) e inclua o registro mais recente (e sua nova versão) para que a UI mostre o que mudou.
4) Incremente ao sucesso, rejeite no descompasso
Quando o salvamento acontecer com sucesso, a resposta deve incluir a nova versão para que o cliente esteja pronto para a próxima edição. Quando falhar, não faça retry automático. Isso pode transformar um sinal claro de “você editou uma cópia antiga” em mais confusão, e ainda assim levar a sobrescritas.
Passo a passo: usar ETags com cabeçalhos If-Match
Um ETag é uma pequena impressão do estado atual de um recurso. Se o recurso muda, a impressão muda também. Isso o torna adequado para bloqueio otimista quando você não quer adicionar uma coluna de versão, ou quando prefere que o servidor decida o que significa “mesmo estado”.
1) Retorne um ETag na leitura (GET)
Quando um cliente carrega um registro para editar, sua API deveria retornar o registro mais um ETag que corresponda exatamente a esse estado. Você pode computar o ETag a partir de uma versão da linha, de um updated_at ou de um hash do JSON que retorna.
Um fluxo simples:
- Cliente envia
GET /items/123 - Servidor responde com o corpo JSON e um cabeçalho
ETag: "abc123" - Cliente armazena esse ETag junto dos dados do formulário que está editando
2) Exigir If-Match na escrita (PUT/PATCH)
Quando o usuário salva, o cliente inclui o ETag que viu originalmente. Isso diz ao servidor: “Só aplique minha atualização se o recurso ainda estiver no estado que editei.”
- Cliente envia
PATCH /items/123com cabeçalhoIf-Match: "abc123" - Servidor compara
If-Matchcom o ETag atual do item 123 - Se baterem, aplica a mudança e retorna o recurso atualizado com um novo ETag
Se não baterem, há um conflito. Não aceite a gravação silenciosamente.
3) Retorne a resposta correta no conflito
A maioria das APIs retorna 412 Precondition Failed quando If-Match não corresponde (o mais preciso), ou 409 Conflict se preferir uma resposta mais geral.
Inclua detalhes suficientes para o cliente se recuperar: um código/mesagem curta, e frequentemente a versão mais recente do recurso (mais seu novo ETag) para que a UI mostre o que mudou.
Quando ETags são mais adequados que uma coluna de versão
ETags são úteis quando você não pode alterar facilmente o esquema do banco, quando vários backends podem atualizar o mesmo recurso, ou quando você já usa semânticas de cache. Também funcionam bem quando o “estado do recurso” não é apenas uma linha, como um documento montado a partir de várias tabelas.
Exemplo: duas abas editam o mesmo perfil. A Aba A carrega a página e recebe ETag "v1". A Aba B salva uma mudança primeiro, tornando o ETag do perfil "v2". Quando a Aba A tenta salvar com If-Match: "v1", o servidor retorna 412. A UI pode então pedir ao usuário para recarregar, ou mostrar uma pequena tela de merge em vez de sobrescrever a Aba B.
Como tratar conflitos sem frustrar os usuários
Um conflito não é um erro aos olhos do usuário. É uma surpresa. Seu trabalho é explicar o que aconteceu e ajudá-lo a manter o trabalho feito.
Use uma mensagem simples que nomeie o problema e o impacto: “Este registro foi alterado em outro lugar enquanto você o editava. Suas alterações ainda não foram salvas.” Evite texto vago como “409 Conflict” ou “Falha na atualização”. As pessoas precisam saber que não foi culpa delas.
Dê escolhas simples (e torne a segura a mais fácil)
A maioria dos apps precisa das mesmas três opções. Mantenha-as simples e faça o caminho mais seguro ser o padrão.
- Recarregar o mais recente: buscar a versão mais nova e mostrá-la.
- Manter minhas edições: preservar a entrada não salva no formulário para que o usuário possa reaplicá-la.
- Sobrescrever mesmo assim: permitir apenas quando o usuário confirmar claramente que quer substituir a mudança de outra pessoa.
Faça “Recarregar o mais recente” a ação principal. “Sobrescrever mesmo assim” deve ser secundário e explícito, com uma confirmação que diga o que será sobrescrito.
Preserve a entrada não salva do usuário
A forma mais rápida de perder confiança é apagar um formulário após um conflito. Mantenha o que o usuário digitou, mesmo se você recarregar o registro.
Um padrão prático: armazene as edições pendentes do usuário separadamente (estado local ou rascunho), recarregue os dados mais recentes do servidor e então re-popule o formulário usando os valores do rascunho onde fizerem sentido. Se possível, destaque campos que diferem da versão do servidor para que o usuário veja rapidamente o que mudou.
Quando recarregar é suficiente vs quando é preciso uma UI de merge
Um recarregamento simples é suficiente quando o formulário é curto, as mudanças geralmente são pequenas e o custo de reescrever é baixo.
Você provavelmente precisa de uma UI de merge quando os usuários editam texto longo (descrições, notas, políticas), quando muitos campos podem mudar ao mesmo tempo (tabelas de preço, setups em múltiplas etapas) ou quando conflitos acontecem com frequência (times ocupados, telas administrativas compartilhadas).
Um fluxo realista: duas pessoas editam o mesmo cadastro de cliente. Uma atualiza o telefone e salva. A outra muda o endereço e tenta salvar depois. Com bom tratamento de conflito, a segunda pessoa vê: “O telefone foi alterado por outra pessoa. Sua alteração de endereço ainda está aqui.” Ela pode recarregar e salvar de novo sem reescrever tudo.
Erros comuns e armadilhas a evitar
Bloqueio otimista é simples na teoria, mas alguns erros podem anular o objetivo. A maioria aparece só depois que usuários reais começam a trabalhar em várias abas, ou quando um novo caminho de escrita é adicionado numa entrega apressada.
Armadilhas que causam sobrescritas silenciosas
- Usar
updated_atcomo “versão” quando timestamps não são precisos o suficiente. Se duas atualizações caírem no mesmo segundo (ou o banco arredondar), ambas podem parecer válidas e uma pode sobrescrever a outra. - Adicionar checagem de versão/ETag em um endpoint mas esquecer em outro. Por exemplo, a tela principal de edição usa a checagem, mas um toggle rápido, autosave, painel admin ou job em background atualiza o mesmo registro sem ela.
- Mudar a versão em leituras, ou em gravações que não alteram campos gerenciados pelo usuário. Se você incrementar a versão quando alguém apenas visualiza a página, cria conflitos que parecem aleatórios e injustos.
- Capturar o conflito e tentar novamente automaticamente sem interação do usuário. Retries cegos podem transformar um sinal claro de “você editou uma cópia antiga” em um loop confuso.
- Fazer atualizações em lote que ignoram a checagem de concorrência. Uma única instrução SQL ou uma ferramenta de batch que atualize várias linhas pode desconsiderar a condição de versão e apagar edições recentes.
Hábitos pequenos que evitam grandes bugs
Seja consistente: se um registro é editável, todo caminho de escrita deve ou (1) requerer versão/ETag, ou (2) ser explicitamente projetado como override forçado e logado como tal.
Mantenha a versionização estável. A versão deve avançar apenas quando você aceita uma atualização baseada na versão mais recente conhecida. Se seu app tem gravações de efeitos colaterais (recalculando contadores, sincronizando metadados, atualizando last_seen), considere movê-las para uma tabela separada para não criar conflitos de edição desnecessários.
Um check rápido de realidade: abra o mesmo formulário de edição em duas abas, salve na Aba A e depois salve na Aba B. Se a Aba B tiver sucesso sem um conflito claro, você ainda tem um caminho de atualização perdida em algum lugar.
Verificações rápidas antes de enviar para produção
Trate bloqueio otimista como um recurso que você pode quebrar de propósito. Se dois salvamentos competirem, você deve receber um conflito claro em vez de uma sobrescrita silenciosa.
Comece com o teste real mais fácil: abra o mesmo registro em duas abas do navegador. Altere campos diferentes em cada aba, depois salve a Aba A e a Aba B. A Aba B não deveria “vencer” silenciosamente. Ela deveria receber uma resposta de conflito (frequentemente HTTP 409) e uma mensagem que diga ao app o que aconteceu.
Redes lentas são onde esses bugs se escondem. Use a limitação de rede do navegador (ou adicione um delay artificial no servidor) para que um salvamento demore alguns segundos. Enquanto estiver em voo, salve de outra aba. Quando a requisição atrasada finalmente retornar, ela deve falhar com segurança.
Um checklist rápido pré-envio:
- Duas abas: edite o mesmo registro, salve em ambas as abas, confirme que o segundo salvamento recebe um conflito.
- Salvamento lento: atrase uma requisição, salve outra, confirme que a atrasada é rejeitada.
- Retomar no mobile: edite, deixe o app em background, volte depois e salve, confirme que versão/ETag ainda é checada.
- Recuperação offline: perca conexão no meio da edição, reconecte e salve, confirme que você trata conflitos em vez de sobrescrever.
- Segurança de rascunho: após um conflito, confirme que a UI preserva o texto não salvo do usuário.
Verifique também os detalhes que importam para os usuários. Quando um conflito ocorrer, a resposta deve ser específica o bastante para o cliente reagir: um status claro, uma mensagem legível por humanos e, de preferência, a versão mais recente do servidor para que a UI possa mostrar “suas mudanças” vs “versão atual”.
Um exemplo realista: duas pessoas editando os mesmos dados
Duas colegas, Maya e Jordan, estão atualizando a mesma regra de precificação em um painel administrativo. A regra diz: “10% de desconto quando o total do carrinho passa de $100.” Maya quer mudar para $120. Jordan quer mudar o desconto para 15%.
As duas abrem a página de edição às 10:00. Cada aba carrega a regra atual. Naquele momento, o registro tem version = 7 (ou um valor equivalente).
O que acontece sem bloqueio
Maya salva primeiro às 10:02. O servidor grava “threshold = 120” no banco.
Jordan salva às 10:03. O navegador dele ainda tem os valores antigos do formulário, então a atualização grava “discount = 15%” e também envia o valor antigo do threshold de quando ele carregou a página. O resultado é uma sobrescrita silenciosa: a mudança de Maya some, e ninguém recebe um aviso. A UI costuma mostrar “Salvo” nas duas vezes, então o time confia em dados errados.
O que acontece com coluna de versão
Com bloqueio otimista, ambas as atualizações incluem a versão de onde partiram.
- A requisição da Maya diz: “atualize esta regra onde id=123 e version=7”
- O servidor atualiza a linha e aumenta para version 8
- A requisição do Jordan também diz: “onde version=7”
- O banco não encontra correspondência (porque o registro agora é version 8)
- O servidor retorna uma resposta de conflito em vez de sobrescrever
O que o usuário vê: Jordan recebe uma mensagem clara como “Esta regra de precificação foi alterada por outra pessoa. Reveja a versão mais recente antes de salvar.” A página recarrega os dados mais novos (threshold 120, version 8). As edições não salvas do Jordan podem ser mantidas localmente para que ele reaplique “15%” e salve de novo.
O que é preservado: o registro salvo mais recente permanece intacto, e a mudança pretendida do Jordan não se perde. Ela é apenas adiada até que ele confirme contra a versão mais nova.
Próximos passos: implantar com segurança (e pedir ajuda se precisar)
Comece escolhendo a abordagem que mais combina com o funcionamento atual do seu app. Uma coluna de versão é frequentemente a mais simples quando você controla o banco e o ORM e a maioria das atualizações passa pelo seu servidor. ETags com If-Match são uma boa escolha quando você tem uma API REST limpa, múltiplos clientes ou fortes necessidades de cache.
Implemente por partes. Escolha um fluxo de edição de alto valor (perfis, pedidos, configurações) e adicione bloqueio otimista de ponta a ponta: leitura, edição, atualização e uma mensagem clara de conflito. Quando esse caminho estiver sólido, repita para o próximo recurso.
Um checklist de rollout seguro:
- Adicione a versão (ou ETag) a toda resposta de leitura e a todo pedido de atualização.
- Retorne uma resposta de conflito clara quando as versões não corresponderem (sem sobrescritas silenciosas).
- Mostre uma escolha de UI simples: recarregar, manter suas mudanças ou tentar novamente após revisar.
- Logue conflitos com tipo de recurso e frequência para identificar pontos quentes.
- Adicione um ou dois testes que simulem duas abas atualizando o mesmo registro.
Não pare só na API. Se a UI apenas disser “Falha ao salvar”, as pessoas vão tentar de novo e ainda podem sobrescrever mudanças de outros. Dê contexto: o que mudou e o que podem fazer. Para formulários simples, um padrão forte é recarregar os dados mais recentes e preservar o rascunho do usuário para que ele possa reaplicar as mudanças.
Se você herdou um app CRUD gerado por IA, vale a pena checar se todo caminho de atualização segue a mesma regra de concorrência. Times como FixMyMess (fixmymess.ai) se especializam em transformar protótipos gerados por IA em software pronto para produção, e uma auditoria rápida frequentemente encontra checagens de versão ou ETag faltantes antes que causem perda real de dados.