Transações - gerenciamento manual vs automático

TL;DR

Uma transação agrupa operações de banco em uma unidade ACID — tudo persiste ou nada persiste. Em Node, cada ORM expõe dois modos: gerenciado (callback com commit/rollback automático) e manual (controle explícito). Prisma usa prisma.$transaction([...]) para batches sem dependência ou prisma.$transaction(async (tx) => {...}) para lógica interativa. TypeORM usa dataSource.transaction(async (manager) => {...}) ou QueryRunner manual, que exige release() no finally. Sequelize usa sequelize.transaction(async (t) => {...}) e requer { transaction: t } em cada operação. Drizzle usa db.transaction(async (tx) => {...}) — todas as queries dentro devem usar tx, nunca db. Confira index para o panorama completo do Galho 6.

Modos de transação por ORM

flowchart TD
    BEGIN["BEGIN\n(transação iniciada)"]
    OPS["Operações\n(INSERT · UPDATE · SELECT FOR UPDATE)"]
    ERR{"Erro?"}
    COMMIT["COMMIT\n(persistido)"]
    ROLLBACK["ROLLBACK\n(revertido)"]

    BEGIN --> OPS
    OPS --> ERR
    ERR -->|"Não"| COMMIT
    ERR -->|"Sim"| ROLLBACK

    subgraph Gerenciado["Modo gerenciado (automático)"]
        MG["prisma.$transaction(async tx)\ntypeorm dataSource.transaction(manager)\nsequelize.transaction(async t)\ndrizzle db.transaction(async tx)"]
    end

    subgraph Manual["Modo manual (QueryRunner/unmanaged)"]
        MM["typeorm QueryRunner\nsequelize startUnmanagedTransaction()"]
    end

    style BEGIN fill:#4A90D9,color:#fff
    style COMMIT fill:#4A90D9,color:#fff
    style ROLLBACK fill:#D0021B,color:#fff
    style OPS fill:#F5A623,color:#000

Como funciona

O que é uma transação

Uma transação de banco de dados é um conjunto de operações tratado como unidade indivisível: ou todas as operações são concluídas e persistidas, ou nenhuma produz efeito. O banco garante essa semântica pelo ciclo BEGIN → operações → COMMIT (ou ROLLBACK em caso de erro).

O acrônimo ACID descreve as quatro propriedades que tornam transações confiáveis:

PropriedadeDefinição
AtomicityTudo ou nada — se qualquer operação falha, todas as mudanças são revertidas
ConsistencyTransição de um estado válido para outro, respeitando constraints e regras do schema
IsolationTransações concorrentes não interferem umas nas outras
DurabilityApós COMMIT, as mudanças persistem mesmo em caso de crash imediato do servidor

O ciclo de vida em SQL serve de referência para o que os ORMs abstraem:

-- Ciclo básico de uma transação em PostgreSQL
BEGIN;
 
INSERT INTO transactions (from_account_id, to_account_id, amount)
VALUES (1, 2, 500.00);
 
UPDATE accounts SET balance = balance - 500.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 500.00 WHERE id = 2;
 
-- Se qualquer operação falhar → ROLLBACK desfaz tudo desde o BEGIN
COMMIT;

Quando transações são necessárias:

  • Múltiplas tabelas com dependência lógica (transferência financeira: débito + crédito + registro)
  • Criação de entidades compostas (pedido + itens + reserva de estoque)
  • Fluxos onde falha parcial deixaria dados inconsistentes

Custo de transações: transações adquirem locks nas linhas modificadas e os mantêm até COMMIT ou ROLLBACK. Transações longas bloqueiam outras transações concorrentes nas mesmas linhas e aumentam o risco de deadlock. Regra prática: transações devem ser curtas — apenas as operações necessárias, sem chamadas HTTP ou I/O externo enquanto os locks estão abertos.

Níveis de isolamento:

NívelDirty ReadNon-Repeatable ReadPhantom Read
Read Uncommittedpossívelpossívelpossível
Read Committed (padrão PostgreSQL)impossívelpossívelpossível
Repeatable Readimpossívelimpossívelpossível
Serializableimpossívelimpossívelimpossível
  • Dirty read: ler dados de uma transação que ainda não fez COMMIT — se ela fizer ROLLBACK, você leu dados que nunca existiram permanentemente
  • Non-repeatable read: ler a mesma linha duas vezes na mesma transação e obter resultados diferentes porque outra transação fez COMMIT entre as leituras
  • Phantom read: executar a mesma query de lista duas vezes e obter linhas diferentes porque outra transação inseriu ou removeu registros

