Optimistic Locking no Node.js detecta alterações concorrentes sem manter locks de banco durante todo o tempo. Cada registro possui uma versão. A atualização só é aceita se a versão ainda for a esperada.
O padrão funciona bem quando conflitos são raros e operações precisam permanecer rápidas. Em vez de bloquear outros usuários, ele rejeita uma gravação obsoleta e permite recarregar, repetir ou pedir decisão ao usuário.
O problema da atualização perdida
Duas requisições leem o mesmo pedido com versão 4. A primeira altera o endereço e salva. A segunda altera o status usando o snapshot antigo e sobrescreve a mudança anterior.
Campo de versão
ALTER TABLE pedidos
ADD COLUMN version integer NOT NULL DEFAULT 1;Update condicional
const result = await client.query(
`UPDATE pedidos
SET status = $1,
version = version + 1,
updated_at = now()
WHERE id = $2 AND version = $3
RETURNING *`,
[novoStatus, pedidoId, expectedVersion]
);
if (result.rowCount === 0) {
throw new ConcurrencyError('Pedido alterado por outra operação');
}A condição evita sobrescrever um registro mais novo.
Entidade com versão
export class Pedido {
constructor({ id, status, version }) {
this.id = id;
this.status = status;
this.version = version;
}
confirmar() {
if (this.status !== 'rascunho') throw new Error('Estado inválido');
this.status = 'confirmado';
}
}O repository usa version durante a persistência e atualiza o objeto após sucesso.
ETag em APIs HTTP
Exponha a versão como ETag.
res.setHeader('ETag', `"pedido-${pedido.version}"`);O cliente envia If-Match no update. Se a versão divergir, retorne 412 Precondition Failed ou 409 Conflict, conforme o contrato.
Veja ETag no Node.js.
Retry automático
Retry é seguro apenas quando a operação pode ser refeita sobre o estado novo. Um incremento ou decisão dependente do snapshot precisa ser recalculado.
for (let attempt = 0; attempt < 3; attempt++) {
const pedido = await repo.obter(id);
pedido.confirmar();
try {
return await repo.salvar(pedido);
} catch (error) {
if (!(error instanceof ConcurrencyError) || attempt === 2) throw error;
}
}Merge
Em interfaces de edição, talvez seja possível mesclar campos independentes. Não faça merge silencioso de regras sensíveis. Mostre diferenças ao usuário quando necessário.
Versão por aggregate
A versão deve proteger a unidade de consistência. Se itens pertencem ao aggregate Pedido, atualizações internas devem incrementar a mesma versão.
Timestamp como versão
updated_at pode perder precisão ou depender de relógio. Um inteiro monotônico é mais claro. UUIDs ou tokens opacos também funcionam quando usados corretamente.
Event Sourcing
Event stores usam a versão esperada do aggregate para impedir dois eventos na mesma posição.
UNIQUE (aggregate_id, aggregate_version)Leia Event Sourcing no Node.js.
Mensagens fora de ordem
Projeções podem guardar a versão aplicada e ignorar eventos antigos.
UPDATE pedidos_resumo
SET status = $1, version = $2
WHERE id = $3 AND version < $2;Quando usar
- conflitos são incomuns;
- edições duram mais que uma transação;
- não é aceitável manter lock enquanto o usuário pensa;
- APIs recebem updates concorrentes;
- aggregates possuem versão natural.
Quando não basta
Se muitos workers disputam a mesma linha e quase todas as tentativas conflitam, locks pessimistas, serialização ou redesenho de particionamento podem ser melhores.
Observabilidade
Meça conflitos por entidade, rota, tenant e horário. Uma taxa alta pode indicar hot record ou fluxo mal particionado.
Testes
- update com versão atual;
- versão obsoleta;
- duas requisições simultâneas;
- retry seguro;
- ETag e If-Match;
- evento fora de ordem;
- merge de campos independentes.
Erros comuns
- ler versão e não usá-la no WHERE;
- incrementar antes do sucesso;
- retry sem recarregar;
- timestamp impreciso;
- retornar sucesso após rowCount zero;
- proteger apenas tabela principal e ignorar aggregate;
- não medir conflitos.
Conclusão
Optimistic Locking no Node.js protege contra atualizações perdidas sem manter locks longos. A versão transforma concorrência silenciosa em conflito explícito.
Use update condicional, erros claros, ETags e retry apenas quando seguro. Combine com Transações PostgreSQL no Node.js e Aggregates no Node.js.
Consulte a documentação RETURNING do PostgreSQL.



