Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 2.2 - Data Mapper: separando domínio e banco de dados

Na primeira parte desta série exploramos como o domínio pode ser modelado, protegido e orquestrado. Mas todo domínio, por mais elegante que seja, precisa encarar uma realidade inescapável: os dados precisam ser persistidos. Não basta calcular, validar ou expressar regras de negócio; é preciso garantir que esse estado sobreviva ao tempo. É nesse momento que a arquitetura encontra a infraestrutura, e que padrões se tornam decisivos para equilibrar clareza do modelo e eficiência técnica.
Martin Fowler dedicou uma boa parte do seu catálogo de Patterns of Enterprise Application Architecture justamente a esse tema: Active Record, Data Mapper, Unit of Work, Identity Map, Lazy Load. Cada um deles enfrenta dilemas recorrentes: como salvar e recuperar o estado do domínio sem deixá-lo refém do banco de dados? Como coordenar mudanças de forma transacional? Como manter performance sem sacrificar consistência?
Eric Evans, no Domain-Driven Design, sempre reforçou que a persistência deve ser invisível ao domínio. Para ele, o modelo precisa refletir a linguagem do negócio, não o schema do banco. Repositórios, Application Services e agregados só cumprem bem seu papel quando o banco fica em segundo plano, um detalhe de implementação.
Robert C. Martin, em Clean Architecture, é ainda mais radical: o banco de dados é um detalhe de infraestrutura, nunca o centro da arquitetura. Ele insiste que o domínio e os casos de uso não devem ter dependência alguma de frameworks ou tecnologias de armazenamento. O dado pode estar em SQL, NoSQL, arquivo em disco ou até mesmo em memória; o que importa é que o domínio continue protegido.
Nesta segunda parte da série, vamos olhar para os padrões de persistência com esses três óculos: Fowler, Evans e Uncle Bob. Vamos ver onde eles brilham, onde tropeçam e que escolhas arquiteturais nos forçam a enfrentar. E, como sempre, vamos usar exemplos em C# para tornar o debate mais concreto.
Í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
Data Mapper: separando domínio e banco de dados
Introdução
No artigo anterior vimos o fascínio do Active Record: rápido, direto e simples, mas perigoso quando o domínio precisa amadurecer. Foi justamente para resolver essa tensão que Martin Fowler descreveu o Data Mapper em Patterns of Enterprise Application Architecture. Sua proposta é clara: separar o modelo de domínio da lógica de persistência, mantendo cada um no seu lugar.
Enquanto o Active Record mistura dados, regras e SQL em uma única classe, o Data Mapper coloca a persistência em um componente dedicado, que traduz entre objetos de domínio e tabelas de banco. Assim, o domínio continua limpo, expressivo, protegido das mudanças tecnológicas.
Martin Fowler, além do Data Mapper, também definiu o padrão Repository em seu catálogo. O Data Mapper resolve a tradução objeto ↔ banco. O Repository, por sua vez, oferece uma interface de coleção para manipular entidades, escondendo os mapeadores por baixo. Eric Evans, no Domain-Driven Design, adota esse mesmo padrão, mas lhe dá um papel mais tático: em vez de ser apenas uma abstração técnica, o Repository passa a ser parte da linguagem do domínio, sempre vinculado a uma raiz de agregado.
Robert C. Martin vê no Data Mapper e no Repository aliados naturais da Clean Architecture. Afinal, ambos permitem que o núcleo da aplicação permaneça isolado, enquanto a persistência é tratada como detalhe plugável.
Essa pureza tem custo: mais código, mais disciplina, mais complexidade inicial. Mas é um preço que se paga para garantir um modelo de domínio duradouro.
Exemplo prático em C#
Vamos reescrever o exemplo de Customer, agora no estilo Data Mapper + Repository.
// Domain entity (pura, sem dependência de infraestrutura)
public class Customer
{
public CustomerId Id { get; }
public string Name { get; private set; }
public Customer(CustomerId id, string name)
{
Id = id;
Rename(name);
}
public void Rename(string newName)
{
if (string.IsNullOrWhiteSpace(newName))
throw new ArgumentException("Name cannot be empty.");
Name = newName;
}
}
// Value Object para identidade
public readonly record struct CustomerId(Guid Value);
// Contrato do repositório
public interface ICustomerRepository
{
Customer? GetById(CustomerId id);
void Add(Customer customer);
void Update(Customer customer);
}
// Implementação com Data Mapper (SQL direto, simplificado)
public class SqlCustomerRepository : ICustomerRepository
{
private readonly string _connectionString;
public SqlCustomerRepository(string connectionString)
{
_connectionString = connectionString;
}
public Customer? GetById(CustomerId id)
{
using var conn = new SqlConnection(_connectionString);
conn.Open();
using var cmd = new SqlCommand(
"SELECT Id, Name FROM Customers WHERE Id = @Id", conn);
cmd.Parameters.AddWithValue("@Id", id.Value);
using var reader = cmd.ExecuteReader();
if (reader.Read())
{
return new Customer(
new CustomerId(reader.GetGuid(0)),
reader.GetString(1));
}
return null;
}
public void Add(Customer customer)
{
using var conn = new SqlConnection(_connectionString);
conn.Open();
using var cmd = new SqlCommand(
"INSERT INTO Customers (Id, Name) VALUES (@Id, @Name)", conn);
cmd.Parameters.AddWithValue("@Id", customer.Id.Value);
cmd.Parameters.AddWithValue("@Name", customer.Name);
cmd.ExecuteNonQuery();
}
public void Update(Customer customer)
{
using var conn = new SqlConnection(_connectionString);
conn.Open();
using var cmd = new SqlCommand(
"UPDATE Customers SET Name = @Name WHERE Id = @Id", conn);
cmd.Parameters.AddWithValue("@Id", customer.Id.Value);
cmd.Parameters.AddWithValue("@Name", customer.Name);
cmd.ExecuteNonQuery();
}
}
Repare:
– Customer é puro, voltado ao negócio.
– SqlCustomerRepository traduz entre objetos e banco.
– O contrato ICustomerRepository vive no domínio; a implementação é detalhe de infraestrutura.
– Testar regras de domínio não exige banco: basta instanciar Customer.
– Substituir o armazenamento (SQL, NoSQL, memória) não toca no domínio, apenas na infraestrutura.
E o Entity Framework Core?
Aqui vale desfazer um mito comum: Entity Framework Core não é Active Record.
Ele implementa os padrões Unit of Work e Data Mapper, e fornece gateways (DbSet<TEntity>) que se parecem com repositórios, mas não são repositórios no sentido de Fowler ou Evans.
– O DbContext é um Unit of Work: rastreia alterações e aplica em lote com SaveChanges().
– O DbSet<TEntity> é um gateway de infraestrutura: expõe operações de consulta/manipulação, mas não fala a linguagem do domínio nem respeita fronteiras de agregados.
– O verdadeiro Repository de DDD deve ser definido no domínio como interface (ICustomerRepository), expondo apenas operações coerentes com a raiz agregada (GetById, Add, Update, etc.).
– A implementação concreta desse repositório pode, sim, usar DbSet internamente, mas o domínio nunca deve conhecê-lo diretamente.
// No domínio
public interface ICustomerRepository
{
Customer? GetById(CustomerId id);
void Add(Customer customer);
void Update(Customer customer);
}
// Na infraestrutura (usando EF Core)
public class EfCustomerRepository : ICustomerRepository
{
private readonly AppDbContext _context;
public EfCustomerRepository(AppDbContext context)
{
_context = context;
}
public Customer? GetById(CustomerId id)
=> _context.Customers.SingleOrDefault(c => c.Id == id);
public void Add(Customer customer)
=> _context.Customers.Add(customer);
public void Update(Customer customer)
=> _context.Customers.Update(customer);
}
// DbContext representando Unit of Work
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; } = null!;
}
Esse arranjo mantém a Clean Architecture:
– O domínio conhece apenas ICustomerRepository.
– A infraestrutura conhece EF Core.
– O DbSet é detalhe interno, nunca contrato de domínio.
O preço da disciplina
O Data Mapper resolve a fragilidade do Active Record, mas exige mais rigor. Fowler alerta: em sistemas simples, o custo pode não compensar. Mas em sistemas estratégicos, com regras complexas e vida longa, é um investimento vital.
Evans mostra que, com Repositories, o código fica mais expressivo: manipulamos coleções como se fossem memória, mas com persistência por baixo. Uncle Bob reforça: se isso mantém o núcleo livre de dependências externas, vale cada linha de código extra.
Conclusão
O Data Mapper é um padrão de maturidade. Ele aceita pagar a conta da complexidade para preservar a independência do domínio. Fowler o descreveu como o contraponto ao Active Record; Evans refinou o conceito de Repository sobre essa base, alinhando-o à linguagem ubíqua e às raízes de agregado; Uncle Bob o abraçou como mecanismo para proteger os círculos internos da arquitetura.
No próximo artigo, veremos o padrão que normalmente caminha ao lado do Data Mapper: o Unit of Work. Ele resolve o problema da coordenação de mudanças, garantindo que múltiplas operações no domínio sejam persistidas de forma transacional e consistente. É a engrenagem que falta para fechar o ciclo entre um modelo rico e uma infraestrutura robusta.