Transações em Prisma

O Prisma oferece dois modos via $transaction: batch sequencial (array de PrismaPromise) e interativo (callback com cliente isolado tx).

// transactions/prisma-batch.ts
import { prisma } from '../prisma';
 
// Modo BATCH — array de PrismaPromise sem dependências entre operações
// Mais rápido: todas as queries enviadas na mesma round-trip ao banco
// Não usar quando o resultado de uma operação alimenta a próxima
export async function criarUsuarioComPerfil(
  nome: string,
  email: string,
  bio: string,
): Promise<void> {
  await prisma.$transaction([
    prisma.user.create({ data: { name: nome, email } }),
    prisma.profile.create({ data: { bio, userEmail: email } }),
  ]);
  // Se qualquer operação falhar → rollback de ambas
}
// transactions/prisma-interativa.ts
import { Prisma } from '@prisma/client';
import { prisma } from '../prisma';
 
// Modo INTERATIVO — callback com tx (Prisma Client isolado para a transação)
// Use quando o resultado de uma operação alimenta a próxima
// NUNCA use prisma.model dentro do callback — use apenas tx
export async function transferirCreditos(
  fromUserId: number,
  toUserId: number,
  quantidade: number,
): Promise<void> {
  await prisma.$transaction(
    async (tx) => {
      const usuarioOrigem = await tx.user.findUniqueOrThrow({
        where: { id: fromUserId },
        select: { id: true, credits: true },
      });
 
      if (usuarioOrigem.credits < quantidade) {
        throw new Error('Créditos insuficientes'); // → rollback automático
      }
 
      await tx.user.update({
        where: { id: fromUserId },
        data: { credits: { decrement: quantidade } },
      });
 
      await tx.user.update({
        where: { id: toUserId },
        data: { credits: { increment: quantidade } },
      });
 
      await tx.creditTransfer.create({
        data: { fromUserId, toUserId, amount: quantidade },
      });
    },
    {
      maxWait: 5_000,   // ms para aguardar conexão disponível no pool (padrão: 2000ms)
      timeout: 15_000,  // ms máximo para a transação inteira durar (padrão: 5000ms)
      isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead,
    },
  );
}

Batch vs interativa no Prisma

Use batch quando as operações são independentes entre si. Use interativa quando precisar ler dados antes de decidir o que escrever (verificar saldo antes de debitar, verificar estoque antes de reservar). O batch é mais performático — envia todas as queries numa única round-trip; a interativa mantém uma conexão aberta durante todo o callback.

Transações em TypeORM

O TypeORM oferece dois modos: dataSource.transaction() (gerenciado, commit/rollback automático) e QueryRunner (manual, com controle explícito do ciclo de vida).

// transactions/typeorm-gerenciado.ts
import { DataSource } from 'typeorm';
import { Account } from '../entities/account.entity';
import { TransactionRecord } from '../entities/transaction-record.entity';
 
// Modo GERENCIADO — manager é um EntityManager isolado para a transação
// NÃO use repositories injetados — eles usam conexão diferente e ficam fora da transação
export async function transferirSaldo(
  dataSource: DataSource,
  fromAccountId: string,
  toAccountId: string,
  valor: number,
): Promise<void> {
  await dataSource.transaction(async (manager) => {
    const contaOrigem = await manager.findOne(Account, {
      where: { id: fromAccountId },
      lock: { mode: 'pessimistic_write' }, // SELECT FOR UPDATE
    });
    const contaDestino = await manager.findOne(Account, {
      where: { id: toAccountId },
    });
 
    if (!contaOrigem || !contaDestino) throw new Error('Conta não encontrada');
    if (contaOrigem.balance < valor) throw new Error('Saldo insuficiente');
 
    contaOrigem.balance -= valor;
    contaDestino.balance += valor;
 
    await manager.save(contaOrigem);
    await manager.save(contaDestino);
    await manager.save(TransactionRecord, {
      fromAccountId,
      toAccountId,
      amount: valor,
      executedAt: new Date(),
    });
    // Saiu sem erro → commit automático
  });
}
// transactions/typeorm-query-runner.ts
import { DataSource, QueryRunner } from 'typeorm';
import { Order } from '../entities/order.entity';
import { Stock } from '../entities/stock.entity';
 
// Modo MANUAL com QueryRunner — controle granular do ciclo de vida
// OBRIGATÓRIO: queryRunner.release() no finally para devolver conexão ao pool
export async function criarPedidoComBaixaDeEstoque(
  dataSource: DataSource,
  produtoId: string,
  usuarioId: string,
  quantidade: number,
): Promise<Order> {
  const queryRunner: QueryRunner = dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction(); // BEGIN
 
  try {
    const estoque = await queryRunner.manager.findOne(Stock, {
      where: { productId: produtoId },
      lock: { mode: 'pessimistic_write' },
    });
 
    if (!estoque || estoque.quantity < quantidade) {
      throw new Error('Estoque insuficiente');
    }
 
    estoque.quantity -= quantidade;
    await queryRunner.manager.save(estoque);
 
    const pedido = queryRunner.manager.create(Order, {
      userId: usuarioId,
      productId: produtoId,
      quantity: quantidade,
    });
    const pedidoSalvo = await queryRunner.manager.save(pedido);
 
    await queryRunner.commitTransaction(); // COMMIT
    return pedidoSalvo;
  } catch (error) {
    await queryRunner.rollbackTransaction(); // ROLLBACK
    throw error;
  } finally {
    // SEMPRE no finally: libera a conexão de volta ao pool
    // Omitir esgota o pool silenciosamente sob carga
    await queryRunner.release();
  }
}

QueryRunner: release() obrigatório no finally

O QueryRunner mantém uma conexão dedicada do pool. Se release() estiver apenas no try — e não no finally — a conexão vaza toda vez que há erro. Com volume de requisições, o pool se esgota e todas as novas requisições ficam travadas com ConnectionTimeoutError.

Transações em Sequelize

O Sequelize oferece modo gerenciado (callback com commit/rollback automático) e não gerenciado (commit e rollback explícitos). Em ambos os modos, { transaction: t } deve ser passado a cada operação individualmente.

// transactions/sequelize-gerenciada.ts
import { Transaction } from 'sequelize';
import { sequelize } from '../database';
import { Account } from '../models/account';
import { TransferRecord } from '../models/transfer-record';
 
// Modo GERENCIADO — commit/rollback automático baseado no resultado do callback
// { transaction: t } em TODAS as operações — omitir faz a operação sair da transação
export async function transferirFundos(
  fromAccountId: number,
  toAccountId: number,
  valor: number,
): Promise<void> {
  await sequelize.transaction(async (t: Transaction) => {
    const contaOrigem = await Account.findOne({
      where: { id: fromAccountId },
      transaction: t,
      lock: Transaction.LOCK.UPDATE, // SELECT FOR UPDATE
    });
    const contaDestino = await Account.findOne({
      where: { id: toAccountId },
      transaction: t,
    });
 
    if (!contaOrigem || !contaDestino) {
      throw new Error('Conta não encontrada'); // → rollback automático
    }
    if (contaOrigem.dataValues.balance < valor) {
      throw new Error('Saldo insuficiente'); // → rollback automático
    }
 
    await contaOrigem.decrement('balance', { by: valor, transaction: t });
    await contaDestino.increment('balance', { by: valor, transaction: t });
    await TransferRecord.create(
      { fromAccountId, toAccountId, amount: valor },
      { transaction: t },
    );
    // Saiu sem erro → commit automático
  });
}
// transactions/sequelize-nao-gerenciada.ts
import { sequelize } from '../database';
import { User } from '../models/user';
 
// Modo NÃO GERENCIADO — commit e rollback explícitos
// Útil quando a decisão de commit ou rollback depende de lógica fora do callback
export async function importarUsuariosEmLote(
  usuarios: Array<{ name: string; email: string }>,
): Promise<User[]> {
  const t = await sequelize.startUnmanagedTransaction(); // Sequelize ≥ 6.29
  try {
    const criados = await User.bulkCreate(usuarios, { transaction: t });
    await t.commit();
    return criados;
  } catch (error) {
    await t.rollback();
    throw error;
  }
}

Transações em Drizzle

O Drizzle usa db.transaction(async (tx) => {...}) onde tx é um client isolado. Todas as queries dentro do callback devem usar tx — queries feitas via db dentro do callback usam uma conexão diferente e ficam fora da transação.

// transactions/drizzle-transfer.ts
import { eq, sql } from 'drizzle-orm';
import { db } from '../database';
import { accounts, transferRecords } from '../schema';
 
