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

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

Capa

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

Parte 2 – Persistência e Infraestrutura

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


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.

Carregando publicação patrocinada...
4

Muito bom o conteúdo, obrigado. Sou front end mas no serviço peguei umas coisas em back Java com Spring Boot e estava separado nessa arquitetura de entity, repositorys, com mapper. No começo demorei pra me achar kkk , e agora sabendo os motivos desta arquitetura fica até mais fácil de entender, clareou as minhas ideias, obrigado amigo.

2

Obrigado pelo comentário Rafa, o próximo artigo sobre unit of work ajuda a fechar o ciclo de persistência. Eu recomendo ler em seguida. Durante a semana vou tentar fechar esta parte 2 com o último capítulo.

Abraço