Uma transação é uma sequência de operações que deve ser executada por completo ou totalmente revertida. Em um monólito com banco de dados único, isso é amplamente gerenciado pelo próprio sistema. Em sistemas distribuídos que abrangem múltiplos serviços, bancos de dados e APIs externas, torna-se um dos problemas mais complexos da engenharia de software. Este artigo cobre os conceitos fundamentais, a implementação prática em Node.js e PHP, e os padrões aplicáveis quando se opera além das fronteiras de um único serviço.

Entendendo Transações e Seus Princípios Fundamentais

As transações garantem quatro propriedades, agrupadas sob o acrônimo ACID:

Atomicidade significa que toda operação em uma transação faz parte de uma unidade indivisível. Se uma única etapa falhar, tudo é revertido. Consistência significa que o sistema está sempre em um estado válido quando uma transação é concluída. Sem gravações parciais, sem violações de restrições. Isolamento significa que transações concorrentes não enxergam os estados intermediários umas das outras. Durabilidade significa que, uma vez confirmada a transação, os dados sobrevivem mesmo que o processo falhe imediatamente após.

Essas propriedades são fáceis de ignorar em bancos de dados relacionais. Tornam-se muito mais difíceis de garantir quando sua transação precisa abranger um banco de dados PostgreSQL, uma coleção MongoDB e uma chamada a uma API de pagamento.

Transações em Bancos de Dados SQL

Bancos de dados relacionais como MySQL, PostgreSQL e SQL Server suportam transações ACID nativamente. Em Node.js, usando um cliente PostgreSQL:

const { Client } = require('pg');

(async () => {
  const client = new Client();
  await client.connect();
  
  try {
    await client.query('BEGIN');
    await client.query('UPDATE accounts SET balance = balance - 100 WHERE id = $1', [1]);
    await client.query('UPDATE accounts SET balance = balance + 100 WHERE id = $2', [2]);
    await client.query('COMMIT');
  } catch (err) {
    await client.query('ROLLBACK');
    console.error('Transaction error:', err);
  } finally {
    client.end();
  }
})();

O helper de transações do Laravel faz o mesmo com menos boilerplate:

DB::transaction(function () {
    DB::table('accounts')->where('id', 1)->decrement('balance', 100);
    DB::table('accounts')->where('id', 2)->increment('balance', 100);
});

O Laravel gerencia BEGIN, COMMIT e ROLLBACK automaticamente e faz novas tentativas em caso de deadlock por padrão.

Transações em Bancos de Dados NoSQL

O MongoDB adicionou transações ACID multi-documento na versão 4.0. Antes disso, só era possível gravar atomicamente um único documento. Transações multi-documento requerem um replica set ou um cluster fragmentado.

Em Node.js, usando o driver nativo do MongoDB:

const session = client.startSession();
session.startTransaction();

try {
  await accountsCollection.updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session });
  await accountsCollection.updateOne({ _id: 2 }, { $inc: { balance: 100 } }, { session });
  await session.commitTransaction();
} catch (err) {
  await session.abortTransaction();
  console.error('Transaction error:', err);
} finally {
  await session.endSession();
}

Em ambientes serverless como o AWS Lambda, o gerenciamento de conexões é ainda mais crítico. Funções serverless escalam horizontalmente, de modo que cada instância pode abrir sua própria conexão com o banco de dados. Reutilize conexões sempre que possível e trate a contenção de forma explícita no tratamento de erros.

Transações Distribuídas e Microsserviços

Quando uma ação do usuário afeta múltiplos serviços, uma transação em banco de dados único não é uma opção. Dois padrões tratam esse problema de formas distintas.

Two-Phase Commit (2PC)

O 2PC coordena uma transação entre múltiplos gerenciadores de recursos dividindo o commit em duas fases: preparação e confirmação. Cada participante confirma que está pronto para fazer o commit, e então um coordenador envia o sinal de commit final. O 2PC garante consistência, mas é lento, exige que todos os participantes estejam disponíveis simultaneamente e pode travar indefinidamente se o coordenador falhar entre as fases. É teoricamente elegante, mas raramente a escolha certa para sistemas distribuídos de alto tráfego.

Padrão Saga

O padrão Saga modela uma transação distribuída como uma sequência de transações locais. Cada serviço executa sua própria transação local e publica um evento. Se uma etapa posterior falhar, cada serviço que já confirmou executa uma transação compensatória para desfazer sua alteração.

sequenceDiagram
    participant Service A
    participant Service B
    participant Service C

    Service A->>Service B: Executar transação local
    Service B->>Service C: Executar transação local
    alt Sucesso
        Service C->>Service A: OK
    else Falha
        Service C->>Service B: Acionar ação compensatória
        Service B->>Service A: Acionar ação compensatória
    end

As Sagas mantêm os serviços fracamente acoplados. Cada serviço conhece apenas sua própria transação local e sua ação compensatória. A contrapartida é que o sistema passa a ter consistência eventual, não imediata. Existe uma janela em que o Serviço A já confirmou, mas o Serviço C ainda não.

Garantindo o Controle de Concorrência

Múltiplas transações concorrentes acessando os mesmos dados precisam de uma estratégia para evitar conflitos.