// db.transaction recebe callback com tx — use tx, nunca db, dentro do callback
export async function transferirSaldo(
  fromAccountId: string,
  toAccountId: string,
  valor: number,
): Promise<void> {
  await db.transaction(async (tx) => {
    const [contaOrigem] = await tx
      .select()
      .from(accounts)
      .where(eq(accounts.id, fromAccountId))
      .for('update'); // SELECT FOR UPDATE
 
    if (!contaOrigem || contaOrigem.balance < valor) {
      tx.rollback(); // lança TransactionRollbackError — não retorna, sai por exceção
    }
 
    // Use tx em TODAS as queries — nunca db dentro do callback
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} - ${valor}` })
      .where(eq(accounts.id, fromAccountId));
 
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} + ${valor}` })
      .where(eq(accounts.id, toAccountId));
 
    await tx.insert(transferRecords).values({
      fromAccountId,
      toAccountId,
      amount: valor,
      transferredAt: new Date(),
    });
  });
}
// transactions/drizzle-savepoint.ts
import { db } from '../database';
import { orders, orderItems, notifications } from '../schema';
 
// Transações aninhadas criam SAVEPOINTs no PostgreSQL
// Permite rollback parcial sem reverter a transação externa
export async function criarPedidoComNotificacao(
  usuarioId: string,
  itens: Array<{ productId: string; quantity: number; price: number }>,
): Promise<string> {
  return db.transaction(async (tx) => {
    const [pedido] = await tx
      .insert(orders)
      .values({ userId: usuarioId, status: 'pending' })
      .returning({ id: orders.id });
 
    for (const item of itens) {
      await tx.insert(orderItems).values({ orderId: pedido.id, ...item });
    }
 
    // tx.transaction() cria SAVEPOINT interno
    // Se a notificação falhar, apenas o savepoint é revertido
    // O pedido e seus itens permanecem intactos na transação externa
    try {
      await tx.transaction(async (sp) => {
        await sp.insert(notifications).values({
          userId: usuarioId,
          type: 'order_created',
          payload: JSON.stringify({ orderId: pedido.id }),
        });
      });
    } catch {
      // Falha na notificação não cancela o pedido — tolerância a falhas intencional
    }
 
    return pedido.id;
  });
}

Quando usar

CenárioAbordagem recomendada
Múltiplas tabelas com dependência (pedido + estoque)Modo gerenciado de qualquer ORM
Leitura condicional antes de escrita (verificar saldo)Prisma interativa, TypeORM dataSource.transaction, Sequelize gerenciada, Drizzle db.transaction
Batch de inserts independentesPrisma $transaction([...]), Sequelize bulkCreate com { transaction: t }
Rollback parcial com savepointsDrizzle tx.transaction() aninhado, Sequelize não gerenciada
Controle de nível de isolamentoTypeORM queryRunner.startTransaction('SERIALIZABLE'), Prisma isolationLevel, Sequelize { isolationLevel }
Injeção de repositórios TypeORM (NestJS)Injete DataSource no service, use dataSource.transaction() — repositórios injetados ficam fora da transação
Publicar evento + atualizar bancoOutbox pattern — persiste evento na mesma transação, publicação desacoplada

Regra de ouro

Se duas ou mais operações precisam ser atômicas — ou todas ocorrem ou nenhuma — use uma transação. Transações longas bloqueiam outras operações: mantenha-as curtas e sem I/O externo (HTTP calls, filas) enquanto os locks estão abertos.

Casos práticos

Cenário A — Transferência financeira com Prisma e lock pessimista

Sistema de carteiras digitais. Transferência de créditos entre dois usuários exige atomicidade total e lock pessimista para evitar double-spend em requisições concorrentes.

// src/wallets/transfer.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma } from '@prisma/client';
 
@Injectable()
export class TransferService {
  constructor(private readonly prisma: PrismaService) {}
 
  async transferCredits(
    fromUserId: string,
    toUserId: string,
    amount: number,
    idempotencyKey: string,
  ): Promise<{ transferId: string }> {
    return this.prisma.$transaction(
      async (tx) => {
        // Verifica idempotência primeiro (fora dos locks)
        const existing = await tx.transfer.findUnique({
          where: { idempotencyKey },
          select: { id: true },
        });
        if (existing) return { transferId: existing.id };
 
        // Lock pessimista: raw SQL para SELECT FOR UPDATE no Prisma
        const [fromWallet] = await tx.$queryRaw<[{ id: string; balance: number }]>(
          Prisma.sql`
            SELECT id, balance FROM "Wallet"
            WHERE "userId" = ${fromUserId}
            FOR UPDATE
          `
        );
 
        if (!fromWallet) throw new BadRequestException('Carteira de origem não encontrada');
        if (fromWallet.balance < amount) {
          throw new BadRequestException('Saldo insuficiente');
        }
 
        // Debita origem, credita destino
        await tx.wallet.update({
          where: { userId: fromUserId },
          data: { balance: { decrement: amount } },
        });
 
        await tx.wallet.upsert({
          where: { userId: toUserId },
          create: { userId: toUserId, balance: amount },
          update: { balance: { increment: amount } },
        });
 
        // Registra a transferência com idempotencyKey
        const transfer = await tx.transfer.create({
          data: {
            fromUserId,
            toUserId,
            amount,
            idempotencyKey,
          },
        });
 
        return { transferId: transfer.id };
      },
      {
        maxWait: 5_000,
        timeout: 15_000,
        isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
      }
    );
  }
}

Cenário B — Outbox pattern com Drizzle para publicação de eventos

Sistema de pedidos. Ao criar um pedido, um evento order.created precisa ser publicado no RabbitMQ. Para garantir consistência, o evento é persistido na mesma transação do pedido (Outbox pattern) — a publicação acontece de forma assíncrona.

// src/orders/create-order.ts
import { db } from '../db';
import { orders, orderItems, outboxEvents } from '../schema';
import { eq } from 'drizzle-orm';
 
interface CreateOrderInput {
  userId: string;
  items: Array<{ productId: string; quantity: number; price: number }>;
}
 
export async function createOrder(input: CreateOrderInput) {
  return db.transaction(async (tx) => {
    // 1. Cria o pedido
    const [order] = await tx
      .insert(orders)
      .values({ userId: input.userId, status: 'pending' })
      .returning({ id: orders.id });
 
    // 2. Cria os itens
    await tx.insert(orderItems).values(
      input.items.map((item) => ({
        orderId: order.id,
        ...item,
      }))
    );
 
    // 3. Persiste o evento no Outbox NA MESMA TRANSAÇÃO
    // Se a transação falhar, o evento NÃO é inserido
    await tx.insert(outboxEvents).values({
      aggregateId: order.id,
      aggregateType: 'Order',
      eventType: 'order.created',
      payload: JSON.stringify({
        orderId: order.id,
        userId: input.userId,
        itemCount: input.items.length,
      }),
    });
 
    return order;
  });
  // Worker separado lê a tabela outboxEvents e publica no RabbitMQ
  // Se a publicação falhar, o evento fica na tabela para retry
}

Por que Outbox: chamada direta ao RabbitMQ dentro da transação manteria o lock aberto durante a chamada HTTP ao broker. Com Outbox, a transação é curta (apenas banco) e a publicação é assíncrona.

Armadilhas comuns

Esquecer { transaction: t } em operações Sequelize

Em Sequelize, a transação não é propagada implicitamente — cada operação precisa receber { transaction: t } explicitamente. Uma operação sem esse parâmetro usa a conexão padrão do pool e executa fora da transação, mesmo dentro do callback gerenciado. O bug é silencioso: a operação sem transação persiste mesmo que o restante seja revertido por erro.

// PROBLEMA — Account.update sem { transaction: t } persiste fora da transação
await sequelize.transaction(async (t) => {
  await Account.findOne({ where: { id: 1 }, transaction: t }); // dentro ✓
  await Account.update({ balance: 0 }, { where: { id: 1 } });  // FORA ✗
  throw new Error('rollback'); // reverte o findOne, mas o update já persistiu
});
 
// CORRETO
await sequelize.transaction(async (t) => {
  await Account.findOne({ where: { id: 1 }, transaction: t });
  await Account.update({ balance: 0 }, { where: { id: 1 }, transaction: t }); // dentro ✓
  throw new Error('rollback'); // reverte ambas as operações
});

Prisma $transaction interativa com timeout insuficiente

O timeout padrão é 5 segundos. Em ambientes com latência ao banco (serverless, cold start), isso causa Transaction already closed: A commit cannot be executed on a closed transaction. Ajuste maxWait (aguardar conexão do pool) e timeout (duração máxima total) para o ambiente real.

