Data Driven vs Domain Driven

Data Driven ou Domain Driven? Essa dúvida aparece em quase todo projeto novo, e a resposta errada costuma custar meses de trabalho.

Recentemente publiquei um vídeo no canal do balta.io no YouTube, onde criamos uma API com ASP.NET Core 3 e EF Core 3 em menos de 15 minutos.

Claro que é apenas uma demo, e ainda faltam recursos na API, como versionamento, cache, compressão e documentação. Mas, se levarmos em conta que no fim temos um cadastro de produtos com categorias em menos de 15 minutos, é algo para se pensar.

Data Driven ou Domain Driven: o que está em jogo

Antes de escolher entre Data Driven ou Domain Driven, vale entender que a decisão não é sobre gosto pessoal. Na prática, ela define onde o time vai gastar tempo e esforço.

Por um lado, o modelo orientado a dados prioriza velocidade de entrega em telas e cadastros simples. Por outro lado, o modelo orientado ao domínio prioriza a correção das regras de negócio ao longo do tempo.

O guia de arquitetura de microsserviços da Microsoft resume bem esse dilema. Segundo ele, aplicar padrões avançados de DDD em serviços simples vira excesso de engenharia, enquanto tratar regras complexas como CRUD gera código de baixa qualidade.

Portanto, a pergunta certa não é "qual é o melhor modelo?". A pergunta certa é "qual é o modelo certo para esta parte do sistema?".

Data Driven

Em alguns vídeos sobre testes de unidade, eu comento sobre o risco de "perder tempo" testando a coisa errada. Por exemplo, testar tamanho de campo e outros itens que não são regras de negócio.

Se você gastar energia nisso, terá BEM MENOS tempo para testar o que realmente importa.

Isso é um fato, e algo que assombra muitos projetos por onde passo. O pessoal diz não ter tempo para testar, mas na verdade dedica esforço no lugar errado.

Inclusive, escrevi sobre isso no artigo eu não tenho tempo para testar, que complementa bem esta discussão.

Quando a dúvida é Data Driven ou Domain Driven e não existe regra de negócio, talvez a melhor escolha seja o modelo Data Driven. Ele nada mais é do que um "espelho" do banco de dados, ou seja, o famoso CRUD.

Menos regras, menos esforço no domínio

Se não há regras, não precisamos de tantos testes nem de tanto esforço no domínio. Então, não faz sentido implementar classes, herança, interfaces e outros recursos de OOP, SOLID e Clean Code em um cadastro simples.

Pense, por exemplo, em um cadastro de categoria, de unidade de medida ou de estado civil. Em todos eles, o trabalho se resume a gravar, listar, editar e excluir registros.

Seja simples e objetivo no seu código, porque nem tudo precisa de Domain Driven. Se for partir para o modelo Data Driven, minha recomendação é a do vídeo no começo deste artigo.

Ou seja, crie os modelos, adicione os Data Annotations e mande bala! Depois, gere as migrações, publique e pronto, API no ar!

Como fica um cadastro Data Driven no código

Um modelo Data Driven costuma ser apenas uma classe com propriedades e validações declarativas. Veja um exemplo de categoria no estilo do vídeo.

public class Category
{
    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "This field is required")]
    [MaxLength(60, ErrorMessage = "This field must have up to 60 characters")]
    public string Title { get; set; }
}

Em seguida, o controller conversa direto com o DbContext do Entity Framework Core, sem camadas intermediárias.

[HttpPost]
public async Task<ActionResult<Category>> Post(
    [FromServices] DataContext context,
    [FromBody] Category model)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    context.Categories.Add(model);
    await context.SaveChangesAsync();
    return model;
}

Repare que não existe regra de negócio aqui, por isso o modelo Data Driven resolve o problema sem esforço extra. As únicas validações são de formato, e o próprio ASP.NET Core cuida delas por meio dos atributos.

Se quiser ver esse estilo aplicado passo a passo, confira a série sobre APIs Data Driven com ASP.NET Core 3 e EF Core 3.

Domain Driven

Agora vamos ao famoso DDD, onde você vai usar tudo o que aprendeu sobre OOP, SOLID, Clean Code, padrões e muito mais.

O Domain Driven funciona quando temos regras de negócio, ou seja, quando temos domínios mais ricos que um cadastro Data Driven. Aplicá-lo em projetos sem esses requisitos é suicídio.

Imagine, por exemplo, um projeto que precisa calcular impostos, algo que não pode errar. Com certeza é um cenário para trabalhar bem no domínio, nas entidades e nos testes de unidade.

A própria Microsoft reforça essa ideia no seu guia. Lá, a recomendação é aplicar DDD apenas em microsserviços complexos, com regras de negócio significativas, e tratar responsabilidades simples, como um CRUD, de forma simples.

Tamanho do projeto não define DDD

Muita gente confunde o uso do DDD com o tamanho do projeto. Às vezes o projeto é grande, mas todo orientado a dados, sem regras complexas de negócio.

Nesse caso, o tamanho sozinho não justifica o DDD. Então, é a complexidade do domínio que define a escolha entre Data Driven ou Domain Driven.

Um sistema com cem telas de cadastro simples continua sendo um sistema Data Driven. Por outro lado, um módulo pequeno de cálculo fiscal pode exigir toda a disciplina do Domain Driven.

Como fica uma entidade Domain Driven no código

Em um modelo Domain Driven, a entidade deixa de ser só um saco de propriedades. Em vez disso, ela protege as próprias regras e só aceita mudanças válidas.

public class Invoice
{
    private readonly List<InvoiceItem> _items = new List<InvoiceItem>();

    public Invoice(decimal taxRate)
    {
        // a alíquota precisa estar entre 0% e 100%
        if (taxRate < 0 || taxRate > 1)
            throw new ArgumentOutOfRangeException(nameof(taxRate));

        TaxRate = taxRate;
    }

    public decimal TaxRate { get; private set; }
    public IReadOnlyCollection<InvoiceItem> Items => _items;

    public void AddItem(string description, decimal price, int quantity)
    {
        // regra de negócio: não existe item com quantidade ou preço inválidos
        if (quantity <= 0 || price <= 0)
            throw new InvalidOperationException("Invalid item");

        _items.Add(new InvoiceItem(description, price, quantity));
    }

    public decimal Subtotal() => _items.Sum(x => x.Price * x.Quantity);

    public decimal Tax() => Math.Round(Subtotal() * TaxRate, 2);

    public decimal Total() => Subtotal() + Tax();
}

public class InvoiceItem
{
    public InvoiceItem(string description, decimal price, int quantity)
    {
        Description = description;
        Price = price;
        Quantity = quantity;
    }

    public string Description { get; private set; }
    public decimal Price { get; private set; }
    public int Quantity { get; private set; }
}

Note que os setters são privados e que o cálculo do imposto mora dentro da entidade. Assim, nenhuma parte da aplicação consegue montar uma nota inválida por acidente.

Como a regra fica isolada, testar o cálculo também fica simples e rápido.

public class InvoiceTests
{
    [Fact]
    public void Should_calculate_tax_over_subtotal()
    {
        var invoice = new Invoice(0.1m);
        invoice.AddItem("Course", 100m, 2);

        Assert.Equal(20m, invoice.Tax());
        Assert.Equal(220m, invoice.Total());
    }
}

Esse é o tipo de teste que vale ouro. Para começar a escrever testes assim, veja os fundamentos dos testes de unidade.

Data Driven ou Domain Driven: por que não os dois juntos?

Em diversos projetos por onde passo, a primeira coisa que fazemos é analisar o contexto e o domínio da aplicação. Na maioria dos cenários, mais de 50% da aplicação é apenas CRUD, ou seja, cadastros básicos.

Em um dos últimos projetos de que participei, chegamos a uma conclusão no fim da análise. Nada menos que 72% da aplicação eram cadastros básicos, como os que citei, de categoria, unidade de medida e por aí vai, sem regras de negócio.

Gerando os cadastros com T4 Templates

O que fizemos? Em um dia e meio, geramos todos os CRUDs via T4 Templates, já com as views em um projeto separado.

Ou seja, em um dia e meio tínhamos mais de 70% do projeto concluído.

O T4 é o mecanismo de templates de texto do Visual Studio. De acordo com a documentação oficial do T4, ele gera código a partir de um modelo, que pode ser um arquivo ou até um banco de dados.

Esse encaixe é perfeito para o lado Data Driven, enquanto o Domain Driven fica reservado para as regras. Afinal, se os cadastros são espelhos das tabelas, a própria estrutura do banco serve como modelo para gerar o código.

Além disso, a Microsoft recomenda gerar classes parciais nesses cenários. Dessa forma, o time consegue complementar o código gerado sem perder as alterações na próxima geração.

Foco total onde o domínio importa

Com esse andamento, ainda sobraram três meses de projeto. Então, o time pôde focar em DDD, SOLID e Clean Code, criar testes de unidade e trabalhar com calma no que realmente importava.

Esse é o grande ganho de escolher entre Data Driven ou Domain Driven conforme a necessidade de cada parte. O esforço sai dos cadastros repetitivos e vai para as regras que geram valor.

O guia da Microsoft mostra a mesma estratégia na aplicação de referência eShopOnContainers. Nela, os serviços de catálogo, carrinho e perfil seguem como CRUD, enquanto o serviço de pedidos aplica padrões de DDD.

Inclusive, o guia afirma que um modelo anêmico não é antipadrão em um serviço CRUD. Ele só vira problema quando o domínio tem muitas regras de negócio em constante mudança.

Como decidir entre Data Driven ou Domain Driven em cada módulo

Para tomar a decisão módulo a módulo, algumas perguntas ajudam bastante.

  • O módulo tem regras que mudam com frequência ou dependem de especialistas do negócio?
  • Um erro nesse módulo gera prejuízo financeiro, fiscal ou jurídico?
  • As operações vão além de gravar, listar, editar e excluir registros?
  • Existem invariantes, ou seja, condições que nunca podem ser violadas?

Se a maioria das respostas for "não", o caminho Data Driven tende a ser suficiente.

Por outro lado, se a maioria for "sim", vale investir no Domain Driven.

Também é possível começar simples e evoluir. Quando um cadastro passa a ganhar regras, você migra apenas aquele módulo para um modelo de domínio mais rico.

Data Driven ou Domain Driven: diferenças lado a lado

Para facilitar a comparação, a tabela abaixo resume como Data Driven ou Domain Driven se comportam nos pontos que mais pesam no dia a dia.

Critério Data Driven Domain Driven
Ponto de partida Estrutura das tabelas Regras e linguagem do negócio
Modelo Classes com propriedades e Data Annotations Entidades ricas, com comportamento
Validação Formato dos campos Regras e invariantes do negócio
Testes Poucos, focados em integração Muitos testes de unidade no domínio
Velocidade inicial Muito alta Menor no começo
Custo de manutenção Cresce se surgirem regras Menor quando as regras mudam muito

Repare que nenhuma coluna é melhor em tudo. Na prática, o Data Driven ganha em velocidade, enquanto o Domain Driven ganha em segurança quando o negócio é complexo.

Também vale notar a linha de testes. Com Data Driven, os testes de integração dos endpoints costumam bastar, porque não há lógica para isolar.

Já com Domain Driven, as regras ficam em entidades sem dependência de banco. Por isso, os testes de unidade rodam rápido e cobrem exatamente o que importa.

Erros comuns ao escolher entre Data Driven ou Domain Driven

O primeiro erro é decidir entre Data Driven ou Domain Driven uma única vez, para o sistema inteiro. Como vimos, cada módulo pode e deve seguir o modelo que combina com a sua complexidade.

O segundo erro é aplicar todos os padrões do DDD em cadastros simples. Repositórios, agregados e eventos de domínio em um cadastro de estado civil só aumentam o código sem trazer valor.

O terceiro erro é o oposto: tratar regras críticas como um CRUD qualquer. Nesse cenário, a lógica se espalha por controllers e serviços, e cada mudança vira um risco.

Por fim, muitos times ignoram que o modelo pode evoluir. Começar com Data Driven não impede migrar um módulo para Domain Driven quando as regras aparecem.

Data Driven ou Domain Driven no acesso a dados

Essa mesma lógica de "usar o melhor dos dois" aparece no acesso a dados. Por exemplo, fazemos isso ao combinar Dapper e EF Core no mesmo projeto.

Assim como na escolha entre Data Driven ou Domain Driven, cada ferramenta tem o seu lugar.

Em telas de leitura pesada, o Dapper entrega performance com SQL direto. Já nos cadastros, o EF Core entrega produtividade com pouco código.

Se quiser se aprofundar nessa escolha, leia o artigo sobre Dapper e Entity Framework Core.

Da mesma forma, no lado do domínio, recursos como value objects ajudam a deixar o modelo mais expressivo. Este artigo mostra como mapear value objects no EF Core.

Conclusão: Data Driven ou Domain Driven sem bala de prata

One size doesn't fit all, ou seja, um tamanho não serve para todos. Assim como o Data Driven não resolve tudo, o DDD também não.

E por que não usar o melhor dos dois? A escolha entre Data Driven ou Domain Driven pode, e deve, variar de um módulo para outro dentro do mesmo sistema.

A estratégia certa na criação do projeto é o que define o sucesso dele. Pensar fora da caixa e entender que não existe bala de prata são atitudes fundamentais para garantir entregas concisas e de valor.

Para aprofundar os princípios que sustentam o lado Domain Driven, recomendo também os artigos sobre SOLID e Clean Code.

FAQ

O que é o modelo Data Driven?

É uma abordagem em que o código espelha a estrutura do banco de dados. Ela funciona bem em cadastros simples, sem regras de negócio relevantes.

O que é Domain Driven Design?

É uma abordagem que modela o software a partir das regras e da linguagem do negócio. Ela compensa quando o domínio é complexo e muda com frequência.

Projeto grande precisa de DDD?

Não necessariamente. O que justifica o DDD é a complexidade das regras de negócio, e não a quantidade de telas ou de tabelas.

Posso usar Data Driven e DDD no mesmo sistema?

Sim, e isso é bastante comum. Os cadastros simples seguem como CRUD, enquanto os módulos com regras críticas recebem um modelo de domínio rico.

Modelo anêmico é sempre um erro?

Não. Em serviços CRUD, o modelo anêmico é aceitável, e ele só vira problema quando há muitas regras de negócio.