No Spring, Controller não é lugar de regra de negócio
Esse erro aparece com muita facilidade.
A requisição chega no controller, os dados já estão ali, o repositório está a uma injeção de distância e a regra parece pequena demais para justificar outra classe.
Então entra um if.
Depois uma consulta.
Depois uma validação que depende do estado atual do banco.
Depois uma mudança de status, uma chamada externa, uma decisão transacional e uma resposta diferente para cada cenário.
Quando você percebe, o controller deixou de ser a porta de entrada da aplicação e virou o lugar onde o negócio acontece.
O código ainda funciona.
Mas a fronteira ficou errada.
O papel do controller é lidar com HTTP
Um @RestController existe para representar a borda HTTP da aplicação.
Ele recebe a requisição, extrai parâmetros, interpreta o corpo enviado, dispara validações de entrada, chama o caso de uso correto e devolve uma resposta adequada.
Esse papel é importante.
Mas ele não é o mesmo papel de decidir se um pedido pode ser fechado, se um pagamento pode ser capturado, se um usuário pode executar uma ação ou se uma operação deve abrir uma transação.
HTTP é protocolo.
Regra de negócio é decisão.
Quando as duas coisas ficam no mesmo lugar, o código passa a misturar detalhes de transporte com comportamento da aplicação.
O atalho que começa pequeno
Um controller assim costuma nascer com boa intenção:
@RestController
@RequestMapping("/pedidos")
public class PedidoController {
private final PedidoRepository pedidoRepository;
private final EstoqueService estoqueService;
public PedidoController(
PedidoRepository pedidoRepository,
EstoqueService estoqueService
) {
this.pedidoRepository = pedidoRepository;
this.estoqueService = estoqueService;
}
@PostMapping("/{id}/fechar")
public ResponseEntity<PedidoResponse> fechar(@PathVariable Long id) {
Pedido pedido = pedidoRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!pedido.estaAberto()) {
throw new ResponseStatusException(
HttpStatus.UNPROCESSABLE_ENTITY,
"Pedido não está aberto"
);
}
if (!estoqueService.temEstoquePara(pedido)) {
throw new ResponseStatusException(
HttpStatus.UNPROCESSABLE_ENTITY,
"Estoque insuficiente"
);
}
pedido.fechar();
pedidoRepository.save(pedido);
return ResponseEntity.ok(PedidoResponse.from(pedido));
}
}
À primeira vista, não parece absurdo.
O endpoint é simples, o fluxo está todo em um lugar e dá para entender o que acontece lendo o método.
Mas o problema não é o tamanho do método no primeiro dia.
O problema é o lugar escolhido para essa decisão morar.
Esse controller agora sabe como buscar o pedido, quais estados são permitidos, como consultar estoque, qual transição executar, quando persistir e como traduzir falhas de negócio para HTTP.
Ele virou fluxo de aplicação disfarçado de endpoint.
Por que isso começa a cobrar caro
Quando regra de negócio fica no controller, alguns problemas aparecem rápido:
- a mesma regra fica difícil de reutilizar fora do HTTP;
- testes precisam montar contexto de controller para validar decisão de negócio;
- exceções de aplicação se misturam com status HTTP;
- transações começam a ser definidas na borda errada;
- mudanças no contrato da API passam a encostar em regra interna;
- o controller cresce até virar um mapa confuso do sistema.
Esse ponto conversa com o post sobre DTO e Entity: quando fronteiras se misturam, uma mudança pequena começa a empurrar efeito colateral para lugares que não deveriam ser afetados.
Também conversa com o post sobre @Service: criar service vazio por cerimônia não ajuda, mas deixar caso de uso real dentro do controller também não.
O problema não é ter poucas classes.
O problema é colocar responsabilidade importante no lugar errado.
O caso de uso merece um lugar próprio
Uma alternativa mais honesta é deixar o controller cuidar do HTTP e mover a decisão para uma classe de aplicação:
@Service
public class FecharPedidoService {
private final PedidoRepository pedidoRepository;
private final EstoqueService estoqueService;
public FecharPedidoService(
PedidoRepository pedidoRepository,
EstoqueService estoqueService
) {
this.pedidoRepository = pedidoRepository;
this.estoqueService = estoqueService;
}
@Transactional
public Pedido fechar(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId)
.orElseThrow(() -> new PedidoNaoEncontradoException(pedidoId));
if (!pedido.estaAberto()) {
throw new PedidoNaoPodeSerFechadoException(pedidoId);
}
if (!estoqueService.temEstoquePara(pedido)) {
throw new EstoqueInsuficienteException(pedidoId);
}
pedido.fechar();
return pedido;
}
}
Agora a operação importante tem nome.
FecharPedidoService representa um caso de uso reconhecível da aplicação.
Ele coordena dependências, protege a transação e deixa a regra em um lugar que pode ser testado sem precisar simular uma chamada HTTP.
O controller fica com a responsabilidade dele:
@RestController
@RequestMapping("/pedidos")
public class PedidoController {
private final FecharPedidoService fecharPedidoService;
public PedidoController(FecharPedidoService fecharPedidoService) {
this.fecharPedidoService = fecharPedidoService;
}
@PostMapping("/{id}/fechar")
public ResponseEntity<PedidoResponse> fechar(@PathVariable Long id) {
Pedido pedido = fecharPedidoService.fechar(id);
return ResponseEntity.ok(PedidoResponse.from(pedido));
}
}
Esse controller não está vazio.
Ele está focado.
Ele recebe uma requisição HTTP, chama a operação certa e transforma o resultado em resposta.
Esse é um trabalho suficiente.
Controller fino não significa aplicação anêmica
Existe uma confusão comum aqui.
Muita gente ouve que controller não deve ter regra de negócio e entende que todo comportamento precisa ir para um service gigante.
Não é isso.
Se a regra pertence à entidade, ela deve morar na entidade.
Se a regra representa um caso de uso, coordenação, transação ou política de aplicação, ela pode morar em uma classe de aplicação.
Se o código só traduz entrada HTTP em chamada de aplicação, ele pertence ao controller.
A separação saudável não é sobre quantidade de camadas.
É sobre cada decisão ficar no lugar que consegue explicá-la melhor.
No exemplo anterior, pedido.fechar() ainda pode carregar regra do domínio.
O service não precisa roubar comportamento da entidade.
Ele só coordena o caso de uso: busca o pedido, valida pré-condições externas, abre a fronteira transacional e devolve o resultado.
O ponto que vale fixar
Controller não deveria ser o lugar onde a aplicação decide.
Ele deveria ser o lugar onde a aplicação conversa por HTTP.
Quando uma regra de negócio fica presa no controller, ela nasce acoplada ao protocolo, mais difícil de testar, mais difícil de reutilizar e mais fácil de misturar com detalhes de resposta.
Spring facilita muito criar endpoints.
Mas facilidade de entrada não significa que toda decisão deve morar na entrada.
O controller é a porta.
A regra de negócio precisa de casa.
