Introdução: A Dor do Acoplamento

Se você desenvolve com .NET há algum tempo, provavelmente já viu ou escreveu um Controller assim: ele recebe uma requisição HTTP, valida os dados, manipula a lógica de negócio, conversa diretamente com o Entity Framework, talvez chame um HttpClient para outra API e, por fim, retorna um ActionResult. No início, parece produtivo. Mas com o tempo, esse controller se torna um monstro: frágil, difícil de testar e terrivelmente acoplado à tecnologia.

Mudar do ASP.NET Core para um worker service? Um pesadelo. Trocar o Entity Framework por Dapper? Reescrita massiva. Testar a lógica de negócio sem subir um servidor web e um banco de dados? Quase impossível.

Essa dor tem uma causa: misturamos o “o quê” (as regras de negócio) com o “como” (os detalhes de infraestrutura). A Arquitetura Hexagonal, também conhecida como Ports & Adapters, surge como uma solução elegante para esse problema, propondo uma separação radical entre a sua aplicação e o mundo exterior.

Neste artigo, vamos desvendar essa arquitetura, não apenas na teoria, mas na prática, analisando o código-fonte de um projeto real: o FluxoSeguro, um microserviço de autenticação e autorização em .NET.

O que é a Arquitetura Hexagonal (Ports & Adapters)?

Criada por Alistair Cockburn, a ideia central é simples: proteger o coração da sua aplicação (o domínio e a lógica de aplicação) de ser contaminado por detalhes de tecnologia.

Imagine sua aplicação como um núcleo, o “hexágono”. Esse núcleo não sabe e não se importa se está sendo usado por uma API REST, uma aplicação de console, um gRPC ou um teste unitário. Ele também não se importa se os dados vêm do PostgreSQL, SQL Server ou de um arquivo em memória.

Essa comunicação entre o núcleo (“dentro”) e o mundo exterior (“fora”) é feita através de Ports e Adapters.

Arquitetura Hexagonal

Ports: As Portas de Entrada e Saída

Um Port é simplesmente uma interface, um contrato. Ele define uma forma de interação, mas não a implementa. Existem dois tipos de ports:

  1. Driving Ports (Portas Primárias): Definem como o mundo exterior pode “dirigir” (acionar) sua aplicação. Pense nelas como a API do seu núcleo de negócio. Geralmente são interfaces de serviço, como IUserRegistrationService.
  2. Driven Ports (Portas Secundárias): Definem o que sua aplicação precisa do mundo exterior. São dependências que o núcleo precisa que sejam satisfeitas, como IUserRepository ou IEmailNotificationService.

Adapters: Os Tradutores Tecnológicos

Um Adapter é a implementação concreta de um Port. Ele “adapta” uma tecnologia específica para que ela possa se comunicar com a aplicação através de um port.

  1. Driving Adapters (Adaptadores Primários): São eles que iniciam a comunicação com a aplicação. Um Controller de API é um exemplo perfeito: ele recebe uma requisição HTTP (tecnologia), a traduz para objetos que o núcleo entende e chama um Driving Port. Outros exemplos incluem handlers de filas ou um cliente de linha de comando.
  2. Driven Adapters (Adaptadores Secundários): São as implementações das dependências da aplicação. Um repositório usando Entity Framework é um exemplo clássico: ele implementa a interface IUserRepository (Driven Port) usando a tecnologia do EF Core para conversar com o banco de dados. Um serviço que envia e-mails via SendGrid seria outro exemplo.

Por que Usar a Arquitetura Hexagonal? Os Benefícios Chave

Adotar essa arquitetura traz vantagens que mudam o jogo no desenvolvimento de software a longo prazo:

  • Isolamento do Domínio: Sua lógica de negócio se torna um ativo puro, livre de qualquer dependência de framework ou infraestrutura. Ela pode ser reutilizada em diferentes contextos com muito mais facilidade.
  • Testabilidade Aumentada: Você pode testar todo o seu núcleo de negócio em isolamento total. Como os Driven Ports são interfaces, você pode facilmente substituí-los por mocks ou stubs nos seus testes unitários, sem precisar de banco de dados, servidor web ou qualquer outra dependência externa.
  • Flexibilidade Tecnológica: A arquitetura brilha quando a tecnologia precisa mudar. Quer expor sua lógica via gRPC em vez de REST? Crie um novo Driving Adapter. Quer migrar do RabbitMQ para o Kafka? Crie um novo Driven Adapter para a sua interface de mensageria. O núcleo da aplicação permanece intocado.
  • Aderência aos Princípios SOLID: A arquitetura é uma manifestação prática do Princípio da Inversão de Dependência (DIP). O núcleo não depende da infraestrutura; ambos dependem de abstrações (os Ports).

Análise Prática: Do Controller Tradicional à Abordagem Hexagonal

Vamos comparar as duas abordagens usando um caso de uso comum: o registro de um novo usuário.

Cenário 1: O Controller “Tudo em Um” (O Anti-Padrão)

Um código fortemente acoplado poderia se parecer com isto:

// Exemplo Hipotético de um Controller Acoplado
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly AppDbContext _context;
    private readonly SmtpClient _smtpClient;

    public UsersController(UserManager<IdentityUser> userManager, AppDbContext context, SmtpClient smtpClient)
    {
        _userManager = userManager;
        _context = context;
        _smtpClient = smtpClient;
    }

    [HttpPost("register")]
    public async Task<IActionResult> Register(RegisterModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var user = new IdentityUser { UserName = model.Email, Email = model.Email };
        
        // Acoplado ao ASP.NET Identity
        var result = await _userManager.CreateAsync(user, model.Password);

        if (!result.Succeeded)
        {
            return BadRequest(result.Errors);
        }

        // Lógica de negócio misturada com acesso a dados
        // Acoplado ao Entity Framework Core
        var userProfile = new UserProfile { UserId = user.Id, FullName = model.FullName };
        _context.UserProfiles.Add(userProfile);
        await _context.SaveChangesAsync();
        
        // Acoplado à implementação de e-mail
        await _smtpClient.SendMailAsync("from@example.com", model.Email, "Welcome!", "...");

        return Ok();
    }
}