O bloqueio otimista assume que conflitos são raros. Adicione um campo de versão aos seus registros, incremente-o a cada atualização e rejeite atualizações que apresentem um número de versão desatualizado. A transação rejeitada tenta novamente com dados atualizados. Isso funciona bem quando conflitos reais são pouco frequentes.

O bloqueio pessimista adquire um bloqueio explícito antes de ler os dados, impedindo que outras transações os modifiquem até que o bloqueio seja liberado. É mais seguro para cenários de alto conflito, mas reduz o throughput sob carga concorrente.

Os níveis de isolamento do banco de dados oferecem controle sobre quais anomalias você está disposto a aceitar. Read Committed evita leituras sujas. Repeatable Read evita leituras não repetíveis. Serializable evita leituras fantasmas. Níveis de isolamento mais altos reduzem a concorrência, portanto escolha com base nos requisitos reais do negócio, em vez de adotar por padrão a configuração mais restritiva.

Para seções críticas em Node.js, o processamento baseado em filas ou operações atômicas no Redis podem garantir uma ordem de execução serializada sem bloqueio no nível de banco de dados.

Transações em Paradigmas Serverless e DevOps

Funções serverless são stateless e efêmeras, o que complica o gerenciamento de transações. Alguns padrões ajudam:

Transações orientadas a eventos modelam mudanças de estado como eventos. Se um handler de evento falhar, um evento compensatório desfaz a mudança de estado anterior. Isso funciona bem com serviços como Amazon Kinesis, Google Pub/Sub ou Azure Event Hubs.

Operações idempotentes garantem que processar o mesmo evento múltiplas vezes produz o mesmo resultado. Isso elimina a necessidade de transações distribuídas rígidas em muitos casos, pois retentar uma operação com falha é seguro.

O AWS Step Functions e o Azure Durable Functions oferecem orquestração integrada para encadear operações com lógica de retry, tratamento de timeout e compensação. Eles não fornecem garantias ACID, mas tornam o estado de um fluxo de trabalho multi-etapa observável e gerenciável.

Melhores Práticas para Implementação de Transações

Projete para falhas parciais. Em sistemas distribuídos, falhas parciais não são casos extremos. Construa fluxos de trabalho que as tratem explicitamente, com comportamento de retry definido e ações compensatórias.

Mantenha o escopo da transação pequeno. Quanto mais tempo uma transação mantém bloqueios, maior a chance de conflito. Realize qualquer computação cara fora do limite da transação e execute o conjunto mínimo de operações dentro dela.

Registre tudo. Inícios de transações, commits, rollbacks e conflitos de concorrência devem ser todos registrados em log. Sem esses dados, diagnosticar problemas em produção é puro chute. AWS CloudWatch, Google Stackdriver e Azure Monitor integram-se sem grande overhead.

Teste a concorrência explicitamente. Testes unitários raramente cobrem conflitos de transação. Escreva testes de integração que simulem requisições concorrentes aos mesmos recursos e verifique se o sistema as trata corretamente.

Aplicando Transações a Stacks Tecnológicas Diversas

Desenvolvedores de Node.js e TypeScript dispõem do TypeORM e do Sequelize para transações SQL, além do driver nativo do MongoDB para NoSQL. Sempre trate rejeições de promise dentro de blocos de transação. Uma rejeição não tratada que pula o rollback deixa o banco de dados em estado inconsistente.

Laravel e CodeIgniter oferecem APIs de transação limpas com rollback automático em caso de exceção. Os helpers integrados tratam bem os casos mais comuns. Concentre os esforços de teste nos caminhos de rollback, que são fáceis de ignorar nos testes manuais, mas críticos para a corretude.

As plataformas Cloud oferecem cada uma seu próprio armazenamento com suporte a transações: DynamoDB Transactions na AWS, Cloud Spanner no Google Cloud e gravações multi-região do Cosmos DB no Azure. Cada uma possui diferentes garantias de consistência e perfis de custo, portanto verifique os tradeoffs antes de se comprometer com uma delas.

Casos de Uso Estratégicos para Transações

Serviços financeiros são o exemplo canônico. Transferências, saques e chamadas a gateways de pagamento exigem que a sequência completa seja executada ou que nada seja alterado. Padrões Saga funcionam bem aqui quando a operação abrange múltiplas contas ou sistemas de terceiros.

O checkout de e-commerce é outro caso comum: criar o registro do pedido, decrementar o estoque e cobrar o gateway de pagamento. Esses passos tocam sistemas diferentes e frequentemente requerem padrões de transação distribuída para tratar falhas parciais de forma limpa.

Fluxos de trabalho baseados em mensagens usando Kafka ou RabbitMQ podem implementar comportamento transacional por meio de semânticas de entrega exactly-once e consumidores idempotentes. Após o processamento de uma mensagem, o consumidor registra esse fato, de modo que a reentrega não cause operações duplicadas.

Conclusão

Transações são simples em um único banco de dados relacional e genuinamente difíceis em serviços distribuídos. As propriedades ACID fornecem o vocabulário para raciocinar sobre as garantias, e o padrão Saga oferece uma ferramenta prática quando essas garantias não podem abranger um único limite de banco de dados. Comece pela abordagem mais simples que atenda aos seus requisitos de consistência, mantenha o escopo das transações estreito e garanta que seus caminhos de concorrência e falha sejam testados com o mesmo rigor que seus caminhos de sucesso.