Como Isolar Regras de Negócio em Projetos Java com Clean Architecture — Guia Prático com Exemplos Reais
Se você está desenvolvendo sistemas em Java e se pergunta:
“Como lidar com regras de negócio que mudam o tempo todo?”
Então este artigo é para você.
Neste guia, vamos explorar como projetar um sistema Java modular e flexível, usando Clean Architecture, DDD leve e boas práticas para isolar regras de negócio da infraestrutura.
Você vai aprender:
- Por que as regras de negócio mudam tanto
- Como isolar essas regras do resto do sistema
- Como aplicar isso com uma arquitetura limpa e escalável
- Exemplos reais em Java com Use Cases, DTOs e controle de transações
🎯 Por Que Isolar as Regras de Negócio?
No desenvolvimento de software, as regras de negócio são o que mais muda ao longo do tempo.
Por isso, deixar essas regras espalhadas entre controllers, repositórios, serviços e bancos de dados cria uma bagunça difícil de manter e testar.
A solução? Separar bem as responsabilidades.
🧠 Princípios Fundamentais
1. SRP — Single Responsibility Principle
Cada classe deve ter apenas um motivo para mudar.
Ou seja: controle de acesso, persistência e lógica de cálculo devem viver em lugares diferentes.
2. Arquitetura em Camadas / Hexagonal / Clean Architecture
A lógica de negócio deve ficar no centro (domínio), desacoplada de:
- Banco de dados
- Frameworks
- APIs externas
3. Inversão de Dependência
As regras de negócio definem interfaces.
As implementações concretas ficam nas camadas externas, e são injetadas.
📁 Estrutura de Projeto Sugerida
src/
├── domain/
│ ├── entities/ → Objetos do negócio
│ ├── services/ → Regras de negócio
│ └── ports/ → Interfaces (gateways e repositórios)
├── application/
│ ├── usecases/ → Orquestra os serviços
│ └── dtos/ → Inputs/Outputs
├── infrastructure/
│ ├── gateways/ → Implementações reais
│ ├── repositories/
│ └── config/
└── api/
└── controllers/
🧾 Exemplo Prático: Criando Pedido e Emitindo Nota Fiscal
1. Entidade de Domínio
public class Order {
private String id;
private List<String> items;
private boolean isPaid;
public Order(List<String> items) {
this.id = UUID.randomUUID().toString();
this.items = items;
this.isPaid = false;
}
public void markAsPaid() {
if (isPaid) throw new IllegalStateException("Pedido já pago");
this.isPaid = true;
}
public List<String> getItems() { return items; }
}
2. Serviço de Domínio
public class OrderService {
public void validate(Order order) {
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Pedido não pode estar vazio");
}
}
}
3. Interface para o Repositório
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
}
4. Use Case: Criar Pedido
public class CreateOrderUseCase {
private final OrderRepository orderRepository;
private final OrderService orderService;
public CreateOrderUseCase(OrderRepository repo, OrderService service) {
this.orderRepository = repo;
this.orderService = service;
}
public String execute(List<String> items) {
Order order = new Order(items);
orderService.validate(order);
orderRepository.save(order);
return order.getId();
}
}
💳 Use Case Avançado: Emitir Nota Fiscal com Transação
@Service
public class EmitirNotaFiscalUseCase {
private final NFeService nfeService;
private final EstoqueGateway estoqueGateway;
private final NFeRepository nfeRepository;
private final SefazGateway sefazGateway;
@Transactional
public void execute(Pedido pedido) {
NotaFiscal nf = nfeService.gerarNota(pedido);
estoqueGateway.baixarEstoque(pedido.getItens());
nfeRepository.salvar(nf);
sefazGateway.enviarXML(nf); // se falhar, rollback automático
}
}
⚠️ O
@Transactional
garante rollback no banco se algo falhar.
📦 Interfaces (Ports)
public interface EstoqueGateway {
void baixarEstoque(List<ItemPedido> itens);
}
public interface NFeRepository {
void salvar(NotaFiscal notaFiscal);
}
public interface SefazGateway {
void enviarXML(NotaFiscal notaFiscal);
}
📐 Quando o Estoque está em outro Microserviço?
Você não pode usar transação distribuída confiável entre microserviços. O que fazer?
✅ Use Padrões como:
- SAGA Coreografada: serviços publicam eventos e reagem
- Outbox Pattern: salva evento no banco e publica de forma segura
- Fila com retry e dead letter
📦 Interface Genérica para Use Cases
public interface UseCase<I, O> {
O execute(I input);
}
DTO de Entrada e Saída
public class EmitirNotaFiscalInput {
private Pedido pedido;
private String usuarioResponsavel;
}
public class EmitirNotaFiscalOutput {
private String numeroNotaFiscal;
private boolean sucesso;
}
🧠 Cálculo de Impostos como Regra de Negócio
Refatorado com Polimorfismo:
public interface RegraICMS {
double calcular(double valorBase, ConfiguracaoFiscal config, String origem, String destino);
}
public class ICMS00 implements RegraICMS {
public double calcular(...) { /* lógica */ }
}
Classe Tributação:
public class Tributacao {
public double calcularICMS(...) {
RegraICMS regra = RegraICMSFactory.getRegra(cst);
return regra.calcular(...);
}
}
Aplicando o Princípio do Aberto/Fechado (OCP), sua lógica se torna testável, extensível e robusta.
✅ Conclusão
Separar regras de negócio da infraestrutura é essencial para a saúde do seu projeto Java. Com Clean Architecture e a estrutura de Use Cases genéricos:
- Seu sistema fica flexível para mudanças
- Os testes ficam simples e isolados
- As integrações são desacopladas
- O código expressa com clareza o domínio