Executando verificação de segurança...
2

Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 3.3 - Gateways e Mappers: defendendo o domínio

Capa

Depois de percorrer os padrões ligados ao coração do domínio e à persistência de dados, chegamos agora à camada mais visível de qualquer aplicação: a apresentação. É aqui que usuários, sistemas externos e integrações tocam o nosso software pela primeira vez. E, embora pareça apenas uma questão de “UI” ou “framework web”, a forma como estruturamos essa porta de entrada pode fortalecer, ou corroer, toda a arquitetura.

Martin Fowler dedicou uma parte importante do seu catálogo de Patterns of Enterprise Application Architecture a esse tema. Ele descreveu como padrões como MVC, Front Controller, Template View e Page Controller organizam a interação inicial com o sistema. São soluções para problemas recorrentes: evitar duplicação de lógica de roteamento, separar responsabilidades de renderização, manter consistência no fluxo de requisições.

Eric Evans, em Domain-Driven Design, lembra que o papel da camada de apresentação é traduzir intenções. Ela deve transformar cliques, mensagens ou chamadas HTTP em comandos compreensíveis para o domínio, preservando a Linguagem Ubíqua. O usuário nunca deveria sentir que está falando com tabelas ou APIs, mas sim com conceitos do negócio.

Já Robert C. Martin, em Clean Architecture, é incisivo: a web, o framework e a interface são apenas detalhes. Controllers, views ou middlewares não podem ditar a forma do modelo. A arquitetura deve gritar casos de uso, e não endpoints ou verbos HTTP.

Nesta terceira parte da série, exploraremos esses padrões de entrada e integração com o mesmo olhar triplo: Fowler com seu catálogo pragmático, Evans com a defesa do domínio como centro, e Uncle Bob com a disciplina que protege esse centro contra a pressão das bordas.


Índice da Série

Parte 1 – A lógica do domínio

Parte 2 – Persistência e Infraestrutura

Parte 3 – Apresentação e Integração


Gateways e Mappers: defendendo o domínio

O problema da contaminação

Depois de organizar a entrada (MVC, Front Controller, Page Controller) e a saída (Template View), falta encarar um ponto inevitável: nenhum sistema vive sozinho. Em algum momento ele vai falar com um banco de dados, com uma API de terceiros, com um serviço legado. O risco é deixar que esses detalhes externos contaminem o coração do sistema.

Martin Fowler, em Patterns of Enterprise Application Architecture, descreve dois padrões fundamentais para essa defesa: o Gateway (p. 466) e o Mapper (p. 473). Eles não são glamour arquitetural, mas trincheiras contra a erosão da linguagem ubíqua. Eric Evans reforça em Domain-Driven Design que a função deles é proteger o domínio dentro de uma Anti-Corruption Layer (ACL). Já Robert C. Martin, em Clean Architecture, coloca ambos na periferia: detalhes que orbitam o núcleo, sempre substituíveis.


Gateway: encapsulando sistemas externos

O Gateway é um objeto que fornece uma interface clara para um recurso externo. Em vez de deixar o domínio lidar com HttpClient, strings de URL ou SQL, criamos uma fachada tipada que concentra essa responsabilidade.

Isso nos dá três benefícios diretos:

  1. Isolamento semântico: o domínio não fala em endpoints ou queries, mas em métodos significativos.
  2. Evolução controlada: se a API externa mudar, só o Gateway precisa ser ajustado.
  3. Testabilidade: é possível simular o Gateway em testes sem precisar chamar o mundo real.

Exemplo em ASP.NET Core: consumir uma API de precificação e estoque.

public interface IInventoryPricingGateway
{
    Task<QuoteApiResponse> QuoteAsync(QuoteApiRequest request, CancellationToken ct = default);
    Task<ReserveApiResponse> ReserveAsync(ReserveApiRequest request, CancellationToken ct = default);
}

public class InventoryPricingGateway : IInventoryPricingGateway
{
    private readonly HttpClient _http;

    public InventoryPricingGateway(HttpClient http) => _http = http;

    public async Task<QuoteApiResponse> QuoteAsync(QuoteApiRequest request, CancellationToken ct = default)
    {
        var response = await _http.PostAsJsonAsync("/quote", request, ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<QuoteApiResponse>(cancellationToken: ct);
    }

    public async Task<ReserveApiResponse> ReserveAsync(ReserveApiRequest request, CancellationToken ct = default)
    {
        var response = await _http.PostAsJsonAsync("/reserve", request, ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<ReserveApiResponse>(cancellationToken: ct);
    }
}

// DTOs da API externa
public record QuoteApiRequest(string Currency, IEnumerable<string> Items, string Channel, decimal Gross);
public record QuoteApiResponse(decimal Net);

public record ReserveApiRequest(Guid OrderId, IEnumerable<string> Items, decimal Net, string Currency, Guid IdempotencyKey);
public record ReserveApiResponse(string Status);

Nada aqui tem cheiro de domínio. O Gateway fala na língua da API externa e oferece métodos claros para o resto do sistema.


Mapper: o tradutor de modelos

O Mapper resolve outro problema: quando os modelos não se encaixam. O domínio tem suas entidades e value objects; a API externa fala em DTOs achatados e strings soltas. O Mapper traduz entre esses mundos.

Mais do que um simples 1:1, ele normaliza, faz flattening, traduz enums e aplica defaults. É o tradutor cultural entre modelos.

// Entidade de domínio
public sealed class Order
{
    public Guid Id { get; }
    public string CustomerName { get; }
    public Channel SalesChannel { get; }
    public IReadOnlyCollection<string> SkuItems { get; }
    public Money TotalGross { get; private set; }
    public Money TotalNet { get; private set; }
    public bool Reserved { get; private set; }

    private readonly List<IDomainEvent> _events = new();
    public IReadOnlyCollection<IDomainEvent> Events => _events;

    public Order(Guid id, string customerName, Channel channel, IEnumerable<string> skuItems, Money gross)
    {
        Id = id;
        CustomerName = customerName.Trim();
        SalesChannel = channel;
        SkuItems = skuItems.ToArray();
        TotalGross = gross;
        TotalNet = gross;
        _events.Add(new OrderReservationRequestedEvent(Id, channel.Code));
    }

    public void ApplyQuote(Quote quote) =>
        TotalNet = new Money(quote.NetAmount, TotalGross.Currency);

    public void MarkReserved(Reservation reservation)
    {
        if (!reservation.Success) throw new InvalidOperationException("Reservation failed");
        Reserved = true;
    }
}

// Evento de domínio
public record OrderReservationRequestedEvent(Guid OrderId, string Channel) : IDomainEvent;

// Factory responsável pela criação da Order
public sealed class OrderFactory
{
    public Order Create(PlaceOrderCommandDto dto)
    {
        var currency = new Currency(dto.CurrencyCode.ToUpperInvariant());
        var gross = new Money(dto.GrossAmount, currency);
        var channel = NormalizeChannel(dto.SalesChannel);
        return new Order(Guid.NewGuid(), dto.CustomerName, channel, dto.SkuItems, gross);
    }

    private static Channel NormalizeChannel(string input) =>
        input.ToLowerInvariant() switch
        {
            "web" or "online" => new Channel("web"),
            "marketplace" or "mp" => new Channel("marketplace"),
            _ => new Channel("kiosk")
        };
}

// Mapper
public sealed class OrderMapper
{
    public QuoteApiRequest MapToQuoteRequest(Order order) =>
        new QuoteApiRequest(order.TotalGross.Currency.Code, order.SkuItems, order.SalesChannel.Code, order.TotalGross.Amount);

    public Quote MapFromQuoteResponse(QuoteApiResponse response) =>
        new Quote(response.Net);

    public ReserveApiRequest MapToReserveRequest(Order order) =>
        new ReserveApiRequest(order.Id, order.SkuItems, order.TotalNet.Amount, order.TotalNet.Currency.Code, order.Id);

    public Reservation MapFromReserveResponse(ReserveApiResponse response) =>
        new Reservation(response.Status is "reserved" or "already_reserved");
}

Repare: o Mapper não apenas copia valores, mas interpreta, valida, normaliza. E a Factory é quem garante a criação inicial correta da entidade.


Use case com evento de reserva

Para não correr o risco de chamar o Gateway e falhar no CommitAsync, usamos eventos de domínio junto com o outbox. O caso de uso grava a entidade e registra o evento OrderReservationRequestedEvent. Após o commit, um handler dispara a chamada externa.

public sealed class PlaceOrderHandler
{
    private readonly IOrderRepository _orders;
    private readonly OrderFactory _factory;
    private readonly IUnitOfWork _uow;

    public PlaceOrderHandler(IOrderRepository orders, OrderFactory factory, IUnitOfWork uow)
    {
        _orders = orders;
        _factory = factory;
        _uow = uow;
    }

    public async Task<Guid> HandleAsync(PlaceOrderCommandDto input, CancellationToken ct = default)
    {
        var order = _factory.Create(input);
        await _orders.AddAsync(order, ct);
        await _uow.CommitAsync(ct); // Evento é persistido junto no outbox

        return order.Id;
    }
}

// Handler do evento de domínio
public sealed class OrderReservationRequestedHandler : INotificationHandler<OrderReservationRequestedEvent>
{
    private readonly IInventoryPricingGateway _gateway;
    private readonly OrderMapper _mapper;
    private readonly IOrderRepository _orders;
    private readonly IUnitOfWork _uow;

    public OrderReservationRequestedHandler(
        IInventoryPricingGateway gateway,
        OrderMapper mapper,
        IOrderRepository orders,
        IUnitOfWork uow)
    {
        _gateway = gateway;
        _mapper = mapper;
        _orders = orders;
        _uow = uow;
    }

    public async Task Handle(OrderReservationRequestedEvent notification, CancellationToken ct)
    {
        var order = await _orders.GetByIdAsync(notification.OrderId, ct);
        if (order is null || order.Reserved) return;

        var quoteRes = await _gateway.QuoteAsync(_mapper.MapToQuoteRequest(order), ct);
        order.ApplyQuote(_mapper.MapFromQuoteResponse(quoteRes));

        var reserveRes = await _gateway.ReserveAsync(_mapper.MapToReserveRequest(order), ct);
        order.MarkReserved(_mapper.MapFromReserveResponse(reserveRes));

        await _uow.CommitAsync(ct);
    }
}

Assim, a chamada ao sistema externo só ocorre após o commit do estado local e do evento. Se o commit falhar, nada externo é invocado. Se a chamada externa falhar, o evento fica na outbox e pode ser reprocessado.


Mapper de Fowler e a ACL de Evans

Fowler descreveu o Mapper como o padrão que traduz entre dois modelos. Evans, em contrapartida, falou da Anti-Corruption Layer como a estratégia para proteger o domínio inteiro contra modelos externos. O Mapper é uma peça dentro dessa camada. A ACL combina Mappers, Gateways, Facades e Adapters para impedir que conceitos de fora corrompam a linguagem ubíqua.

Uncle Bob sintetiza: tudo isso são detalhes, periféricos. O núcleo não pode depender deles.


Conclusão

Gateways e Mappers são menos sobre elegância e mais sobre disciplina. O Gateway encapsula sistemas externos, o Mapper traduz modelos com fidelidade semântica, e a Factory garante criação coerente. Com eventos de domínio e outbox, evitamos inconsistências e preservamos atomicidade.

Fowler nos mostrou o padrão, Evans nos deu a estratégia com a ACL, e Uncle Bob nos lembrou que são apenas detalhes que orbitam o centro.

No próximo artigo, sairemos do ponto-a-ponto e entraremos na mensageria e integração, analisando como eventos e filas podem reforçar, ou desafiar, a disciplina arquitetural que temos construído.

Carregando publicação patrocinada...