Since K2, the Kladde's house rules have said: business rules live in the model, not in Razor and not in the endpoint. In the last two lessons you built a model that knows its rules itself. In this lesson you see what happens when a rule isn't there, and how to get it there. You work on a small lab in which one rule stands in two places and contradicts itself. After that, the Kladde's quote gets a discount whose rule applies across several quotes. For that you get to know the last building block of tactical DDD, the domain service.

The anemic model

This is what a quote looks like in many applications:

C#
// Excerpt: not like this
public sealed class QuoteRecord
{
    public Guid Id { get; set; }

    public string Status { get; set; } = "Draft";

    public decimal DiscountPercent { get; set; }

    public List<QuoteLineRecord> Lines { get; set; } = [];
}

public sealed class QuoteService(KladdeDbContext db)
{
    public async Task SendAsync(Guid id)
    {
        QuoteRecord quote = await db.Quotes.SingleAsync(q => q.Id == id);
        if (quote.Status != "Approved")
        {
            throw new InvalidOperationException("Only approved quotes can be sent.");
        }

        quote.Status = "Sent";
        await db.SaveChangesAsync();
    }
}

The class QuoteRecord has data but no behavior. Every property has a public setter, every list can be changed from outside. The rules live elsewhere, here in the QuoteService. Martin Fowler calls something like this an anemic domain model: it looks like a model, but the life, the rules, flows elsewhere.

That alone wouldn't be a problem yet. The problem is that nothing enforces the rules. SendAsync checks the status, but any other place in the program can write quote.Status = "Sent" without checking. A Razor page that is supposed to show something quickly. An endpoint that someone added later. An import. And each of these places brings its own version of the rule, or none.

Is an anemic model always wrong? No. Microsoft's guide puts it clearly: for a context that mostly just creates, reads, updates and deletes (CRUD), it isn't an anti-pattern. The master data of a tender from TED has hardly any rules of its own, so a rich model there would be ceremony. It becomes an anti-pattern where there are many rules that change, that is, exactly where you use DDD. Whether a context needs that, you decided in module 4.2, and in lesson 4.4.1 you decide it once more for data storage.