Exemplo Clean com Spring
Estrutura de pacotes
Seção intitulada “Estrutura de pacotes”com.exemplo.pedidos├── domain (1. Entities)│ ├── Pedido.java│ ├── ItemPedido.java│ └── Dinheiro.java├── application (2. Use Cases)│ ├── port│ │ ├── in│ │ │ ├── CriarPedidoInputPort.java│ │ │ ├── CriarPedidoRequest.java│ │ │ └── ItemRequest.java│ │ └── out│ │ ├── CriarPedidoOutputPort.java│ │ └── PedidoGateway.java│ ├── model│ │ └── PedidoResponse.java│ └── usecase│ └── CriarPedidoInteractor.java└── infrastructure (3 e 4. Adapters + Frameworks/Drivers) ├── web │ ├── PedidoController.java │ └── PedidoPresenter.java ├── persistence │ ├── PedidoJpaEntity.java │ ├── ItemPedidoJpaEntity.java │ ├── PedidoJpaRepository.java │ └── PedidoGatewayJpaAdapter.java └── config └── BeanConfig.javaCriarPedidoOutputPort mora em port/out ao lado do gateway de propósito: ambos são contratos que o interactor chama — driven ports, na terminologia da Arquitetura Hexagonal —, enquanto port/in reúne o que o mundo externo chama. A diferença entre os dois é de papel: o gateway pede um serviço (persistir), o output port entrega o resultado a quem apresenta.
1. Camada de domínio (Entities)
Seção intitulada “1. Camada de domínio (Entities)”package com.exemplo.pedidos.domain;
import java.math.BigDecimal;
public record Dinheiro(BigDecimal valor) {
public Dinheiro { if (valor == null || valor.signum() < 0) { throw new IllegalArgumentException("Valor monetario invalido"); } }
public Dinheiro somar(Dinheiro outro) { return new Dinheiro(this.valor.add(outro.valor)); }
public Dinheiro multiplicar(int fator) { return new Dinheiro(this.valor.multiply(BigDecimal.valueOf(fator))); }
public static Dinheiro zero() { return new Dinheiro(BigDecimal.ZERO); }}package com.exemplo.pedidos.domain;
public class ItemPedido {
private final String produtoId; private final int quantidade; private final Dinheiro precoUnitario;
public ItemPedido(String produtoId, int quantidade, Dinheiro precoUnitario) { if (produtoId == null || produtoId.isBlank()) { throw new IllegalArgumentException("Produto obrigatorio"); } if (quantidade <= 0) { throw new IllegalArgumentException("Quantidade deve ser positiva"); } this.produtoId = produtoId; this.quantidade = quantidade; this.precoUnitario = precoUnitario; }
public Dinheiro subtotal() { return precoUnitario.multiplicar(quantidade); }
public String getProdutoId() { return produtoId; }
public int getQuantidade() { return quantidade; }
public Dinheiro getPrecoUnitario() { return precoUnitario; }}package com.exemplo.pedidos.domain;
import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.UUID;
public class Pedido {
private final String id; private final String clienteId; private final List<ItemPedido> itens; private StatusPedido status;
public Pedido(String clienteId, List<ItemPedido> itens) { if (clienteId == null || clienteId.isBlank()) { throw new IllegalArgumentException("Cliente obrigatorio"); } if (itens == null || itens.isEmpty()) { throw new IllegalArgumentException("Pedido deve ter ao menos um item"); } this.id = UUID.randomUUID().toString(); this.clienteId = clienteId; this.itens = new ArrayList<>(itens); this.status = StatusPedido.CRIADO; }
public Pedido(String id, String clienteId, List<ItemPedido> itens, StatusPedido status) { this.id = id; this.clienteId = clienteId; this.itens = new ArrayList<>(itens); this.status = status; }
public Dinheiro total() { return itens.stream() .map(ItemPedido::subtotal) .reduce(Dinheiro.zero(), Dinheiro::somar); }
public void confirmar() { if (status != StatusPedido.CRIADO) { throw new IllegalStateException("Somente pedidos criados podem ser confirmados"); } this.status = StatusPedido.CONFIRMADO; }
public String getId() { return id; }
public String getClienteId() { return clienteId; }
public List<ItemPedido> getItens() { return Collections.unmodifiableList(itens); }
public StatusPedido getStatus() { return status; }}package com.exemplo.pedidos.domain;
public enum StatusPedido { CRIADO, CONFIRMADO, CANCELADO}2. Camada de aplicação (Use Cases)
Seção intitulada “2. Camada de aplicação (Use Cases)”package com.exemplo.pedidos.application.port.in;
public interface CriarPedidoInputPort { void executar(CriarPedidoRequest request);}package com.exemplo.pedidos.application.port.in;
import java.util.List;
public record CriarPedidoRequest(String clienteId, List<ItemRequest> itens) {}package com.exemplo.pedidos.application.port.in;
import java.math.BigDecimal;
public record ItemRequest(String produtoId, int quantidade, BigDecimal precoUnitario) {}package com.exemplo.pedidos.application.port.out;
import com.exemplo.pedidos.application.model.PedidoResponse;
public interface CriarPedidoOutputPort { void apresentar(PedidoResponse response);}package com.exemplo.pedidos.application.model;
import java.math.BigDecimal;
public record PedidoResponse(String pedidoId, String status, BigDecimal total) {}package com.exemplo.pedidos.application.port.out;
import com.exemplo.pedidos.domain.Pedido;
public interface PedidoGateway { void salvar(Pedido pedido);}package com.exemplo.pedidos.application.usecase;
import com.exemplo.pedidos.application.model.PedidoResponse;import com.exemplo.pedidos.application.port.in.CriarPedidoInputPort;import com.exemplo.pedidos.application.port.in.CriarPedidoRequest;import com.exemplo.pedidos.application.port.out.CriarPedidoOutputPort;import com.exemplo.pedidos.application.port.out.PedidoGateway;import com.exemplo.pedidos.domain.Dinheiro;import com.exemplo.pedidos.domain.ItemPedido;import com.exemplo.pedidos.domain.Pedido;import java.util.List;
public class CriarPedidoInteractor implements CriarPedidoInputPort {
private final PedidoGateway gateway; private final CriarPedidoOutputPort outputPort;
public CriarPedidoInteractor(PedidoGateway gateway, CriarPedidoOutputPort outputPort) { this.gateway = gateway; this.outputPort = outputPort; }
@Override public void executar(CriarPedidoRequest request) { List<ItemPedido> itens = request.itens().stream() .map(i -> new ItemPedido(i.produtoId(), i.quantidade(), new Dinheiro(i.precoUnitario()))) .toList();
Pedido pedido = new Pedido(request.clienteId(), itens); pedido.confirmar(); gateway.salvar(pedido);
PedidoResponse response = new PedidoResponse( pedido.getId(), pedido.getStatus().name(), pedido.total().valor());
outputPort.apresentar(response); }}3 e 4. Infraestrutura (Interface Adapters + Frameworks & Drivers)
Seção intitulada “3 e 4. Infraestrutura (Interface Adapters + Frameworks & Drivers)”package com.exemplo.pedidos.infrastructure.web;
import com.exemplo.pedidos.application.model.PedidoResponse;import com.exemplo.pedidos.application.port.out.CriarPedidoOutputPort;import org.springframework.context.annotation.Scope;import org.springframework.context.annotation.ScopedProxyMode;import org.springframework.stereotype.Component;import org.springframework.web.context.WebApplicationContext;
@Component@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)public class PedidoPresenter implements CriarPedidoOutputPort {
private PedidoResponse response;
@Override public void apresentar(PedidoResponse response) { this.response = response; }
public PedidoResponse getResponse() { return response; }}package com.exemplo.pedidos.infrastructure.web;
import com.exemplo.pedidos.application.model.PedidoResponse;import com.exemplo.pedidos.application.port.in.CriarPedidoInputPort;import com.exemplo.pedidos.application.port.in.CriarPedidoRequest;import com.exemplo.pedidos.application.port.in.ItemRequest;import java.math.BigDecimal;import java.util.List;import org.springframework.beans.factory.ObjectProvider;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
@RestController@RequestMapping("/pedidos")public class PedidoController {
private final ObjectProvider<CriarPedidoInputPort> inputPortProvider; private final ObjectProvider<PedidoPresenter> presenterProvider;
public PedidoController(ObjectProvider<CriarPedidoInputPort> inputPortProvider, ObjectProvider<PedidoPresenter> presenterProvider) { this.inputPortProvider = inputPortProvider; this.presenterProvider = presenterProvider; }
@PostMapping public ResponseEntity<PedidoResponse> criar(@RequestBody CriarPedidoHttpRequest body) { List<ItemRequest> itens = body.itens().stream() .map(i -> new ItemRequest(i.produtoId(), i.quantidade(), i.precoUnitario())) .toList();
CriarPedidoRequest request = new CriarPedidoRequest(body.clienteId(), itens);
PedidoPresenter presenter = presenterProvider.getObject(); CriarPedidoInputPort inputPort = inputPortProvider.getObject(); inputPort.executar(request);
return ResponseEntity.status(HttpStatus.CREATED).body(presenter.getResponse()); }
public record CriarPedidoHttpRequest(String clienteId, List<ItemHttp> itens) {}
public record ItemHttp(String produtoId, int quantidade, BigDecimal precoUnitario) {}}package com.exemplo.pedidos.infrastructure.persistence;
import jakarta.persistence.CascadeType;import jakarta.persistence.Column;import jakarta.persistence.Entity;import jakarta.persistence.FetchType;import jakarta.persistence.Id;import jakarta.persistence.OneToMany;import jakarta.persistence.Table;import java.util.ArrayList;import java.util.List;
@Entity@Table(name = "pedidos")public class PedidoJpaEntity {
@Id private String id;
@Column(nullable = false) private String clienteId;
@Column(nullable = false) private String status;
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, fetch = FetchType.EAGER) private List<ItemPedidoJpaEntity> itens = new ArrayList<>();
protected PedidoJpaEntity() { }
public PedidoJpaEntity(String id, String clienteId, String status) { this.id = id; this.clienteId = clienteId; this.status = status; }
public void addItem(ItemPedidoJpaEntity item) { item.setPedido(this); this.itens.add(item); }
public String getId() { return id; }
public String getClienteId() { return clienteId; }
public String getStatus() { return status; }
public List<ItemPedidoJpaEntity> getItens() { return itens; }}package com.exemplo.pedidos.infrastructure.persistence;
import jakarta.persistence.Column;import jakarta.persistence.Entity;import jakarta.persistence.GeneratedValue;import jakarta.persistence.GenerationType;import jakarta.persistence.Id;import jakarta.persistence.JoinColumn;import jakarta.persistence.ManyToOne;import jakarta.persistence.Table;import java.math.BigDecimal;
@Entity@Table(name = "itens_pedido")public class ItemPedidoJpaEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false) private String produtoId;
@Column(nullable = false) private int quantidade;
@Column(nullable = false) private BigDecimal precoUnitario;
@ManyToOne @JoinColumn(name = "pedido_id") private PedidoJpaEntity pedido;
protected ItemPedidoJpaEntity() { }
public ItemPedidoJpaEntity(String produtoId, int quantidade, BigDecimal precoUnitario) { this.produtoId = produtoId; this.quantidade = quantidade; this.precoUnitario = precoUnitario; }
public void setPedido(PedidoJpaEntity pedido) { this.pedido = pedido; }
public String getProdutoId() { return produtoId; }
public int getQuantidade() { return quantidade; }
public BigDecimal getPrecoUnitario() { return precoUnitario; }}package com.exemplo.pedidos.infrastructure.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PedidoJpaRepository extends JpaRepository<PedidoJpaEntity, String> {}package com.exemplo.pedidos.infrastructure.persistence;
import com.exemplo.pedidos.application.port.out.PedidoGateway;import com.exemplo.pedidos.domain.ItemPedido;import com.exemplo.pedidos.domain.Pedido;import org.springframework.stereotype.Component;
@Componentpublic class PedidoGatewayJpaAdapter implements PedidoGateway {
private final PedidoJpaRepository repository;
public PedidoGatewayJpaAdapter(PedidoJpaRepository repository) { this.repository = repository; }
@Override public void salvar(Pedido pedido) { PedidoJpaEntity jpaEntity = new PedidoJpaEntity( pedido.getId(), pedido.getClienteId(), pedido.getStatus().name());
for (ItemPedido item : pedido.getItens()) { jpaEntity.addItem(new ItemPedidoJpaEntity( item.getProdutoId(), item.getQuantidade(), item.getPrecoUnitario().valor())); }
repository.save(jpaEntity); }}package com.exemplo.pedidos.infrastructure.config;
import com.exemplo.pedidos.application.port.in.CriarPedidoInputPort;import com.exemplo.pedidos.application.port.out.PedidoGateway;import com.exemplo.pedidos.application.usecase.CriarPedidoInteractor;import com.exemplo.pedidos.infrastructure.web.PedidoPresenter;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Scope;import org.springframework.context.annotation.ScopedProxyMode;import org.springframework.web.context.WebApplicationContext;
@Configurationpublic class BeanConfig {
@Bean @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) public CriarPedidoInputPort criarPedidoInputPort(PedidoGateway gateway, PedidoPresenter presenter) { return new CriarPedidoInteractor(gateway, presenter); }}Fluxo do caso de uso
Seção intitulada “Fluxo do caso de uso”sequenceDiagram
participant Cliente
participant Controller as PedidoController
participant Input as CriarPedidoInputPort
participant Interactor as CriarPedidoInteractor
participant Dominio as Pedido (Entity)
participant Gateway as PedidoGateway
participant Jpa as PedidoGatewayJpaAdapter
participant Output as PedidoPresenter
Cliente->>Controller: POST /pedidos
Controller->>Controller: monta CriarPedidoRequest (DTO)
Controller->>Input: executar(request)
Input->>Interactor: (implementacao)
Interactor->>Dominio: new Pedido(...), confirmar()
Dominio-->>Interactor: total(), status
Interactor->>Gateway: salvar(pedido)
Gateway->>Jpa: (implementacao)
Jpa->>Jpa: mapeia para PedidoJpaEntity e persiste
Interactor->>Output: apresentar(PedidoResponse)
Output-->>Controller: getResponse()
Controller-->>Cliente: 201 Created + PedidoResponse
Note que PedidoGateway e CriarPedidoOutputPort são interfaces do núcleo; suas implementações (PedidoGatewayJpaAdapter, PedidoPresenter) vivem na infraestrutura. Toda dependência de código aponta para dentro — a essência da A Regra da Dependência.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Anotar
Pedidocom@Entity: reusar o domínio como entidade JPA amarra o núcleo ao Hibernate. Por isso existePedidoJpaEntityseparada, com mapper no adapter. @RequestBody CriarPedidoRequestdireto: usar o DTO da aplicação como payload HTTP acopla o contrato interno à API. Aqui háCriarPedidoHttpRequestsó para a web.- Controller chamando
PedidoJpaRepository: pular o interactor colapsa a arquitetura em camadas comuns. - Presenter singleton guardando estado: o presenter carrega o
responsede uma requisição em campo mutável; como singleton, seria compartilhado entre requisições concorrentes. Por isso ambos são request-scoped — o input port noBeanConfig, o presenter via@Scopena própria classe. - Interactor com
@Service: aceitável por pragmatismo, mas aqui foi montado por@Beanpara manter o núcleo livre de anotações Spring.
Relações
Seção intitulada “Relações”- Teoria aplicada: A Regra da Dependência, Camadas da Clean Architecture, Use Cases.
- Inversão que sustenta o gateway: DIP, OCP, SOLID.
- Domínio: Entidades, Value Objects, Repositórios, Domain-Driven Design.
- Equivalência de portas/adaptadores: Arquitetura Hexagonal, Ports, Adapters, Driving e Driven.
- Testes do núcleo sem Spring: Testando o Núcleo.
- Índice e comparação: Clean Architecture, Comparação - Hexagonal vs Clean vs Camadas, Como Tudo se Conecta, Home.
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. Por que Pedido e PedidoJpaEntity são classes separadas?
Resposta
Para manter o domínio livre de JPA/Hibernate. Pedido é a Entity pura do anel 1; PedidoJpaEntity é um detalhe de persistência do anel externo. O PedidoGatewayJpaAdapter faz o mapeamento entre elas.
2. Onde está a inversão de dependência neste exemplo?
Resposta
O interactor depende das interfaces PedidoGateway e CriarPedidoOutputPort, ambas no núcleo. As implementações (PedidoGatewayJpaAdapter, PedidoPresenter) estão na infraestrutura e dependem do núcleo, apontando para dentro.
3. Por que existe CriarPedidoHttpRequest além de CriarPedidoRequest?
Resposta
CriarPedidoHttpRequest é o DTO da fronteira web (anel externo), acoplado ao formato HTTP. CriarPedidoRequest é o request model do caso de uso. Separá-los evita que o contrato da API vaze para o núcleo e vice-versa.
4. Por que o input port e o presenter são request-scoped?
Resposta
Porque o presenter guarda o response daquela requisição específica. Um escopo singleton compartilharia estado entre requisições concorrentes, causando race conditions. O input port é declarado request-scoped no BeanConfig; o presenter, via @Scope na própria classe — ambos com proxy.