Voltar para Artigos
Back-end7 min de leitura

Transações no TypeORM: Atomicidade e o Padrão Unit of Work

Como garantir integridade de dados usando QueryRunner. O problema do vazamento de abstração ao injetar o EntityManager nos Use Cases e como o padrão Unit of Work (UoW) resolve o isolamento arquitetural.

12 de agosto de 2026

Se um Use Case desconta o saldo do cliente (UPDATE) e falha logo antes de criar o registro do pedido (INSERT), você acabou de roubar o dinheiro do usuário sem entregar o produto. Operações que envolvem múltiplas tabelas e dependem umas das outras exigem Transações: ou tudo funciona (Commit), ou tudo é desfeito (Rollback).

A Abordagem Básica (e Acoplada) com QueryRunner

A documentação do TypeORM ensina a usar o QueryRunner para controlar a transação manualmente. O problema dessa abordagem é que ela vaza conceitos do TypeORM direto para a camada de regra de negócio (Use Case), quebrando a Arquitetura Limpa.

CheckoutUseCase.ts (Acoplado ao TypeORM)
import { dataSource } from '../infra/typeorm';

export class CheckoutUseCase {
  async execute(orderId: string, userId: string) {
    const queryRunner = dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // PROBLEMA 1: O Use Case conhece entidades do TypeORM (OrderModel)
      const orderRepo = queryRunner.manager.getRepository(OrderModel);
      const userRepo = queryRunner.manager.getRepository(UserModel);

      const user = await userRepo.findOneBy({ id: userId });
      user.balance -= 100;
      await userRepo.save(user);

      const order = orderRepo.create({ status: 'PAID' });
      await orderRepo.save(order);

      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

A Solução Elegante: Unit of Work (UoW)

O problema do QueryRunner exposto no Use Case não é apenas estético: ele torna o Use Case intestavível de forma unitária e acopla a lógica de negócio ao TypeORM. Se você quiser trocar o ORM por outro (ou escrever um FakeUnitOfWork para testes), precisa reescrever o Use Case inteiro. Para não acoplar o Use Case ao TypeORM e manter o Repository Pattern, usamos o padrão Unit of Work.

Para não acoplar o Use Case ao TypeORM e manter o Repository Pattern, usamos o padrão Unit of Work. Ele abstrai o conceito de transação em uma interface genérica: o Use Case chama uow.transaction(callback) e recebe repositórios isolados dentro daquele contexto.

IUnitOfWork.ts
import type { IUsersRepository } from '../repositories/IUsersRepository';
import type { IOrdersRepository } from '../repositories/IOrdersRepository';

export interface IWorkContext {
  usersRepo: IUsersRepository;
  ordersRepo: IOrdersRepository;
}

export interface IUnitOfWork {
  // Recebe um callback que executa dentro da transação
  transaction<T>(work: (ctx: IWorkContext) => Promise<T>): Promise<T>;
}
CheckoutUseCase.ts (Desacoplado)
import { injectable, inject } from 'tsyringe';
import type { IUnitOfWork } from '../domain/providers/IUnitOfWork';

@injectable()
export class CheckoutUseCase {
  constructor(
    @inject('UnitOfWork')
    private uow: IUnitOfWork
  ) {}

  async execute(orderId: string, userId: string) {
    // A transação é iniciada aqui. Se o callback lançar erro, faz rollback automático.
    await this.uow.transaction(async ({ usersRepo, ordersRepo }) => {
      // Usamos os repositórios injetados no contexto da transação
      const user = await usersRepo.findById(userId);
      if (!user || user.balance < 100) throw new Error('Saldo insuficiente');

      user.balance -= 100;
      await usersRepo.save(user); // Faz parte da transação

      const order = await ordersRepo.findById(orderId);
      order.status = 'PAID';
      await ordersRepo.save(order); // Faz parte da transação
      
      // Se der sucesso, o UoW faz o commitTransaction por trás dos panos
    });
  }
}

Implementando o Unit of Work no TypeORM

O TypeORMUnitOfWork encapsula todo o QueryRunner por baixo dos panos. O ponto crítico é instanciar os repositórios passando o queryRunner.manager — sem isso, o repositório usaria um EntityManager diferente, fora da transação, e as operações não seriam atômicas. O Try/Catch/Finally garante que o queryRunner sempre seja liberado de volta ao pool de conexões, mesmo se o rollback falhar.

TypeORMUnitOfWork.ts
import { DataSource } from 'typeorm';
import { inject, injectable } from 'tsyringe';
import type { IUnitOfWork, IWorkContext } from '../../../domain/providers/IUnitOfWork';
import { TypeORMUsersRepository } from '../repositories/TypeORMUsersRepository';
import { TypeORMOrdersRepository } from '../repositories/TypeORMOrdersRepository';

@injectable()
export class TypeORMUnitOfWork implements IUnitOfWork {
  constructor(
    @inject('DataSource')
    private dataSource: DataSource
  ) {}

  async transaction<T>(work: (ctx: IWorkContext) => Promise<T>): Promise<T> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Instancia os repositórios passando o entityManager da transação
      // IMPORTANTE: o repositório precisa estar preparado para receber o manager opcional
      const ctx: IWorkContext = {
        usersRepo: new TypeORMUsersRepository(queryRunner.manager),
        ordersRepo: new TypeORMOrdersRepository(queryRunner.manager),
      };

      // Executa a regra de negócio
      const result = await work(ctx);

      await queryRunner.commitTransaction();
      return result;
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

Cuidado com deadlocks: Transações bloqueiam as linhas editadas (e às vezes tabelas inteiras) até que o commit ou rollback aconteça. NUNCA faça requisições externas demoradas (ex: chamar a API do Stripe) DENTRO do bloco da transação. Faça a chamada HTTP antes, e abra a transação apenas no momento exato de salvar os dados no banco.

Conclusão

Transações são obrigatórias para escritas atômicas múltiplas, mas poluem a camada de domínio se feitas diretamente com o QueryRunner do TypeORM. O Unit of Work atua como uma fachada: isola o banco de dados e fornece um escopo transacional limpo para os Use Cases, mantendo a arquitetura testável e permitindo a criação de um FakeUnitOfWork (em memória) para os testes unitários.