Exemplo Java Spring Boot: Auth API Hexagonal¶
Este documento mostra, passo a passo, como implementar uma API de autenticação usando Java, Spring Boot e Arquitetura Hexagonal.
A ideia é explicar de forma prática onde cada classe nasce, por que ela existe e quando usar cada conceito: model, command, result, port, adapter, mapper, controller, entity, repository e configuração de beans.
Antes de começar
Leia primeiro Arquitetura Hexagonal. Este documento é o exemplo prático com Spring Boot. Spring, JPA, Redis e HTTP aparecem aqui como adapters, não como centro da arquitetura.
O caso de uso¶
Vamos implementar o login de um usuário.
Fluxo esperado:
1. Cliente envia email e senha para POST /api/v1/auth/login.
2. Controller recebe o request.
3. Mapper converte LoginRequestDTO em LoginCommand.
4. LoginUseCase executa a regra de login.
5. Use case busca usuário por email usando FindUserByEmailPort.
6. Persistence adapter implementa essa porta usando JPA.
7. Use case valida senha usando PasswordVerifierPort.
8. Security adapter implementa essa porta usando PasswordEncoder.
9. Use case gera tokens usando TokenIssuerPort.
10. Use case salva refresh token usando SaveRefreshTokenPort.
11. Redis adapter implementa essa porta usando Redis.
12. Use case retorna AuthTokenResult.
13. Mapper converte AuthTokenResult em AuthTokenResponseDTO.
14. Controller retorna HTTP 200.
Estrutura final¶
com.arkorizon.service.auth
|-- AuthApplication.java
|
|-- config
}
| |-- SecurityConfig.java
|
|-- domain
| |-- model
| | |-- User.java
| |-- command
| | |-- LoginCommand.java
| |-- result
| | |-- AuthTokenResult.java
| |-- exception
| | |-- InvalidCredentialsException.java
| | |-- UserInactiveException.java
| |-- port
| | |-- in
| | | |-- LoginUseCase.java
| | |-- out
| | | |-- FindUserByEmailPort.java
| | | |-- PasswordVerifierPort.java
| | | |-- TokenIssuerPort.java
| | | |-- SaveRefreshTokenPort.java
| |-- service
| | |-- LoginUseCaseImpl.java
|
|-- adapter
| |-- in
| | |-- web
| | | |-- controller
| | | | |-- AuthController.java
| | | |-- dto
| | | | |-- LoginRequestDTO.java
| | | | |-- AuthTokenResponseDTO.java
| | | |-- mapper
| | | | |-- AuthWebMapper.java
| | | |-- exception
| | | | |-- GlobalExceptionHandler.java
| |-- out
| | |-- persistence
| | | |-- adapter
| | | | |-- UserPersistenceAdapter.java
| | | |-- entity
| | | | |-- UserEntity.java
| | | |-- mapper
| | | | |-- UserPersistenceMapper.java
| | | |-- repository
| | | | |-- UserJpaRepository.java
| | |-- redis
| | | |-- adapter
| | | | |-- RefreshTokenRedisAdapter.java
| | | |-- model
| | | | |-- RefreshTokenCacheValue.java
| | |-- security
| | | |-- BCryptPasswordVerifierAdapter.java
| | | |-- JwtTokenIssuerAdapter.java
Como decidir cada classe¶
| Pergunta | Classe criada | Camada |
|---|---|---|
| Qual ação o usuário quer executar? | LoginUseCase |
domain.port.in |
| Quais dados entram no caso de uso? | LoginCommand |
domain.command |
| Quais dados saem do caso de uso? | AuthTokenResult |
domain.result |
| Qual conceito de negócio existe? | User |
domain.model |
| O caso de uso precisa buscar usuário fora do núcleo? | FindUserByEmailPort |
domain.port.out |
| O caso de uso precisa validar senha fora do núcleo? | PasswordVerifierPort |
domain.port.out |
| O caso de uso precisa gerar token fora do núcleo? | TokenIssuerPort |
domain.port.out |
| O caso de uso precisa salvar refresh token fora do núcleo? | SaveRefreshTokenPort |
domain.port.out |
| Quem recebe HTTP? | AuthController |
adapter.in.web |
| Qual contrato HTTP chega? | LoginRequestDTO |
adapter.in.web.dto |
| Qual contrato HTTP sai? | AuthTokenResponseDTO |
adapter.in.web.dto |
| Quem traduz DTO para command? | AuthWebMapper |
adapter.in.web.mapper |
| Quem usa JPA? | UserPersistenceAdapter |
adapter.out.persistence |
| Quem representa tabela? | UserEntity |
adapter.out.persistence.entity |
| Quem usa Redis? | RefreshTokenRedisAdapter |
adapter.out.redis |
| Quem conecta tudo? | UseCaseConfig |
config |
Passo 1: criar o domain model¶
Comece pelo conceito de negócio. Para login, precisamos de um usuário.
package com.arkorizon.service.auth.domain.model;
import com.arkorizon.service.auth.domain.exception.UserInactiveException;
import java.util.UUID;
public record User(
UUID id,
String email,
String passwordHash,
boolean active
) {
public void ensureCanLogin() {
if (!active) {
throw new UserInactiveException();
}
}
}
Use model quando estiver representando algo do negócio. Neste exemplo, User é um conceito real do domínio de autenticação.
Passo 2: criar command e result¶
O use case precisa receber dados de entrada. Esses dados entram como command.
package com.arkorizon.service.auth.domain.command;
public record LoginCommand(
String email,
String password
) {
}
Use command quando quiser representar uma intenção enviada ao use case.
O use case também precisa devolver uma saída.
package com.arkorizon.service.auth.domain.result;
public record AuthTokenResult(
String accessToken,
String refreshToken,
long expiresInSeconds
) {
}
Use result quando quiser representar a resposta do núcleo para um adapter.
Passo 3: criar o input port¶
Input port é a porta de entrada do núcleo.
package com.arkorizon.service.auth.domain.port.in;
import com.arkorizon.service.auth.domain.command.LoginCommand;
import com.arkorizon.service.auth.domain.result.AuthTokenResult;
public interface LoginUseCase {
AuthTokenResult execute(LoginCommand command);
}
O controller vai depender de LoginUseCase, não da implementação concreta.
Passo 4: descobrir os output ports¶
Agora pergunte: o login precisa de quais coisas externas?
Ele precisa:
- buscar usuário no banco;
- validar senha com algoritmo de hash;
- gerar tokens;
- salvar refresh token no Redis.
Cada necessidade externa vira uma porta de saída.
package com.arkorizon.service.auth.domain.port.out;
import com.arkorizon.service.auth.domain.model.User;
import java.util.Optional;
public interface FindUserByEmailPort {
Optional<User> findByEmail(String email);
}
package com.arkorizon.service.auth.domain.port.out;
public interface PasswordVerifierPort {
boolean matches(String rawPassword, String passwordHash);
}
package com.arkorizon.service.auth.domain.port.out;
import com.arkorizon.service.auth.domain.model.User;
import com.arkorizon.service.auth.domain.result.AuthTokenResult;
public interface TokenIssuerPort {
AuthTokenResult issueFor(User user);
}
package com.arkorizon.service.auth.domain.port.out;
import java.time.Duration;
import java.util.UUID;
public interface SaveRefreshTokenPort {
void save(UUID userId, String refreshToken, Duration ttl);
}
Output port não diz como será feito. Ele só diz o que o núcleo precisa.
Passo 5: implementar o use case¶
Agora o use case pode ser implementado sem saber nada de HTTP, JPA ou Redis.
package com.arkorizon.service.auth.domain.service;
import com.arkorizon.service.auth.domain.command.LoginCommand;
import com.arkorizon.service.auth.domain.exception.InvalidCredentialsException;
import com.arkorizon.service.auth.domain.model.User;
import com.arkorizon.service.auth.domain.port.in.LoginUseCase;
import com.arkorizon.service.auth.domain.port.out.FindUserByEmailPort;
import com.arkorizon.service.auth.domain.port.out.PasswordVerifierPort;
import com.arkorizon.service.auth.domain.port.out.SaveRefreshTokenPort;
import com.arkorizon.service.auth.domain.port.out.TokenIssuerPort;
import com.arkorizon.service.auth.domain.result.AuthTokenResult;
import java.time.Duration;
public final class LoginUseCaseImpl implements LoginUseCase {
private static final Duration REFRESH_TOKEN_TTL = Duration.ofDays(30);
private final FindUserByEmailPort findUserByEmailPort;
private final PasswordVerifierPort passwordVerifierPort;
private final TokenIssuerPort tokenIssuerPort;
private final SaveRefreshTokenPort saveRefreshTokenPort;
public LoginUseCaseImpl(
final FindUserByEmailPort findUserByEmailPort,
final PasswordVerifierPort passwordVerifierPort,
final TokenIssuerPort tokenIssuerPort,
final SaveRefreshTokenPort saveRefreshTokenPort
) {
this.findUserByEmailPort = findUserByEmailPort;
this.passwordVerifierPort = passwordVerifierPort;
this.tokenIssuerPort = tokenIssuerPort;
this.saveRefreshTokenPort = saveRefreshTokenPort;
}
@Override
public AuthTokenResult execute(final LoginCommand command) {
final User user = findUserByEmailPort.findByEmail(command.email())
.orElseThrow(InvalidCredentialsException::new);
user.ensureCanLogin();
final boolean validPassword = passwordVerifierPort.matches(
command.password(),
user.passwordHash()
);
if (!validPassword) {
throw new InvalidCredentialsException();
}
final AuthTokenResult token = tokenIssuerPort.issueFor(user);
saveRefreshTokenPort.save(user.id(), token.refreshToken(), REFRESH_TOKEN_TTL);
return token;
}
}
Perceba: o use case não sabe se o usuário vem de PostgreSQL, se o token é JWT ou se o refresh token vai para Redis. Ele só conhece contratos.
Passo 6: criar DTOs HTTP¶
DTO é contrato externo. Ele pertence ao adapter web.
package com.arkorizon.service.auth.adapter.in.web.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record LoginRequestDTO(
@NotBlank @Email String email,
@NotBlank String password
) {
}
package com.arkorizon.service.auth.adapter.in.web.dto;
public record AuthTokenResponseDTO(
String accessToken,
String refreshToken,
long expiresInSeconds
) {
}
Use DTO quando existe contrato de entrada ou saída com o mundo externo.
Passo 7: criar mapper web¶
Mapper separa contrato externo do núcleo.
package com.arkorizon.service.auth.adapter.in.web.mapper;
import com.arkorizon.service.auth.adapter.in.web.dto.AuthTokenResponseDTO;
import com.arkorizon.service.auth.adapter.in.web.dto.LoginRequestDTO;
import com.arkorizon.service.auth.domain.command.LoginCommand;
import com.arkorizon.service.auth.domain.result.AuthTokenResult;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface AuthWebMapper {
LoginCommand toCommand(LoginRequestDTO request);
AuthTokenResponseDTO toResponse(AuthTokenResult result);
}
Crie mapper quando existe tradução entre bordas. Não faça essa tradução manualmente dentro do controller se ela começar a crescer.
Passo 8: criar controller¶
Controller é input adapter. Ele recebe HTTP e chama o input port.
package com.arkorizon.service.auth.adapter.in.web.controller;
import com.arkorizon.service.auth.adapter.in.web.dto.AuthTokenResponseDTO;
import com.arkorizon.service.auth.adapter.in.web.dto.LoginRequestDTO;
import com.arkorizon.service.auth.adapter.in.web.mapper.AuthWebMapper;
import com.arkorizon.service.auth.domain.command.LoginCommand;
import com.arkorizon.service.auth.domain.port.in.LoginUseCase;
import com.arkorizon.service.auth.domain.result.AuthTokenResult;
import jakarta.validation.Valid;
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("/api/v1/auth")
public class AuthController {
private final LoginUseCase loginUseCase;
private final AuthWebMapper authWebMapper;
public AuthController(
final LoginUseCase loginUseCase,
final AuthWebMapper authWebMapper
) {
this.loginUseCase = loginUseCase;
this.authWebMapper = authWebMapper;
}
@PostMapping("/login")
public ResponseEntity<AuthTokenResponseDTO> login(
@Valid @RequestBody final LoginRequestDTO request
) {
final LoginCommand command = authWebMapper.toCommand(request);
final AuthTokenResult result = loginUseCase.execute(command);
final AuthTokenResponseDTO response = authWebMapper.toResponse(result);
return ResponseEntity.ok(response);
}
}
Controller não contém regra de negócio. Ele só traduz HTTP para caso de uso.
Passo 9: criar persistence entity¶
Entity representa tabela. Ela pertence ao adapter de persistência.
package com.arkorizon.service.auth.adapter.out.persistence.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
@Entity
@Table(name = "tb_user")
public class UserEntity {
@Id
@Column(name = "id", nullable = false)
private UUID id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Column(name = "active", nullable = false)
private boolean active;
protected UserEntity() {
}
public UUID getId() {
return id;
}
public String getEmail() {
return email;
}
public String getPasswordHash() {
return passwordHash;
}
public boolean isActive() {
return active;
}
}
UserEntity não é User. Um é persistência; o outro é domínio.
Passo 10: criar repository JPA¶
Repository é detalhe de infraestrutura.
package com.arkorizon.service.auth.adapter.out.persistence.repository;
import com.arkorizon.service.auth.adapter.out.persistence.entity.UserEntity;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserJpaRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmail(String email);
}
O use case nunca deve receber UserJpaRepository.
Passo 11: criar persistence mapper¶
package com.arkorizon.service.auth.adapter.out.persistence.mapper;
import com.arkorizon.service.auth.adapter.out.persistence.entity.UserEntity;
import com.arkorizon.service.auth.domain.model.User;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface UserPersistenceMapper {
User toDomain(UserEntity entity);
}
Esse mapper evita que UserEntity vaze para o núcleo.
Passo 12: criar persistence adapter¶
package com.arkorizon.service.auth.adapter.out.persistence.adapter;
import com.arkorizon.service.auth.adapter.out.persistence.mapper.UserPersistenceMapper;
import com.arkorizon.service.auth.adapter.out.persistence.repository.UserJpaRepository;
import com.arkorizon.service.auth.domain.model.User;
import com.arkorizon.service.auth.domain.port.out.FindUserByEmailPort;
import java.util.Optional;
import org.springframework.stereotype.Component;
@Component
public class UserPersistenceAdapter implements FindUserByEmailPort {
private final UserJpaRepository userJpaRepository;
private final UserPersistenceMapper userPersistenceMapper;
public UserPersistenceAdapter(
final UserJpaRepository userJpaRepository,
final UserPersistenceMapper userPersistenceMapper
) {
this.userJpaRepository = userJpaRepository;
this.userPersistenceMapper = userPersistenceMapper;
}
@Override
public Optional<User> findByEmail(final String email) {
return userJpaRepository.findByEmail(email)
.map(userPersistenceMapper::toDomain);
}
}
Aqui é onde JPA encontra Hexagonal: o adapter implementa a porta do núcleo.
Passo 13: criar Redis adapter¶
O Redis também é detalhe externo. O núcleo só conhece SaveRefreshTokenPort.
package com.arkorizon.service.auth.adapter.out.redis.model;
import java.util.UUID;
public record RefreshTokenCacheValue(
UUID userId,
String refreshToken
) {
}
package com.arkorizon.service.auth.adapter.out.redis.adapter;
import com.arkorizon.service.auth.adapter.out.redis.model.RefreshTokenCacheValue;
import com.arkorizon.service.auth.domain.port.out.SaveRefreshTokenPort;
import java.time.Duration;
import java.util.UUID;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RefreshTokenRedisAdapter implements SaveRefreshTokenPort {
private static final String KEY_PREFIX = "auth:refresh-token:";
private final RedisTemplate<String, RefreshTokenCacheValue> redisTemplate;
public RefreshTokenRedisAdapter(
final RedisTemplate<String, RefreshTokenCacheValue> redisTemplate
) {
this.redisTemplate = redisTemplate;
}
@Override
public void save(final UUID userId, final String refreshToken, final Duration ttl) {
final String key = KEY_PREFIX + userId;
final RefreshTokenCacheValue value = new RefreshTokenCacheValue(userId, refreshToken);
redisTemplate.opsForValue().set(key, value, ttl);
}
}
O use case não sabe que existe Redis. Ele só chama SaveRefreshTokenPort.
Passo 14: criar adapters de segurança¶
Validação de senha é infraestrutura.
package com.arkorizon.service.auth.adapter.out.security;
import com.arkorizon.service.auth.domain.port.out.PasswordVerifierPort;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class BCryptPasswordVerifierAdapter implements PasswordVerifierPort {
private final PasswordEncoder passwordEncoder;
public BCryptPasswordVerifierAdapter(final PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public boolean matches(final String rawPassword, final String passwordHash) {
return passwordEncoder.matches(rawPassword, passwordHash);
}
}
Geração de token também é infraestrutura.
package com.arkorizon.service.auth.adapter.out.security;
import com.arkorizon.service.auth.domain.model.User;
import com.arkorizon.service.auth.domain.port.out.TokenIssuerPort;
import com.arkorizon.service.auth.domain.result.AuthTokenResult;
import org.springframework.stereotype.Component;
@Component
public class JwtTokenIssuerAdapter implements TokenIssuerPort {
@Override
public AuthTokenResult issueFor(final User user) {
final String accessToken = "generated-access-token";
final String refreshToken = "generated-refresh-token";
return new AuthTokenResult(accessToken, refreshToken, 3600);
}
}
Em uma implementação real, JwtTokenIssuerAdapter usa JwtEncoder, chave privada, claims, expiração e configurações seguras.
Passo 15: configurar beans¶
O use case não tem @Service. Ele vira bean por configuração.
package com.arkorizon.service.auth.config;
import com.arkorizon.service.auth.domain.port.in.LoginUseCase;
import com.arkorizon.service.auth.domain.port.out.FindUserByEmailPort;
import com.arkorizon.service.auth.domain.port.out.PasswordVerifierPort;
import com.arkorizon.service.auth.domain.port.out.SaveRefreshTokenPort;
import com.arkorizon.service.auth.domain.port.out.TokenIssuerPort;
import com.arkorizon.service.auth.domain.service.LoginUseCaseImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UseCaseConfig {
@Bean
public LoginUseCase loginUseCase(
final FindUserByEmailPort findUserByEmailPort,
final PasswordVerifierPort passwordVerifierPort,
final TokenIssuerPort tokenIssuerPort,
final SaveRefreshTokenPort saveRefreshTokenPort
) {
return new LoginUseCaseImpl(
findUserByEmailPort,
passwordVerifierPort,
tokenIssuerPort,
saveRefreshTokenPort
);
}
}
Esse é o ponto em que Spring conecta as implementações concretas aos contratos do núcleo.
Passo 16: configurar Spring Security¶
package com.arkorizon.service.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login", "/actuator/health").permitAll()
.anyRequest().authenticated()
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Security configuration pertence à infraestrutura. Não coloque essa lógica no domínio.
Passo 17: tratar exceções HTTP¶
package com.arkorizon.service.auth.adapter.in.web.exception;
import com.arkorizon.service.auth.domain.exception.InvalidCredentialsException;
import com.arkorizon.service.auth.domain.exception.UserInactiveException;
import java.time.Instant;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InvalidCredentialsException.class)
public ResponseEntity<ApiErrorDTO> handleInvalidCredentials(
final InvalidCredentialsException exception
) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ApiErrorDTO("INVALID_CREDENTIALS", exception.getMessage(), Instant.now()));
}
@ExceptionHandler(UserInactiveException.class)
public ResponseEntity<ApiErrorDTO> handleUserInactive(
final UserInactiveException exception
) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ApiErrorDTO("USER_INACTIVE", exception.getMessage(), Instant.now()));
}
}
package com.arkorizon.service.auth.adapter.in.web.exception;
import java.time.Instant;
public record ApiErrorDTO(
String code,
String message,
Instant timestamp
) {
}
A exceção nasce no domínio. O HTTP status nasce no adapter web.
Como rodar com Spring Boot¶
Classe principal:
package com.arkorizon.service.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthApplication {
public static void main(final String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
Configuração:
server:
servlet:
context-path: /ark-auth-service
spring:
application:
name: ark-auth-service
datasource:
url: jdbc:postgresql://localhost:5432/arkorizon
username: ark_auth_user
password: ${ARK_AUTH_DB_PASSWORD}
data:
redis:
host: localhost
port: 6379
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
Comando local:
./mvnw spring-boot:run
Chamada HTTP:
curl -X POST http://localhost:8080/ark-auth-service/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@arkorizon.com","password":"secret"}'
Ordem recomendada para implementar¶
Para quem nunca implementou Hexagonal, siga esta ordem:
- Escreva o nome do caso de uso.
- Crie o
Command. - Crie o
Result. - Crie o
Input Port. - Crie os
Output Portsnecessários. - Crie os
Domain Models. - Implemente o
UseCaseImplem Java puro. - Teste o use case com mocks dos output ports.
- Crie DTOs do adapter web.
- Crie mapper web.
- Crie controller.
- Crie entities e repositories JPA.
- Crie mapper de persistência.
- Crie persistence adapter.
- Crie adapters de Redis, segurança, APIs externas ou filas.
- Configure beans no Spring.
- Rode a aplicação.
- Adicione testes de adapter e integração.
Como saber se fez certo¶
Você fez certo se consegue testar LoginUseCaseImpl sem Spring, sem banco, sem Redis e sem HTTP.
Exemplo:
class LoginUseCaseImplTest {
private final FindUserByEmailPort findUserByEmailPort = mock(FindUserByEmailPort.class);
private final PasswordVerifierPort passwordVerifierPort = mock(PasswordVerifierPort.class);
private final TokenIssuerPort tokenIssuerPort = mock(TokenIssuerPort.class);
private final SaveRefreshTokenPort saveRefreshTokenPort = mock(SaveRefreshTokenPort.class);
private final LoginUseCase loginUseCase = new LoginUseCaseImpl(
findUserByEmailPort,
passwordVerifierPort,
tokenIssuerPort,
saveRefreshTokenPort
);
}
Se esse teste exige subir contexto Spring, conectar banco ou instanciar controller, o núcleo está acoplado demais.
Resumo prático¶
| Se você precisa... | Crie... |
|---|---|
| Receber HTTP | Controller + DTO |
| Traduzir HTTP para núcleo | Web mapper |
| Representar entrada do use case | Command |
| Representar saída do use case | Result |
| Representar conceito de negócio | Domain model |
| Expor ação da aplicação | Input port |
| Pedir algo externo | Output port |
| Implementar banco | Persistence adapter |
| Mapear tabela | Entity |
| Consultar via Spring Data | Repository |
| Usar Redis | Redis adapter |
| Usar JWT/BCrypt | Security adapter |
| Conectar dependências | Configuration |
Regra final¶
Spring Boot, JPA, Redis, HTTP e security são ferramentas. Elas devem servir o núcleo, não definir o núcleo.
O núcleo deve continuar compreensível mesmo se todos os adapters forem removidos.