Transações PostgreSQL no Node.js garantem que um conjunto de operações seja confirmado por inteiro ou revertido. Elas são fundamentais para pagamentos, estoque, pedidos, contabilidade e qualquer regra que atualize múltiplas tabelas.
Uma transação correta precisa usar a mesma conexão do início ao fim, manter duração curta, tratar rollback e escolher isolamento adequado. Abrir BEGIN em uma conexão e executar queries em outra não oferece atomicidade.
Transação básica
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE contas SET saldo = saldo - $1 WHERE id = $2',
[valor, origemId]
);
await client.query(
'UPDATE contas SET saldo = saldo + $1 WHERE id = $2',
[valor, destinoId]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}Helper transacional
export async function transaction(pool, callback) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}Repositories recebem client para garantir a mesma conexão.
Regra dentro da transação
await transaction(pool, async trx => {
const pedido = await pedidos.obterPorId(pedidoId, trx);
pedido.confirmar();
await pedidos.salvar(pedido, trx);
await outbox.adicionar(...pedido.removerEventos(), trx);
});A mudança e a Outbox são atômicas.
ACID
- Atomicidade: tudo ou nada;
- Consistência: constraints e regras permanecem válidas;
- Isolamento: transações concorrentes controlam visibilidade;
- Durabilidade: commit confirmado sobrevive conforme configuração.
Níveis de isolamento
PostgreSQL oferece Read Committed, Repeatable Read e Serializable. Read Uncommitted se comporta como Read Committed.
BEGIN ISOLATION LEVEL SERIALIZABLE;Isolamento maior reduz anomalias, mas pode aumentar aborts e necessidade de retry.
Read Committed
É o padrão. Cada comando vê dados confirmados antes do início daquele comando. Duas leituras na mesma transação podem observar valores diferentes.
Repeatable Read
A transação trabalha com snapshot consistente. Conflitos de atualização podem causar erro de serialização.
Serializable
Oferece comportamento equivalente a uma execução serial, mas algumas transações precisam ser repetidas.
Retry de serialização
async function withSerializableRetry(run, max = 3) {
for (let attempt = 0; attempt < max; attempt++) {
try {
return await run();
} catch (error) {
if (error.code !== '40001' || attempt === max - 1) throw error;
await new Promise(r => setTimeout(r, 20 * 2 ** attempt));
}
}
}O callback precisa ser seguro para repetição e não executar efeitos externos irreversíveis dentro da tentativa.
Locks de linha
SELECT id, saldo
FROM contas
WHERE id = $1
FOR UPDATE;O lock impede atualizações concorrentes na linha até o fim da transação.
Ordem de locks
Ao travar várias linhas, use ordem estável para reduzir deadlocks.
const ids = [origemId, destinoId].sort();Deadlocks
PostgreSQL detecta deadlock e aborta uma transação. Trate o erro, registre contexto e repita apenas quando a operação for segura.
Savepoints
SAVEPOINT antes_opcional;
-- operação
ROLLBACK TO SAVEPOINT antes_opcional;Savepoints permitem reverter parte da transação, mas não devem esconder fluxo confuso.
Constraints
Use constraints como última linha de defesa.
ALTER TABLE contas
ADD CONSTRAINT saldo_nao_negativo CHECK (saldo >= 0);Validação apenas na aplicação está sujeita a concorrência.
Transações longas
Não aguarde usuário, fila ou API remota com a transação aberta. Locks longos aumentam contenção e impedem manutenção.
Efeitos externos
Não é possível incluir um e-mail ou chamada HTTP na transação PostgreSQL. Grave intenção em Outbox e execute depois.
Veja Outbox Pattern no Node.js.
Timeouts
Configure statement_timeout, lock_timeout e idle_in_transaction_session_timeout conforme o workload.
Observabilidade
Monitore duração, rollbacks, deadlocks, espera por locks, conexões ocupadas e transações ociosas.
Testes
- commit completo;
- rollback no segundo passo;
- constraint violada;
- duas operações concorrentes;
- deadlock e retry;
- serialização;
- queda antes do commit;
- Outbox gravada junto.
Erros comuns
- queries usando conexões diferentes;
- esquecer rollback;
- transação longa;
- chamada HTTP dentro do lock;
- retry sem idempotência;
- ignorar constraints;
- não tratar deadlocks;
- liberar conexão antes do commit.
Conclusão
Transações PostgreSQL no Node.js protegem invariantes locais quando são curtas, usam a mesma conexão e tratam concorrência. Escolha o isolamento com base nas anomalias que realmente precisam ser evitadas.
Combine constraints, locks, retries e Outbox. Leia também Connection Pooling no Node.js.
Consulte a documentação de transações do PostgreSQL e a documentação de transações do node-postgres.




