Skip to content

enisonoliveira/bff-exagonal-implementation

bff-exagonal-implementation

Desenvolvimento de uma Arquitetura Hexagonal com WebClient para Processamento Reativo

Introdução

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 Desafio: Processamento de Dados de Forma Desacoplada

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.

Arquitetura Hexagonal: O Poder do Desacoplamento

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.

Componentes Chave da Arquitetura

  • 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.

WebClient e a Abordagem Reativa

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
    }
}

Adapters e Ports: A Integração da Arquitetura

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.

Camada Inbound: Recebendo Requisições do Cliente

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.

Camada Outbound: Adaptando para o Backend

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);
}

DTOs: Representando Dados de Entrada e Saída

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.

Conclusão

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.

Próximos Passos

  • 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

About

Exagonal implementation

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages