Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Unit of Work no Node.js

Atualizado em: 2 de setembro de 2026

Rack de servidores processando fluxos de dados no Node.js

O Unit of Work no Node.js coordena várias alterações de persistência como uma única unidade. Em vez de cada repository abrir e confirmar sua própria transação, o caso de uso recebe um conjunto de repositories vinculados à mesma conexão e decide quando confirmar ou reverter.

Esse padrão é útil quando uma operação atualiza múltiplos agregados, registra eventos em uma outbox ou precisa garantir que todas as mudanças sejam atômicas. Sem uma unidade compartilhada, o pedido pode ser salvo enquanto o estoque falha, deixando o sistema parcialmente atualizado.

Neste guia, você aprenderá a implementar Unit of Work com PostgreSQL e pg, expor repositories transacionais, evitar transações aninhadas, tratar erros, integrar outbox, testes, retries e observabilidade.

O que é Unit of Work?

Unit of Work acompanha ou coordena alterações realizadas durante uma operação e confirma todas em conjunto. O padrão é descrito no catálogo Patterns of Enterprise Application Architecture.

Em aplicações Node.js com PostgreSQL, uma implementação simples pode abrir um client do pool, iniciar BEGIN, criar repositories com esse client e executar um callback.

Para os fundamentos de banco, consulte Transações PostgreSQL no Node.js. Para contratos de persistência, veja Repository Pattern no Node.js.

Problema com transações separadas

await orderRepository.save(order);
await inventoryRepository.reserve(items);
await outboxRepository.add(event);

Se cada método usa pool.query(), cada query pode executar em uma conexão diferente. Uma falha no terceiro passo não desfaz os anteriores.

Implementação básica

export class PgUnitOfWork {
  constructor(private readonly pool: Pool) {}

  async run<T>(
    callback: (context: TransactionContext) => Promise<T>
  ): Promise<T> {
    const client = await this.pool.connect();

    try {
      await client.query('BEGIN');

      const context = this.createContext(client);
      const result = await callback(context);

      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }
}

O callback nunca recebe o pool; recebe apenas componentes vinculados ao client.

Contexto transacional

type TransactionContext = {
  orders: OrderRepository;
  inventory: InventoryRepository;
  outbox: OutboxRepository;
};

A implementação cria cada repository:

private createContext(client: PoolClient): TransactionContext {
  return {
    orders: new PgOrderRepository(client),
    inventory: new PgInventoryRepository(client),
    outbox: new PgOutboxRepository(client)
  };
}

Interface Queryable

export interface Queryable {
  query<T>(text: string, values?: unknown[]): Promise<QueryResult<T>>;
}

Tanto Pool quanto PoolClient podem implementar o contrato necessário. Repositories ficam independentes do tipo concreto.

Caso de uso

export class CreateOrder {
  constructor(private readonly uow: UnitOfWork) {}

  async execute(input: CreateOrderInput) {
    return this.uow.run(async tx => {
      const order = Order.create(input);

      await tx.inventory.reserve(order.items);
      await tx.orders.add(order);
      await tx.outbox.add(order.pullEvents());

      return order.toDTO();
    });
  }
}

Se reserva, inserção ou outbox falhar, tudo é revertido.

Transação pertence ao caso de uso

O Service Layer conhece a unidade de negócio. O repository conhece apenas persistência. Por isso, a decisão de iniciar e terminar a transação deve ficar na camada de aplicação.

Consulte Service Layer no Node.js.

Não exponha commit ao domínio

Entidades e serviços de domínio não devem chamar commit(). Persistência é uma responsabilidade externa.

Callback versus objeto manual

Uma API com callback reduz o risco de esquecer rollback ou release:

await uow.run(async tx => {
  // operação
});

Uma API manual:

const uow = await factory.begin();
try {
  // operação
  await uow.commit();
} catch (error) {
  await uow.rollback();
} finally {
  await uow.release();
}

É mais flexível, mas mais fácil de usar incorretamente.

Rollback com erro no rollback

} catch (error) {
  try {
    await client.query('ROLLBACK');
  } catch (rollbackError) {
    logger.error({ rollbackError }, 'Falha no rollback');
  }

  throw error;
}

Preserve o erro original e registre a falha secundária.

Commit ambíguo

Se a conexão cai durante COMMIT, a aplicação pode não saber se o banco confirmou. Use idempotência e chaves de negócio para consultar o estado antes de repetir.

Consulte Idempotência em APIs Node.js.

Isolamento

await client.query(
  'BEGIN ISOLATION LEVEL SERIALIZABLE'
);

Permita configurar isolamento por caso de uso, mas use defaults seguros e documentados.

uow.run(callback, {
  isolationLevel: 'serializable'
});

Read-only

BEGIN TRANSACTION READ ONLY;

Consultas que precisam de snapshot consistente podem usar transação somente leitura, embora muitas leituras simples não precisem de Unit of Work.

Timeouts locais

await client.query(`SET LOCAL lock_timeout = '2s'`);
await client.query(`SET LOCAL statement_timeout = '5s'`);

Use valores apropriados ao caso de uso. Um timeout aborta a transação e exige rollback.

Transações curtas

Não execute dentro da Unit of Work:

  • chamadas HTTP longas;
  • upload para S3;
  • envio de e-mail;
  • espera por usuário;
  • processamento pesado;
  • sleep de retry;
  • publicação síncrona em broker.

Essas operações mantêm conexão e locks ocupados.

Outbox transacional

O padrão Outbox é uma aplicação natural:

await tx.orders.save(order);
await tx.outbox.add({
  id: crypto.randomUUID(),
  type: 'order.created',
  aggregateId: order.id,
  payload: order.toEventPayload()
});

Um worker publica depois. Veja Outbox Pattern no Node.js.

Eventos de domínio

Entidades podem acumular eventos, mas a Unit of Work decide como persistir:

const events = order.pullEvents();
await tx.outbox.addMany(events);

Limpe os eventos somente depois que foram adicionados ao contexto transacional.

Repositories lazy

Se há muitos repositories, crie sob demanda:

get orders() {
  return this._orders ??=
    new PgOrderRepository(this.client);
}

Isso evita instanciar componentes não utilizados.

Não use singleton transacional

Uma Unit of Work por requisição ou operação não deve ser compartilhada globalmente. Isso misturaria conexões e estados concorrentes.

AsyncLocalStorage

É possível armazenar o client no contexto assíncrono:

storage.run({ client }, callback);

Repositories consultam o client atual. Essa abordagem reduz parâmetros, mas esconde dependências e exige testes cuidadosos. Consulte AsyncLocalStorage no Node.js.

Preferência por dependência explícita

Passar repositories no contexto é mais claro. Use AsyncLocalStorage somente quando o benefício operacional superar o custo de dependência implícita.

Transações aninhadas

PostgreSQL não possui transações aninhadas independentes. Se um serviço interno chama outra Unit of Work, pode abrir uma conexão diferente e quebrar atomicidade.

Propagação obrigatória

Uma política pode reutilizar o contexto existente:

uow.run(callback, { propagation: 'required' });

Se já existe transação, usa a atual; caso contrário, cria uma.

Savepoints

Para uma suboperação opcional:

await client.query('SAVEPOINT optional_step');
try {
  await optionalOperation();
  await client.query('RELEASE SAVEPOINT optional_step');
} catch {
  await client.query('ROLLBACK TO SAVEPOINT optional_step');
}

Não use savepoint para esconder falhas que deveriam abortar o caso de uso.

ORMs

Prisma, Sequelize, TypeORM, Drizzle e Kysely possuem APIs de transação. A Unit of Work pode encapsular o client ou transaction object do ORM.

Exemplo com Kysely

await db.transaction().execute(async trx => {
  const tx = createRepositories(trx);
  await callback(tx);
});