Este código é um pesadelo para testar e manter.

Cenário 2: A Abordagem Hexagonal no FluxoSeguro

Agora, vamos ver como o projeto FluxoSeguro lida com a mesma tarefa. O fluxo é desacoplado e segue os princípios da Arquitetura Hexagonal, usando MediatR para orquestrar as ações.

1. O Driving Adapter: UsersController

O Controller é enxuto. Sua única responsabilidade é receber a requisição HTTP, traduzi-la em um comando e despachá-lo. Ele não sabe como o usuário será criado.

Localização: FluxoSeguro.Services.Auth/API/Controllers/UsersController.cs

[ApiController]
[Route("api/v1/users")]
public class UsersController : ControllerBase
{
    private readonly IMediator _mediator;

    public UsersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost("register")]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserCommand command)
    {
        var result = await _mediator.Send(command);

        // ... tratamento de resultado ...

        return CreatedAtAction(nameof(GetUserById), new { id = result.Data.Id }, result.Data);
    }
}

O Controller depende do MediatR e não tem ideia da lógica de negócio. Este é um Driving Adapter clássico.

2. O Application Core: CreateUserCommandHandler

Este é o coração da nossa funcionalidade. Ele pertence à camada de Aplicação e orquestra o processo de criação do usuário. Note que ele depende de uma abstração (IUserService), que é nosso Port.

Localização: FluxoSeguro.Services.Auth/Application/Features/Users/Commands/CreateUser/CreateUserCommandHandler.cs

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>>
{
    private readonly IUserService _userService;
    private readonly IUnitOfWork _unitOfWork;

    public CreateUserCommandHandler(IUserService userService, IUnitOfWork unitOfWork)
    {
        _userService = userService;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<UserResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        // ... validações ...

        var user = new User
        {
            // ... mapeamento de request para entidade ...
        };

        // Usa o Port para executar a ação
        var result = await _userService.CreateUserAsync(user, request.Password);
        
        if (!result.Succeeded)
        {
            return Result<UserResponse>.Fail(result.Errors.FirstOrDefault()?.Description);
        }

        await _unitOfWork.CommitAsync();

        var response = user.ToResponse();
        return Result<UserResponse>.Success(response);
    }
}

Este handler é totalmente testável em isolamento. Podemos “mockar” IUserService e IUnitOfWork para verificar sua lógica sem tocar em nenhuma infraestrutura real.

3. Os Ports (Interfaces)

As interfaces IUserService e IUnitOfWork são os contratos que o núcleo da aplicação exige.

  • IUserService (Driven Port): Define a operação de criar um usuário, abstraindo os detalhes do provedor de identidade.
  • IUnitOfWork (Driven Port): Define a operação de persistir as mudanças, abstraindo o banco de dados.

4. O Driven Adapter: UserService

Finalmente, na camada de Infraestrutura, encontramos a implementação concreta do port IUserService. Este adapter “traduz” a chamada do nosso port para o mundo do ASP.NET Core Identity.

Localização: FluxoSeguro.Services.Auth/Infrastructure/Services/UserService.cs

public class UserService : IUserService
{
    private readonly UserManager<User> _userManager;

    public UserService(UserManager<User> userManager)
    {
        _userManager = userManager;
    }

    public async Task<(bool Succeeded, IEnumerable<IdentityError> Errors)> CreateUserAsync(User user, string password)
    {
        var result = await _userManager.CreateAsync(user, password);
        
        if (!result.Succeeded)
        {
            return (false, result.Errors);
        }

        // Poderia adicionar a uma role aqui, etc.
        // await _userManager.AddToRoleAsync(user, "Default");

        return (true, null);
    }

    // ... outros métodos ...
}

Se amanhã decidirmos trocar o ASP.NET Core Identity por outro sistema de identidade, apenas esta classe (o adapter) precisaria ser substituída. O CreateUserCommandHandler permaneceria intacto.

Quando Usar (e Quando Não Usar) a Arquitetura Hexagonal?

Apesar de poderosa, ela não é uma bala de prata.

Use-a quando:

  • O projeto tem regras de negócio complexas que são o verdadeiro ativo do software.
  • O sistema é projetado para ter uma vida longa, onde a troca de tecnologias é uma possibilidade real.
  • Você está construindo microserviços que precisam de limites claros e alta coesão.
  • A testabilidade é uma prioridade máxima para a equipe.

Pode ser excessivo quando:

  • Você está construindo um CRUD simples ou uma aplicação com pouca lógica de negócio.
  • O projeto é um protótipo rápido ou uma prova de conceito.
  • A equipe não tem familiaridade com os conceitos de design guiado pelo domínio e inversão de dependência.

Conclusão: Construindo Software Resiliente

A Arquitetura Hexagonal nos força a pensar sobre o que é verdadeiramente central em nosso software: a lógica de negócio. Ao tratá-la como o núcleo e isolá-la de detalhes de implementação através de Ports & Adapters, construímos sistemas mais robustos, flexíveis e, acima de tudo, mais fáceis de manter e testar a longo prazo.

Não é apenas um padrão de arquitetura; é uma mudança de mentalidade que nos leva a escrever código melhor, mais limpo e preparado para o futuro.

Convido você a explorar o repositório FluxoSeguro no GitHub, analisar a estrutura dos projetos e ver como esses conceitos foram aplicados em um cenário de microserviços.