Skip to content

Latest commit

 

History

History
250 lines (191 loc) · 6.87 KB

File metadata and controls

250 lines (191 loc) · 6.87 KB

Arquitetura — jussub

O projeto segue o padrão Hexagonal Architecture (Ports & Adapters), também conhecido como Clean Architecture.


Conceito

O núcleo da aplicação (regras de negócio) fica isolado no centro. Tudo que é externo — banco, SQS, APIs — se conecta por meio de portas (interfaces) e adapters (implementações).

           [SQS]      [HTTP]
              ↓          ↓
         adapter/in  ←  entrada
              ↓
         usecase/in  ←  porta de entrada (interface)
              ↓
      application/service  ←  regra de negócio
              ↓
         usecase/out  ←  porta de saída (interface)
              ↓
         adapter/out  ←  saída
              ↓
     [PostgreSQL]  [SNS]  [Feign APIs]

Regra fundamental: o service nunca depende de uma implementação concreta. Ele só conhece interfaces (usecase/out). Os adapters implementam essas interfaces.


Estrutura de Pacotes

br.com.jusfy
├── adapter
│   ├── in
│   │   ├── consumer/{dominio}/         ← @SqsListener
│   │   └── rest
│   │       ├── exception/handler/      ← ApiExceptionHandler + ApiError DTO
│   │       └── {dominio}/{operacao}/   ← Controller + Mapper + dto/
│   └── out
│       ├── feign/{servico}/            ← @FeignClient + dto/
│       ├── repository/{dominio}/       ← JpaRepository + Adapter
│       └── sns/                        ← SnsPublisherClient
├── application
│   ├── exception/                      ← Exceções de negócio
│   ├── model
│   │   ├── domain/{dominio}/           ← Records de evento/domínio
│   │   ├── entity/                     ← Entidades JPA
│   │   └── enumeration/                ← Enums
│   └── service
│       ├── {dominio}/                  ← Services de negócio
│       └── utils/                      ← EventEnvelopeUtils, TraceIdSupport
├── config/                             ← Beans Spring (SQS, SNS, Feign, Cache…)
└── usecase
    ├── in/{dominio}/                   ← Inbound ports
    └── out/{dominio}/                  ← Outbound ports

Camadas em Detalhe

adapter/in — Entradas

O que dispara algo na aplicação.

Tipo Anotação Responsabilidade
Consumer @SqsListener Recebe mensagem SQS, delega para usecase/in
Controller @RestController Recebe HTTP, delega para usecase/in

Sem lógica de negócio. Só recebe, converte e delega.


usecase/in — Portas de Entrada

Interfaces que definem o que a aplicação oferece.

// Quem implementa: Services
// Quem usa: Controllers e Consumers
public interface CreateSubscriptionUseCase {
    Subscription create(CreateSubscriptionDomain domain);
}

application/service — Regra de Negócio

Onde a lógica de negócio vive. Implementa usecase/in e depende de usecase/out.

@Service
@RequiredArgsConstructor
@Slf4j
public class CreateSubscriptionService implements CreateSubscriptionUseCase {

    private final SubscriptionRepositoryUseCase subscriptionRepositoryUseCase; // interface!

    @Override
    public Subscription create(CreateSubscriptionDomain domain) {
        // lógica aqui
    }
}

usecase/out — Portas de Saída

Interfaces que definem o que a aplicação precisa de fora.

// Quem implementa: Adapters (repository, feign…)
// Quem usa: Services
public interface SubscriptionRepositoryUseCase {
    Subscription save(Subscription subscription);
    Optional<Subscription> findById(UUID id);
    boolean existsById(UUID id);
}

adapter/out — Saídas

Implementam as interfaces de usecase/out, conectando a aplicação ao mundo externo.

Tipo Responsabilidade
RepositoryAdapter Implementa RepositoryUseCase, chama JpaRepository
FeignClient Chamadas HTTP para outros serviços
SnsPublisherClient Publica eventos no AWS SNS

Exceptions

Hierarquia

RuntimeException
└── BusinessException               ← exceção base (genérica)
    └── SubscriptionAlreadyExistsException
    └── {NovaExcecaoEspecifica}Exception

Como criar uma exceção de negócio

1. Criar a classe em application/exception/:

public class SubscriptionNotFoundException extends BusinessException {
    public SubscriptionNotFoundException(String message) {
        super(message);
    }
}

2. Lançar no service:

return subscriptionRepositoryUseCase.findById(id)
        .orElseThrow(() -> new SubscriptionNotFoundException("Subscription not found: " + id));

3. Mapear no ApiExceptionHandler:

@ExceptionHandler(SubscriptionNotFoundException.class)
public ResponseEntity<ApiError> handleSubscriptionNotFound(
        SubscriptionNotFoundException ex, HttpServletRequest request) {

    HttpStatus status = HttpStatus.NOT_FOUND;
    ApiError body = ApiError.builder()
            .timestamp(Instant.now())
            .status(status.value())
            .error("SUBSCRIPTION_NOT_FOUND")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .build();

    return ResponseEntity.status(status).body(body);
}

Formato padrão de erro (ApiError)

{
  "timestamp": "2026-03-16T10:00:00Z",
  "status": 404,
  "error": "SUBSCRIPTION_NOT_FOUND",
  "message": "Subscription not found: 550e8400-...",
  "path": "/subscriptions/550e8400-..."
}

Configurações

Classe Responsabilidade
GsonConfig Bean Gson com serialização de LocalDateTime
AwsSqsClientConfig SQS async client
SnsConfig SNS client
CacheConfig Caffeine — cache de token (50 min)
AuthenticatorFeignConfig Logging e content-type do Feign
JusPayFeignConfig Interceptor Bearer token para JusPay
TraceIdMdcFilter Propaga traceId em requisições HTTP
SqsListenerTraceIdAspect AOP — extrai traceId de mensagens SQS

Banco de Dados

  • PostgreSQL com migrations via Flyway
  • Arquivos em src/main/resources/db/migrations/
  • Nomenclatura: V{numero}__{descricao}.sql
-- V1__create_subscriptions_table.sql
CREATE TABLE subscriptions (
    id               UUID PRIMARY KEY,
    customer_id      UUID         NOT NULL,
    status           VARCHAR(50)  NOT NULL,
    cycle_start_date TIMESTAMP,
    cycle_end_date   TIMESTAMP,
    cycle_number     INT          NOT NULL,
    cancelled_at     TIMESTAMP,
    created_at       TIMESTAMP    NOT NULL,
    updated_at       TIMESTAMP    NOT NULL
);

Cobertura de Testes

Mínimo exigido pelo JaCoCo: 89% de linhas cobertas.

./mvnw verify  # roda testes + verifica cobertura