// PROBLEMA — timeout padrão de 5s insuficiente em serverless
await prisma.$transaction(async (tx) => {
  await tx.order.create({ data: pedidoData });
  await tx.stock.update({ where: stockQuery, data: stockData });
});
 
// CORRETO — ajuste timeouts para o ambiente real
await prisma.$transaction(
  async (tx) => {
    await tx.order.create({ data: pedidoData });
    await tx.stock.update({ where: stockQuery, data: stockData });
  },
  { maxWait: 5_000, timeout: 15_000 },
);

QueryRunner TypeORM sem release() no finally

release() no try faz a conexão vazar ao pool toda vez que há erro — com volume de requisições, o pool se esgota e requisições ficam bloqueadas com ConnectionTimeoutError. O finally garante a liberação independente do resultado.

Usar db em vez de tx dentro de db.transaction() no Drizzle

Queries via db dentro do callback usam conexão diferente e executam fora da transação. A transação commitará ou fará rollback apenas das operações feitas via tx.

I/O externo dentro de transações

Chamadas HTTP (APIs de pagamento, webhooks, notificações) dentro de uma transação mantêm os locks durante todo o tempo do I/O externo — potencialmente centenas de milissegundos. Isso bloqueia outras transações concorrentes nas mesmas linhas. Use o Outbox pattern: persista o evento em uma tabela outbox na mesma transação e processe a publicação assincronamente.

Em entrevista

”What is a database transaction and what does ACID mean?”

A transaction is a unit of work that groups multiple operations into an atomic sequence — either all succeed and are committed, or none take effect. ACID describes the four guarantees that make transactions reliable: Atomicity means all-or-nothing, so if any operation fails, all changes are rolled back. Consistency means the transaction moves the database from one valid state to another, respecting all schema constraints. Isolation means concurrent transactions don’t see each other’s in-progress changes — each sees the database in a consistent state regardless of what other transactions are doing simultaneously. Durability means once committed, the changes survive even an immediate server crash. In Node, each ORM exposes a managed mode — Prisma’s $transaction(async (tx) => {}), TypeORM’s dataSource.transaction(async (manager) => {}), Sequelize’s sequelize.transaction(async (t) => {}), Drizzle’s db.transaction(async (tx) => {}) — where commit and rollback happen automatically based on whether the callback throws.

”What is the difference between optimistic and pessimistic locking, and when do you use each?”

Pessimistic locking acquires an exclusive row lock at read time using SELECT FOR UPDATE, holding it until the transaction commits. This prevents concurrent transactions from modifying the locked rows, making it safe when conflicts are frequent — financial transfers where multiple concurrent operations may target the same account balance. The trade-off is reduced concurrency and deadlock risk. Optimistic locking reads without a lock, but before committing the update it verifies the data hasn’t changed since the read — typically by checking a version counter or timestamp. If the data was modified by another transaction, the update fails and the caller retries. This is appropriate when conflicts are rare, such as editing a user profile. In TypeORM, optimistic locking is built in via the @VersionColumn() decorator — the ORM automatically increments the version on each update and rejects updates where the version in the database no longer matches the version that was read.

Vocabulário

PortuguêsEnglish
TransaçãoTransaction
AtomicidadeAtomicity
ConsistênciaConsistency
IsolamentoIsolation
DurabilidadeDurability
Nível de isolamentoIsolation level
Leitura sujaDirty read
Leitura não repetívelNon-repeatable read
Leitura fantasmaPhantom read
Bloqueio pessimistaPessimistic locking
Bloqueio otimistaOptimistic locking
Ponto de salvamentoSavepoint
ImpasseDeadlock
Padrão de caixa de saídaOutbox pattern
Vazamento de conexãoConnection pool leak

O que vem a seguir

Transações garantem atomicidade nas operações de escrita, mas performance de leitura também é crítica. 09 - Paginação - offset, cursor e keyset explora como paginar eficientemente sem full table scans — um problema que se intensifica quando transações de longo prazo mantêm snapshots que bloqueiam vacuum no PostgreSQL. Para um comparativo consolidado de todos os padrões (N+1, migrations, transações, paginação) com snippets side-by-side dos 4 ORMs, 10 - Cheatsheet e decision tree de ORMs é a referência rápida de revisão. O padrão Outbox desta nota conecta com a detecção de N+1 em 06 - N+1 queries - detecção e DataLoader — o worker do Outbox também precisa de eager loading para processar eventos eficientemente.

Veja também

Fontes