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

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
- 1.1 Transaction Script: do procedural ao domínio rico
- 1.2 Table Module: tabelas falam mais alto que objetos?
- 1.3 Domain Model: o coração da arquitetura em três vozes
- 1.4 Service Layer: orquestrando casos de uso
Parte 2 – Persistência e Infraestrutura
- 2.1 Active Record: simplicidade que pode custar caro
- 2.2 Data Mapper: separando domínio e banco de dados
- 2.3 Unit of Work: coordenando mudanças no domínio
- 2.4 Identity Map & Lazy Load: truques de performance e consistência
Parte 3 – Apresentação e Integração
- 3.1 MVC e Front Controller: a porta de entrada
- 3.2 Template View e Page Controller: quando a UI dita o jogo
- 3.3 Gateways e Mappers: defendendo o domínio
- 3.4 Mensageria e integração: eventos em três perspectivas
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:
- Isolamento semântico: o domínio não fala em endpoints ou queries, mas em métodos significativos.
- Evolução controlada: se a API externa mudar, só o Gateway precisa ser ajustado.
- 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.