Cansado de Rollbacks Falhando? Descubra Como o Design Pattern Unit of Work Pode Salvar Suas Transações no Java
Imagine este cenário:
Você precisa salvar um pedido no banco, baixar os itens do estoque, atualizar o status do cliente e, de quebra, registrar um log de auditoria.
Tudo parece perfeito… até a terceira etapa falhar — e você percebe que os dados já foram parcialmente salvos.
Pronto. Inconsistência instaurada.
Se você já passou por isso, a boa notícia é que existe um padrão de projeto pensado exatamente para resolver esse tipo de problema: o Unit of Work.
Neste artigo, você vai entender:
- O que é o pattern Unit of Work
- Quais problemas ele resolve (e por que você provavelmente já precisou dele)
- Como implementá-lo em Java, inclusive com frameworks como Spring
- Boas práticas para aplicar esse padrão de forma limpa
- Dicas para testar e evitar bugs difíceis de rastrear
🧨 O Problema: Operações Que Quebram No Meio
Em sistemas reais, dificilmente uma transação é composta de uma única ação.
Vamos ver um exemplo:
pedidoService.salvar(pedido);
estoqueService.baixarEstoque(pedido.getItens());
clienteService.atualizarStatus(pedido.getCliente());
auditoriaService.registrar(pedido);
Se tudo der certo, beleza. Mas e se a linha 3 lançar uma exceção?
O pedido e o estoque já foram salvos, mas o status e o log não foram.
Você acabou de criar um estado inconsistente no banco de dados.
💡 A Solução: Design Pattern Unit of Work
O Unit of Work é um padrão de projeto criado para:
Agrupar um conjunto de operações em uma única “unidade de trabalho”, garantindo que tudo seja executado ou nada seja aplicado.
Em outras palavras:
Ou tudo é salvo com sucesso, ou tudo é revertido.
🔍 Definição simples:
O Unit of Work mantém o rastreamento das alterações feitas em um conjunto de objetos e coordena a gravação dessas mudanças como uma única transação.
Ele é muito usado em:
- Frameworks ORM (como Hibernate)
- Serviços com múltiplas chamadas de banco
- Processos que exigem consistência transacional forte
🔧 Como Implementar Unit of Work em Java (com e sem Spring)
✅ Usando Spring Boot (o mais comum)
O Spring já oferece suporte embutido ao conceito de Unit of Work através da anotação @Transactional
.
Exemplo real:
@Service
public class PedidoService {
private final PedidoRepository pedidoRepo;
private final EstoqueService estoqueService;
private final AuditoriaService auditoriaService;
@Transactional
public void processarPedido(Pedido pedido) {
pedidoRepo.salvar(pedido);
estoqueService.baixarEstoque(pedido.getItens());
auditoriaService.registrarPedido(pedido);
}
}
Se qualquer linha lançar uma exceção, o Spring faz rollback automático de tudo que alterou no banco.
❗️Importante: Regras para que o rollback funcione corretamente
- A exceção precisa ser uma
RuntimeException
(unchecked) - O método anotado com
@Transactional
deve estar sendo chamado de fora da mesma classe (por outro bean) - As operações precisam ser executadas dentro do mesmo
EntityManager
(ou seja, mesma transação)
🛠️ Criando sua própria abstração de Unit of Work (fora do Spring)
Se você quiser uma abordagem manual ou mais explícita (por exemplo, num microserviço leve sem Spring), pode implementar um Unit of Work assim:
public class UnitOfWork {
private final Connection connection;
public UnitOfWork(Connection connection) {
this.connection = connection;
}
public void execute(TransactionCallback callback) {
try {
connection.setAutoCommit(false);
callback.run(connection);
connection.commit();
} catch (Exception e) {
try {
connection.rollback();
} catch (SQLException rollbackEx) {
throw new RuntimeException("Erro no rollback", rollbackEx);
}
throw new RuntimeException("Erro na transação", e);
} finally {
try {
connection.setAutoCommit(true);
} catch (SQLException ignore) {}
}
}
}
Uso:
unitOfWork.execute(conn -> {
pedidoRepo.salvar(conn, pedido);
estoqueRepo.baixar(conn, pedido.getItens());
logRepo.registrar(conn, pedido);
});
Essa abordagem dá mais controle — ideal para aplicações fora do ecossistema Spring.
🎯 Vantagens do Padrão Unit of Work
✔ Consistência garantida: Tudo ou nada. Sem estados quebrados no meio.
✔ Agrupamento lógico: Você pensa em “casos de uso” e não em “queries isoladas”.
✔ Desacoplamento da persistência: Os objetos podem mudar sem alterar o fluxo.
✔ Testabilidade: Fica mais fácil testar a lógica sem persistir dados reais.
✔ Rollback automático: Menos chances de esquecer de desfazer algo.
📦 Use Case Real com Unit of Work + Clean Architecture
@Service
public class EmitirNotaFiscalUseCase implements UseCase<EmitirNotaFiscalInput, Void> {
@Transactional
@Override
public Void execute(EmitirNotaFiscalInput input) {
NotaFiscal nf = nfeService.gerarNota(input.getPedido());
estoqueGateway.baixarEstoque(input.getPedido().getItens());
nfeRepository.salvar(nf);
sefazGateway.enviarXML(nf);
return null;
}
}
Nesse caso, a anotação
@Transactional
no Use Case define que todo o conjunto de ações deve ser atômico.
❓E com microserviços? Funciona?
Aí o buraco é mais embaixo.
Unit of Work tradicional NÃO funciona entre microserviços.
Quando você está lidando com múltiplos serviços (estoque em um serviço, nota fiscal em outro), você precisa de outras estratégias:
- Padrão SAGA (com compensações)
- Outbox Pattern
- Mensageria + Retry
Exemplo:
- O serviço de pedido aprova o pedido
- Publica evento
PedidoAprovado
- Serviço de estoque consome e baixa estoque
- Serviço de nota fiscal consome e emite nota
- Se algo falha, um evento de compensação é enviado (ex:
ReporEstoque
)
É um tipo de Unit of Work distribuído, mas eventualmente consistente.
✅ Dicas de Ouro para Aplicar Unit of Work Corretamente
- Sempre que seu caso de uso envolver mais de um repositório, pense em Unit of Work.
- Evite fazer chamadas de rede (como enviar e-mail ou chamar outra API) dentro de transações — pois isso pode prender a conexão do banco.
- Se precisar chamar APIs externas, faça depois da transação ou use eventos.
- Em testes unitários, use mocks e valide se a transação foi completa.
- Evite repositórios que já fazem commit por conta própria (isso quebra o controle do UoW).
🏁 Conclusão
O padrão Unit of Work pode ser a peça que faltava para organizar seu fluxo de persistência, garantir consistência e evitar aquele famoso “bug fantasma” que só aparece quando a operação quebra na metade.
Seja com Spring ou em aplicações leves, entender e aplicar esse pattern vai te colocar em um outro nível de maturidade como desenvolvedor.
Comece agora mesmo:
- Identifique seus casos de uso que envolvem múltiplas ações
- Agrupe essas ações dentro de um método transacional
- Use
@Transactional
ou implemente seu próprio Unit of Work - E pare de sofrer com rollback que não funciona!