Arquitetura Hexagonal¶
Arquitetura Hexagonal, também conhecida como Ports and Adapters, é um estilo arquitetural em que o núcleo da aplicação fica isolado do mundo externo. O domínio e os casos de uso ficam no centro; interfaces de entrada e saída ficam nas bordas do núcleo; frameworks, banco de dados, HTTP, filas e serviços externos ficam fora do núcleo, conectados por adapters.
Este documento define o padrão conceitual da Arkorizon para Arquitetura Hexagonal. Exemplos específicos com Spring Boot ficam em Exemplo Java Spring Boot.
Regra principal
Se o domínio depende de Spring, JPA, HTTP, Redis, Kafka, SDKs externos ou detalhes de banco de dados, a implementação não está seguindo Arquitetura Hexagonal.
Ideia central¶
A aplicação deve ser desenhada de dentro para fora.
Núcleo da aplicação -> Ports <- Adapters <- Mundo externo
O núcleo define o que precisa. O mundo externo fornece detalhes.
A aplicação não deve nascer perguntando:
Qual controller vou criar?
Qual tabela vou consultar?
Qual repository vou injetar?
Ela deve nascer perguntando:
Qual caso de uso existe?
Qual regra de negócio precisa ser protegida?
Quais entradas iniciam esse caso de uso?
Quais saídas externas esse caso de uso precisa chamar?
Camadas conceituais¶
flowchart LR
Driver[Driver actor] --> InputAdapter[Input adapter]
InputAdapter --> InputPort[Input port]
subgraph Core[Núcleo da aplicação]
InputPort --> UseCase[Use case]
UseCase --> Domain[Domain model]
UseCase --> OutputPort[Output port]
end
OutputPort --> OutputAdapter[Output adapter]
OutputAdapter --> Driven[Driven actor]
O que é cada coisa¶
| Conceito | Definição | Exemplo |
|---|---|---|
| Núcleo da aplicação | Parte que contém regras e comportamento de negócio. | LoginUseCaseImpl, User, LoginCommand |
| Driver actor | Algo que inicia uma ação na aplicação. | Usuário, REST client, scheduler, fila |
| Driven actor | Algo usado pela aplicação para cumprir uma ação. | Banco, Redis, API externa, filesystem |
| Input adapter | Traduz uma entrada externa para uma chamada do núcleo. | AuthController, Kafka consumer, CLI command |
| Input port | Contrato de entrada do núcleo. | LoginUseCase |
| Use case | Implementação de uma intenção de negócio. | LoginUseCaseImpl |
| Domain model | Modelo que representa conceito de negócio. | User, AuthToken, Account |
| Output port | Contrato que o núcleo exige do mundo externo. | FindUserByEmailPort, SaveRefreshTokenPort |
| Output adapter | Implementação técnica de um output port. | UserPersistenceAdapter, RedisRefreshTokenAdapter |
Direção das dependências¶
A dependência sempre aponta para dentro.
adapter.in -> domain.port.in
adapter.out -> domain.port.out
config -> domain + adapters
domain -> Java puro
Permitido:
Controller -> LoginUseCase
LoginUseCaseImpl -> FindUserByEmailPort
UserPersistenceAdapter -> FindUserByEmailPort
Proibido:
LoginUseCaseImpl -> UserJpaRepository
User -> @Entity
LoginCommand -> @NotBlank
Domain service -> RedisTemplate
Domain model -> ResponseEntity
Hexágono não é pasta¶
A arquitetura não é definida apenas pela estrutura de diretórios. A estrutura ajuda, mas o que torna a arquitetura hexagonal é a direção das dependências.
Você pode ter pastas chamadas domain, port e adapter e ainda assim violar a arquitetura se o domínio depender de JPA, HTTP ou Spring.
Núcleo da aplicação¶
O núcleo deve conter:
- use cases;
- domain models;
- input ports;
- output ports;
- commands;
- results;
- exceções de negócio;
- regras e invariantes.
O núcleo não deve conter:
- controllers;
- DTOs HTTP;
- entidades JPA;
- repositories Spring Data;
- clients HTTP;
- Redis templates;
- anotações Spring;
- status HTTP;
- configuração de framework.
Input port¶
Input port é o contrato que permite acionar um caso de uso.
public interface LoginUseCase {
AuthTokenResult execute(LoginCommand command);
}
Use quando quiser expor uma intenção da aplicação para adapters de entrada.
Exemplos de input ports:
CreateUserUseCase
LoginUseCase
CreateProjectUseCase
UploadDocumentUseCase
GenerateReportUseCase
Output port¶
Output port é o contrato que o núcleo usa para pedir algo ao mundo externo.
public interface FindUserByEmailPort {
Optional<User> findByEmail(String email);
}
Use quando o caso de uso precisa de uma dependência externa.
Exemplos de output ports:
FindUserByEmailPort
SaveUserPort
SaveRefreshTokenPort
SendEmailPort
LoadDocumentPort
PublishDomainEventPort
Use case¶
Use case é a implementação de uma intenção de negócio. Ele orquestra regras, domain models e output ports.
public final class LoginUseCaseImpl implements LoginUseCase {
private final FindUserByEmailPort findUserByEmailPort;
private final PasswordVerifierPort passwordVerifierPort;
private final TokenIssuerPort tokenIssuerPort;
public LoginUseCaseImpl(
final FindUserByEmailPort findUserByEmailPort,
final PasswordVerifierPort passwordVerifierPort,
final TokenIssuerPort tokenIssuerPort
) {
this.findUserByEmailPort = findUserByEmailPort;
this.passwordVerifierPort = passwordVerifierPort;
this.tokenIssuerPort = tokenIssuerPort;
}
@Override
public AuthTokenResult execute(final LoginCommand command) {
final User user = findUserByEmailPort.findByEmail(command.email())
.orElseThrow(InvalidCredentialsException::new);
user.ensureCanLogin();
if (!passwordVerifierPort.matches(command.password(), user.passwordHash())) {
throw new InvalidCredentialsException();
}
return tokenIssuerPort.issueFor(user);
}
}
Um use case não deve saber se o usuário veio de REST, GraphQL, CLI ou fila. Também não deve saber se o usuário foi carregado de PostgreSQL, MongoDB, Redis ou API externa.
Domain model¶
Domain model representa conceito de negócio. Ele deve expressar comportamento e invariantes do domínio.
public record User(
UUID id,
String email,
String passwordHash,
boolean active
) {
public void ensureCanLogin() {
if (!active) {
throw new UserInactiveException();
}
}
}
Use record quando:
- o objeto for imutável;
- o objeto representar dados de domínio com pouca lógica;
- não houver ciclo de vida complexo;
- não houver alteração controlada de estado.
Use class quando:
- o objeto tiver invariantes fortes;
- o objeto tiver métodos de negócio que alteram estado;
- você precisar proteger mutações;
- o conceito tiver ciclo de vida rico.
Command¶
Command representa a entrada de um use case.
public record LoginCommand(
String email,
String password
) {
}
Use command quando:
- dados externos precisam entrar no núcleo;
- o use case precisa receber uma intenção de execução;
- você quer separar DTO externo de entrada interna do caso de uso.
Command não é DTO HTTP. Command pertence ao núcleo.
Result¶
Result representa a saída de um use case.
public record AuthTokenResult(
String accessToken,
String refreshToken,
long expiresInSeconds
) {
}
Use result quando:
- o use case precisa devolver dados ao adapter de entrada;
- a resposta do núcleo não deve depender de HTTP;
- você quer separar saída de negócio de response DTO.
Result não é response DTO. Result pertence ao núcleo.
DTO¶
DTO é objeto de transporte de um adapter.
public record LoginRequestDTO(
@NotBlank @Email String email,
@NotBlank String password
) {
}
Use DTO quando:
- dados vêm de HTTP, fila, arquivo ou outro mecanismo externo;
- você precisa de validação de entrada;
- você precisa controlar contrato público;
- você quer proteger o núcleo de detalhes externos.
DTO não deve entrar no use case.
Mapper¶
Mapper traduz objetos entre bordas.
@Mapper(componentModel = "spring")
public interface AuthWebMapper {
LoginCommand toCommand(LoginRequestDTO request);
AuthTokenResponseDTO toResponse(AuthTokenResult result);
}
Crie mapper quando:
- existe conversão entre DTO e command;
- existe conversão entre result e response DTO;
- existe conversão entre JPA entity e domain model;
- existe conversão entre modelo de API externa e domain model.
Não crie mapper genérico para tudo. Cada borda deve ter seu mapper.
Adapter¶
Adapter conecta o núcleo ao mundo externo.
Input adapter chama o núcleo:
REST Controller -> LoginUseCase
Output adapter implementa contrato do núcleo:
UserPersistenceAdapter implements FindUserByEmailPort
Adapters podem usar frameworks. O núcleo não.
Configuration¶
Configuration é a raiz de composição da aplicação. É onde implementações concretas são conectadas aos ports.
@Configuration
public class UseCaseConfig {
@Bean
public LoginUseCase loginUseCase(
final FindUserByEmailPort findUserByEmailPort,
final PasswordVerifierPort passwordVerifierPort,
final TokenIssuerPort tokenIssuerPort
) {
return new LoginUseCaseImpl(
findUserByEmailPort,
passwordVerifierPort,
tokenIssuerPort
);
}
}
Essa configuração permite que o use case continue sendo Java puro.
Sobre Spring, JPA, Redis e segurança¶
Spring, JPA, Redis e segurança não fazem parte da Arquitetura Hexagonal em si. Eles são tecnologias que podem ser usadas nos adapters.
Por isso:
- Spring Boot compõe e expõe a aplicação;
- JPA implementa output adapters de persistência;
- Redis implementa output adapters de cache, sessão ou token;
- Spring Security implementa adapters/configuração de segurança;
- controllers implementam input adapters HTTP.
A arquitetura continua correta somente se essas tecnologias ficarem fora do núcleo.
Checklist canônico¶
Uma implementação segue Arquitetura Hexagonal quando:
- o domínio é Java puro;
- use cases não dependem de framework;
- input ports pertencem ao núcleo;
- output ports pertencem ao núcleo;
- input adapters chamam input ports;
- output adapters implementam output ports;
- DTOs ficam nos adapters;
- JPA entities ficam nos adapters;
- repositories são detalhes de output adapter;
- configuração de beans fica fora do domínio;
- regras de negócio são testáveis sem Spring.
Anti-patterns¶
Domínio com Spring¶
@Service
public class LoginUseCaseImpl implements LoginUseCase {
}
O problema não é funcionar. O problema é o núcleo passar a depender de framework.
Use case chamando repository¶
public class LoginUseCaseImpl implements LoginUseCase {
private final UserJpaRepository userJpaRepository;
+}
Isso faz o núcleo depender de persistência.
Controller com regra de negócio¶
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequestDTO request) {
if (request.password().length() < 8) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok().build();
}
Controller deve traduzir HTTP, não ser dono de regra de negócio.
Entity JPA como domain model¶
@Entity
public class User {
}
Isso acopla o modelo de negócio ao banco. Pode parecer simples no começo, mas dificulta testes, evolução e troca de infraestrutura.
Padrão Arkorizon¶
Nos projetos Arkorizon, Arquitetura Hexagonal significa:
- domínio e use cases são o centro;
- ports são contratos do núcleo;
- adapters são detalhes substituíveis;
- Spring fica na borda;
- banco de dados fica na borda;
- Redis fica na borda;
- HTTP fica na borda;
- regras de negócio continuam testáveis sem framework.
Se uma implementação faz o núcleo depender de Spring, JPA, Redis, HTTP, filas, SDKs ou detalhes de banco, ela não deve ser aprovada como Arquitetura Hexagonal.