Pular para o conteúdo

Adapters

Se a porta é o contrato do núcleo, o adapter é o tradutor que faz uma tecnologia concreta conversar com esse contrato. Ele isola o núcleo de tudo que é volátil: protocolos, formatos de serialização, dialetos de banco, SDKs de terceiros. Quando a tecnologia muda, muda-se o adapter; o núcleo permanece intacto.

Um adapter tem uma única responsabilidade: converter e delegar. Converter o dado externo para o tipo do domínio (e vice-versa) e delegar a decisão de negócio para a porta. Ele nunca decide regras.

Ficam no lado esquerdo do hexágono e acionam o núcleo. Recebem um estímulo do mundo externo e o transformam em uma chamada a uma driving port.

  • Controller REST: desserializa JSON, valida formato, chama o caso de uso, serializa a resposta.
  • CLI: faz parsing de argumentos e invoca o caso de uso.
  • Consumer de fila: lê a mensagem, converte e chama o caso de uso.
  • Agendador (scheduler): dispara casos de uso periodicamente.
package com.exemplo.banco.infrastructure.rest;
import com.exemplo.banco.application.port.in.AbrirContaUseCase;
import com.exemplo.banco.domain.Conta;
import java.math.BigDecimal;
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("/contas")
public class ContaRestController {
private final AbrirContaUseCase abrirContaUseCase;
public ContaRestController(AbrirContaUseCase abrirContaUseCase) {
this.abrirContaUseCase = abrirContaUseCase;
}
public record AbrirContaRequest(String titular, BigDecimal depositoInicial) {}
public record ContaResponse(String numero, String titular, BigDecimal saldo) {}
@PostMapping
public ResponseEntity<ContaResponse> abrir(@RequestBody AbrirContaRequest req) {
Conta conta = abrirContaUseCase.abrir(req.titular(), req.depositoInicial());
ContaResponse body = new ContaResponse(
conta.numero(),
conta.titular(),
conta.saldo().valor());
return ResponseEntity.ok(body);
}
}

Ficam no lado direito e são acionados pelo núcleo. Implementam as driven ports, fornecendo a tecnologia concreta que o domínio pediu de forma abstrata.

  • Adapter JPA: implementa ContaRepository mapeando entre a entidade de domínio e a entidade persistente.
  • Client HTTP: implementa um gateway chamando uma API externa.
  • Produtor de fila: implementa uma porta de notificação publicando eventos.
package com.exemplo.banco.infrastructure.persistence;
import com.exemplo.banco.application.port.out.ContaRepository;
import com.exemplo.banco.domain.Conta;
import java.util.Optional;
import org.springframework.stereotype.Component;
@Component
public class ContaJpaAdapter implements ContaRepository {
private final ContaJpaRepository jpaRepository;
public ContaJpaAdapter(ContaJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Conta salvar(Conta conta) {
ContaJpaEntity entity = ContaJpaEntity.deDominio(conta);
return jpaRepository.save(entity).paraDominio();
}
@Override
public Optional<Conta> porNumero(String numero) {
return jpaRepository.findByNumero(numero).map(ContaJpaEntity::paraDominio);
}
}

O princípio é manter um adapter dedicado por tecnologia externa. Isso significa que oferecer a mesma funcionalidade por REST e por CLI resulta em dois adapters primários distintos, ambos chamando a mesma driving port. Da mesma forma, trocar Postgres por Mongo é escrever um novo adapter secundário que implementa a mesma driven port, sem tocar no núcleo. Essa granularidade é o que dá à arquitetura sua portabilidade e testabilidade.

graph LR
    subgraph Primarios[Adapters Primarios]
        R[Controller REST]
        C[CLI]
        Q1[Consumer Fila]
    end

    subgraph Nucleo[Nucleo]
        DIN[Driving Ports]
        DOUT[Driven Ports]
    end

    subgraph Secundarios[Adapters Secundarios]
        J[Adapter JPA]
        H[Client HTTP]
        Q2[Produtor Fila]
    end

    R --> DIN
    C --> DIN
    Q1 --> DIN
    J -.implementa.-> DOUT
    H -.implementa.-> DOUT
    Q2 -.implementa.-> DOUT

Setas cheias: chamada dos adapters primários às driving ports. Setas tracejadas: os adapters secundários implementam as driven ports. Nos dois lados, a dependência de código aponta para o núcleo.

  • Regra de negócio no adapter: validar saldo, calcular juros ou decidir fluxo dentro do controller. Isso pertence ao núcleo.
  • Adapter que conhece outro adapter: o controller chamando o repositório JPA diretamente, pulando a porta e o application service.
  • Entidade única atravessando tudo: usar a @Entity JPA como objeto de domínio, DTO de request e resposta REST ao mesmo tempo — acopla persistência, transporte e regra.
  • Mapeamento espalhado: conversões domínio↔DTO/entidade fora do adapter, vazando formato externo para o núcleo.
  • Adapter multitarefa: um mesmo componente falando REST e mexendo direto no banco, violando “um adapter por tecnologia”.

1. Qual a única responsabilidade legítima de um adapter?

Resposta

Converter e delegar: traduzir o dado externo para o tipo do domínio (e vice-versa) e delegar a decisão de negócio para a porta. Ele nunca implementa regras de negócio.

2. Por que “um adapter por tecnologia” e não um adapter genérico?

Resposta

Porque cada tecnologia tem detalhes voláteis próprios (protocolo, formato, SDK). Adapters dedicados permitem adicionar uma nova entrada (CLI além de REST) ou trocar uma saída (Mongo no lugar de Postgres) sem tocar o núcleo nem os outros adapters, preservando a portabilidade.

3. Um controller REST pode chamar o repositório JPA diretamente? Por quê?

Resposta

Não. Isso pula a driving port e o application service, jogando fora o isolamento do núcleo e permitindo que orquestração e regra vazem para o controller. O controller deve chamar apenas a driving port; a persistência é acessada pelo núcleo via driven port.

4. Qual a diferença de direção entre um adapter primário e um secundário?

Resposta

O adapter primário aciona o núcleo (chama uma driving port), estando no lado driving/esquerdo. O secundário é acionado pelo núcleo (implementa uma driven port), estando no lado driven/direito. Em ambos, a dependência de código aponta para dentro, mas o fluxo de controle vai em sentidos opostos.