Consulte Kysely com TypeScript e SQL.

Exemplo com Drizzle

await db.transaction(async txDb => {
  const context = createRepositories(txDb);
  await callback(context);
});

Consulte Drizzle ORM com PostgreSQL.

MongoDB

MongoDB também possui sessões e transações em replica sets. A Unit of Work pode carregar a session e garantir que todas as operações a recebam.

Não use transação para tudo

Uma única inserção atômica ou update condicional não precisa de uma abstração pesada. Use Unit of Work quando há coordenação real.

Retries de serialização

SQLSTATE 40001 e deadlock 40P01 podem ser repetidos com limites:

for (let attempt = 0; attempt < 3; attempt += 1) {
  try {
    return await uow.run(callback, {
      isolationLevel: 'serializable'
    });
  } catch (error) {
    if (!isRetryable(error) || attempt === 2) throw error;
    await delay(backoff(attempt));
  }
}

O callback não pode executar efeitos externos não idempotentes.

Locks

A Unit of Work não elimina deadlocks. Repositories devem adquirir locks em ordem consistente. Consulte Lock Pessimista no Node.js.

Observabilidade

Registre:

  • nome do caso de uso;
  • duração total;
  • tempo para obter conexão;
  • commit ou rollback;
  • isolamento;
  • SQLSTATE;
  • retries;
  • deadlocks;
  • timeouts.

Não registre parâmetros sensíveis.

Métricas

Use counters e histograms:

unit_of_work_duration_seconds
unit_of_work_commits_total
unit_of_work_rollbacks_total
unit_of_work_retries_total

Use labels de baixa cardinalidade, como nome do caso de uso.

Teste unitário

Um fake pode executar callback diretamente com repositories em memória:

class InMemoryUnitOfWork {
  async run(callback) {
    const snapshot = structuredClone(this.state);

    try {
      return await callback(this.repositories);
    } catch (error) {
      this.state = snapshot;
      throw error;
    }
  }
}

O fake ajuda na regra, mas não reproduz banco real.

Teste de integração

Confirme:

  • commit persiste todas as mudanças;
  • erro reverte todas;
  • conexão retorna ao pool;
  • timeout provoca rollback;
  • erro no outbox reverte agregado;
  • dois casos concorrentes respeitam locks;
  • retry serializable não duplica efeitos.

Falha antes do BEGIN

Se obter conexão falhar, não há rollback. Classifique como indisponibilidade do banco e aplique retry fora da Unit of Work quando apropriado.

Falha depois do COMMIT

Não execute lógica crítica depois do commit dentro da mesma função sem considerar que o banco já confirmou. Eventos externos devem vir da outbox.

Erros comuns

  • Repository usa pool: escapa da transação.
  • HTTP dentro do callback: transação fica longa.
  • Unit of Work global: operações se misturam.
  • Commit manual esquecido: conexão fica inconsistente.
  • Transação aninhada: atomicidade é quebrada.
  • Retry indiscriminado: efeitos externos duplicam.
  • Sem timeout: locks ficam presos.

Boas práticas

  • Use callback seguro.
  • Crie repositories com o mesmo client.
  • Coordene no Service Layer.
  • Mantenha transações curtas.
  • Use outbox para eventos.
  • Configure timeouts locais.
  • Evite transações aninhadas.
  • Repita apenas erros transitórios.
  • Monitore commits e rollbacks.
  • Teste com banco real.

Conclusão

O Unit of Work no Node.js garante que vários repositories participem da mesma transação. O caso de uso passa a controlar uma unidade atômica, evitando estados parciais quando estoque, pedido e outbox precisam mudar juntos.

Uma implementação com callback, client exclusivo, repositories transacionais e timeouts é suficiente para muitos sistemas. Quando combinada com Service Layer, Repository Pattern, outbox e testes de rollback, a Unit of Work transforma operações compostas em fluxos previsíveis e seguros.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita