Always valid vs not always valid domain model

I’m back to the regular posting schedule. No more game development, at least for now.

Always valid domain models

The topic of always valid domain models is beaten to death already. There’s little doubt in the DDD camp that your domain model should be valid at all times. It’s much easier to program in an environment where you know that objects you operate reside in a valid state and you don’t need to worry about their internal consistency.

It also removes a lot of issues related to the human factor. When you have to keep in mind to always call the IsValid method before doing something meaningful with an entity, you are asking for a trouble. It’s too easy to omit such a call.

Always valid models help avoid unnecessary confusion too. Consider the following example:

public class Company
{
    public void AssignDelivery(Delivery delivery)
    {
        if (!delivery.IsValid())
            throw new Exception();

        _deliveries.Add(delivery);
    }

    public void PostponeDelivery(Delivery delivery)
    {
        if (_deliveries.Contains(delivery))
        {
            _deliveries.Remove(delivery);
        }
    }
}

Is it intentional that we validate the delivery entity in the first method but not in the second? Who knows. Maybe we don’t need it to be valid in order to postpone one’s delivery. In this case, the simple Contains check would suffice. Or maybe it’s a bug and the author just forgot to add this validation. You never know until you dig deeper. And even if you prove this code is correct, the requirements may change, and it would be really hard to track all cases that didn’t require a valid delivery object but now do.

So, it’s easier to keep your domain model valid at all times. And for that, you need to do the following:

  • Make the boundary of your domain model explicit.

  • Validate all external requests before they cross that boundary.

The first point means you should be clear which classes in your code base represent the domain model and which not. The second - that you need to perform all required validations upfront, before you delegate the work to domain classes.

Here’s a diagram that depicts this process:

Validation
Validation

In my experience, this whole dichotomy of always-valid and not-always-valid is actually an issue with the single responsibility principle.

As Greg Young pointed out in his old post, there are two types of validity: invariants and client input validation. People who argue in favor of the domain model being not always valid just try to make the domain classes both model the domain and hold data coming from the client input.

Indeed, when you have such code:

public class Company
{
    [Required]
    [MaxLength(200)]
    public string Name { get; set; }

    [Phone]
    public string Phone { get; set; }

    public void AssignDelivery(Delivery delivery)
    {
        /* ... */
    }

    public void PostponeDelivery(Delivery delivery)
    {
        /* ... */
    }
}

it’s hard to see how else you can validate the company’s name and phone number if not allowing them to enter a potentially invalid state first.

There’s a better alternative to this solution, though. You need to stop using the domain model to carry data that come from the outside world. Create a DTO instead and pass the data further to the Company domain class only after the validation is completed:

public class CompanyDto
{
    [Required]
    [MaxLength(200)]
    public string Name { get; set; }

    [Phone]
    public string Phone { get; set; }
}

public class Company
{
    public void AssignDelivery(Delivery delivery)
    {
        /* ... */
    }

    public void PostponeDelivery(Delivery delivery)
    {
        /* ... */
    }
}

Alternatively - and that’s the approach I usually take - you can create Value Objects for notions of Name and Phone and put the validation logic there in a form of factory methods:

public class Phone : ValueObject<Phone>
{
    private string _phone;

    private Phone(string phone)
    {
        _phone = phone;
    }

    public static Result<Phone> Create(string phone)
    {
        if (string.IsNullOrWhiteSpace(phone))
            return Result.Fail<Phone>(phone);

        return Result.Ok(new Phone(phone));
    }
}

You still need the DTO but the validation logic is now located in domain model itself. Which is a preferable place for such logic.

So, again, separation of concerns and adherence to SRP makes wonders. Make the domain model a safe harbor, protect it from the external influence, and maintain its invariants.

What if invariants change?

But what if invariants change? Let’s say that the business people come to you and say that they now work with US companies exclusively and you need to reject companies with phone numbers outside the "+1" country region. And that they also want you to add a new mandatory field - NumberOfEmployees.

Now, all of the sudden, each company you have in your database has become invalid. That’s because you’ve strengthened the preconditions for the Company entity and thus broken its backward compatibility with the existing data.

So how to handle this situation? If you merely update the validation rules associated with the Phone value object, you will have an exception every time you try to work with an already existing company.

Does it mean you need to allow the domain model to become invalid? After all, how else you will be able to work with those companies?

One could argue that it breaks the whole always-valid paradigm but in reality, it is not. Strengthening an existing precondition like we did in the above example is a clear business case and should be treated as such. Meaning that you cannot simply outlaw all existing companies, you need to come up with a transition plan.

You, as a developer, need to work with the domain experts on how to make this happen. There’s no universal solution, each transition should be evaluated on a case-by-case basis.

So for the number of employees, maybe it would be enough to require this field for new companies. And for old, ask them to fill it out only when they make a change to their profile. Eventually, you will have this precondition consistent with all your data but there’s no way to impose it momentarily. Unless you are willing to purge your existing client base of course.

As for the US companies requirement, maybe you will send out a letter to those of them who reside outside the country and propose a migration plan. After some period of time, you will be able to remove them from your database. Or maybe not. In which case you will need to come up with a mechanism to distinguish between US and non-US companies in your domain model.

In any case, you can’t just strengthen the requirements, you have to come up with a transition plan and express this plan explicitly in your domain model.

Summary

  • Your domain model should be valid at all times.

  • For that, do the following:

    • Make the boundary of your domain model explicit.

    • Validate all external requests before they cross that boundary.

  • Don’t use domain classes to carry data that come from the outside world. Use DTOs for that purpose.

  • You cannot strengthen the invariants in your domain model as it would break backward compatibility with the existing data. You need to come up with a transition plan.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus