No desenvolvimento de sistemas modernos, a arquitetura hexagonal (ou arquitetura de portas e adaptadores) tem se destacado como uma abordagem poderosa para desacoplar a lógica de negócios das dependências externas, promovendo maior flexibilidade, escalabilidade e testabilidade. Em um cenário de processamento reativo, a utilização do WebClient, no lugar de abordagens tradicionais baseadas em blocking I/O, torna a aplicação ainda mais eficiente e escalável.
Neste projeto, exploramos como implementamos um serviço baseado na arquitetura hexagonal em um sistema que utiliza WebClient para realizar requisições HTTP reativas, garantindo um fluxo de dados eficiente e desacoplado para interações com sistemas externos.
O objetivo do projeto é criar um processador de usuários que se comunica com uma API externa utilizando uma abordagem reativa. Isso permite que o sistema seja altamente escalável e eficiente. A arquitetura hexagonal foi escolhida para garantir que o núcleo da aplicação ficasse completamente desacoplado das implementações externas, como a comunicação com o backend.
A ideia é que a lógica de negócios central interaja com as APIs de forma indireta, utilizando portas (interfaces) e adaptadores (implementações de adaptadores) para garantir que as dependências externas, como a API de usuário, possam ser facilmente alteradas sem impactar a aplicação como um todo.
A arquitetura hexagonal divide a aplicação em dois componentes principais:
- Núcleo da aplicação (domínio): Onde a lógica de negócios é centralizada.
- Portas e adaptadores: Que conectam o núcleo da aplicação ao mundo exterior (APIs, banco de dados, interfaces de usuário, etc.).
Com essa abordagem, o sistema se torna altamente flexível e fácil de testar, pois as dependências externas são tratadas como implementações de interfaces (adaptadores), enquanto a lógica de negócios continua sendo independente de qualquer tecnologia específica.
- Porta (Port): Define a interface ou contrato pelo qual o núcleo da aplicação interage com as dependências externas.
- Adaptador (Adapter): Implementação que adapta uma tecnologia ou sistema externo à interface definida pela porta.
No nosso caso, temos o seguinte fluxo:
- O serviço
ProcessorUserServiceImpl
é o adaptador que interage com o mundo externo. - A porta
AdapterPortUserService
define os métodos necessários para adaptar as entradas e saídas do sistema.
Em vez de utilizar o tradicional RestTemplate, que é bloqueante, optamos por usar o WebClient, uma ferramenta da Spring WebFlux para realizar requisições HTTP de forma não bloqueante e reativa.
A principal vantagem de usar o WebClient em uma arquitetura reativa é que ele permite que o sistema lide com chamadas assíncronas de forma eficiente, sem bloquear threads, o que é ideal para aplicações que precisam lidar com grandes volumes de requisições e dados em tempo real.
@Service
public class ProcessorUserServiceImpl implements ProcessorUserService {
private final WebClient webClient;
private final AdapterPortUserService adapter;
@Autowired
public ProcessorUserServiceImpl(AdapterPortUserService adapter, WebClient webClient) {
this.adapter = adapter;
this.webClient = webClient;
}
@Override
public <T, R> Mono<R> processarFluxoEspecifico(Object requestBody, Class<T> requestType, Class<R> responseType, String backendUrl) {
// Adapta o corpo de entrada para o tipo específico
T adaptedInput = adapter.adaptarEntrada(requestBody, requestType);
// Faz a requisição para o backend com WebClient
return webClient.post()
.uri(backendUrl) // URL dinâmica
.bodyValue(adaptedInput) // Envia o corpo de entrada adaptado
.retrieve()
.bodyToMono(responseType) // Retorna a resposta já como tipo específico
.onErrorResume(e -> Mono.just(adapter.adaptarMensagemErro(e.getMessage()))); // Tratamento de erro
}
}
A utilização de Portas e Adaptadores no seu projeto segue o modelo da arquitetura hexagonal. Aqui, temos as implementações de inbound e outbound que interagem com a lógica central da aplicação.
A camada inbound é representada pelo RunController, que recebe as requisições HTTP e as encaminha para o serviço adequado para processamento. Os métodos de controle do inbound tratam especificamente das entradas e saídas, utilizando os DTOs (UserRequest e UserResponse) para formatar os dados.
Copiar
@RestController
public class RunController {
private final Processor processor;
private final ProcessorUserService processorUserService;
public RunController(Processor processor, ProcessorUserService processorUserService) {
this.processor = processor;
this.processorUserService = processorUserService;
}
@GetMapping("/api/aggregated")
public Mono<UserResponse> obterDados(
@Validated @RequestBody UserRequest userRequest,
@RequestParam String backendUrl) {
return processorUserService.processarFluxoEspecifico(userRequest, UserRequest.class, UserResponse.class, backendUrl);
}
@GetMapping("/api/aggregated/generic")
public Mono<Object> obterDadosGenerico(
@RequestBody Object userRequest,
@RequestParam String backendUrl) {
return processor.processarFluxoGenerico(userRequest, Object.class, Object.class, backendUrl);
}
}
- O DTO UserRequest é utilizado para representar os dados que o cliente envia, e o DTO UserResponse representa a resposta formatada que será retornada ao cliente.
A camada outbound é responsável por adaptar os dados antes de enviá-los ao backend. No seu caso, a classe UserAdapterPortUser implementa a interface AdapterPortUserService e adapta tanto os dados de entrada quanto os de saída, além de tratar erros. Este componente implementa as funções de adaptação dos objetos entre a aplicação e o mundo exterior, garantindo que os dados sejam manipulados de maneira correta.
@Component
public class UserAdapterPortUser implements AdapterPortUserService {
@Override
public <T> T adaptarEntrada(Object requestBody, Class<T> requestType) {
return requestType.cast(requestBody); // Simples cast como exemplo
}
@Override
public <R> R adaptarResposta(Object response, Class<R> responseType) {
return responseType.cast(response); // Simples cast como exemplo
}
@SuppressWarnings("unchecked")
@Override
public ErrorResponse adaptarMensagemErro(String errorMessage) {
return new ErrorResponse("error", errorMessage, 500);
}
}
A interface AdapterPortUserService define os métodos para adaptar as entradas e saídas de dados de maneira genérica, permitindo que diferentes tecnologias ou tipos de dados possam ser tratados de forma consistente:
Copiar
public interface AdapterPortUserService {
<T> T adaptarEntrada(Object requestBody, Class<T> requestType);
<R> R adaptarResposta(Object response, Class<R> responseType);
<T> T adaptarMensagemErro(String message);
}
Os DTOs (Data Transfer Objects) são essenciais para garantir que os dados enviados e recebidos sejam bem estruturados. O UserRequest representa a entrada dos dados que o cliente fornece ao chamar a API, enquanto o UserResponse define a resposta que será retornada após o processamento.
- UserRequest: Contém os dados que o cliente envia para que a aplicação possa processá-los. UserResponse: Representa a resposta adaptada que será retornada para o cliente após a comunicação com o backend.
A arquitetura hexagonal, combinada com a abordagem reativa do WebClient, oferece uma solução altamente escalável, flexível e fácil de manter para sistemas que precisam interagir com APIs externas. Ao adotar esse modelo, conseguimos criar uma aplicação desacoplada, eficiente e pronta para crescer conforme as necessidades de negócio evoluem.
A escolha de tecnologias como Spring WebFlux e WebClient torna a aplicação não apenas mais eficiente, mas também mais alinhada com as práticas modernas de desenvolvimento de sistemas distribuídos e escaláveis.
- Refatoração contínua: A arquitetura hexagonal permite que o código seja constantemente refatorado sem impactar diretamente a lógica de negócios, garantindo que o sistema se mantenha saudável à medida que novas funcionalidades sejam implementadas.
- Expansão de testes: Ampliar a cobertura de testes de integração, garantindo que o comportamento do sistema frente a diferentes tipos de erros externos seja bem validado. Escalabilidade: Continuar monitorando o desempenho da