Always-Valid Domain Model

1. Not-always-valid domain model

It may seem strange to write a response to an 11 years old article, but the concepts it talks about are timeless and still relevant today. Moreover, I still see people asking the same questions raised in that article and coming to the same conclusions, which in my opinion are incorrect. I also haven’t seen a complete rebuttal anywhere (Greg’s rebuttal is great but not complete), so I decided to write my own.

  1. Validation rules are context-dependent and can’t be equally applied across all use cases.
  2. The message returned during the validation is the responsibility of the presentation layer and should not reside in the domain layer.
  3. Existing validation rules may not apply to historical data.

2. Validation rules are context-dependent

This is the point Greg responds to in his article. The crux of his response is that:

  • There are invariants attached to each domain class — conditions that must be held true at all times. These invariants define the domain class: that class is what it is because of them. Therefore, you can’t possibly violate these invariants. If you do so, the domain class would simply cease being the thing you expect it to be; it’d become something else.
  • Reasoning about the domain model using invariants helps with the DRY principle. When there are multiple operations doing the same check, you have to be diligent to not miss it in any of those operations. Making the domain class itself responsible for that check absolves you of this additional mental burden and ensures that the check is always there.

2.1. Validations vs invariants

But there’s also something that I think both Jeffrey and Greg are incorrect about: the difference between validations and invariants.

Business rules manifest themselves as invariants in the domain model and as input validation in application services.
Validation vs invariant violation
// Controller
public string UpdateStudent(int studentId, string name)
{
if (string.IsNullOrWhiteSpace(name)
return "Name cannot be empty";

Student student = GetStudent(studentId);
student.UpdateName(name);

return "OK";
}

// Domain layer
public class Student
{
public void UpdateName(string name)
{
if (name == null)
throw new ArgumentNullException();

Name = name;
}
}

2.2. Simple vs complex validations

I’ll throw another misconception into the mix while I’m at it (it wasn’t part of the original debate between Jeffrey and Greg).

2.3. Context is part of the business rules

Now, back to the first point that validation rules are context-dependent.

// StudentController
public string Enroll(int studentId, int courseId)
{
Student student = GetStudent(studentId);
Course course = GetCourse(courseId);

Result result = student.CanEnrollIn(course); '1
if (result.IsFailure)
return result.Error;

student.EnrollIn(course);
SaveStudent(student);

return "OK";
}

// Student
public Result CanEnrollIn(Course course)
{
if (Status != StudentStatus.Active)
return Result.Failure("Inactive students can't enroll in new courses");

return Result.Success();
}

public void EnrollIn(Course course)
{
Result canEnrollResult = CanEnrollIn(course); '2
if (canEnrollResult.IsFailure)
throw new InvalidOperationException(canEnrollResult.Error);

/* Enrollment logic */
}
  • Provides the API for the application layer to use this knowledge for input validation — The controller uses the CanEnrollIn method (line '1) to filter incoming data.
  • Uses the same API for invariant check — The Student protects that invariant in line '2 by throwing an exception if enrollment is impossible.
The Student exposes the domain knowledge via the CanEnroll API. This API is then used by both the controller (to conduct input validation) and the Student itself (to protect its invariants).
public class Person
{
public PersonType Type { get; }

public void EnrollIn(Course course)
{
// Guard clause
if (Type != PersonType.Student)
throw new InvalidOperationException();

/* Enrollment logic */
}

public void Start(Course course)
{
// Guard clause
if (Type != PersonType.Instructor)
throw new InvalidOperationException();

/* Starting logic */
}
}

public enum PersonType
{
Student,
Instructor
}
public class Person
{
}

public class Student : Person
{
public void EnrollIn(Course course)
{
/* Enrollment logic */
}
}

public class Instructor : Person
{
public void Start(Course course)
{
/* Starting logic */
}
}

2.4. Data contracts vs domain model

I believe that the whole idea to make your domain model able to enter an invalid state stems from the conflation of data contracts (DTOs) and domain classes.

3. The text message is the responsibility of the presentation layer

Let’s move on to the next point: the message returned during the validation is the responsibility of the presentation layer and should not reside in the domain layer.

// Student domain class
public Result CanEnrollIn(Course course)
{
if (Status != StudentStatus.Active)
return Result.Failure("Inactive students can't enroll in new courses");

return Result.Success();
}
// Student domain class
public UnitResult<Error> CanEnrollIn(Course course)
{
if (Status != StudentStatus.Active)
return Errors.InactiveStudentsCannotEnrollInNewCourses();

return Result.Success();
}

4. Validating historical data

Finally, we have the last point: existing validation rules may not apply to historical data.

  • Remove all existing data that doesn’t comply with the new rules.
  • Make the Student class handle both the old and the new sets of rules
  • Create a separate Student class for the new set of rules.

5. Dealing with huge UI forms

And the last point for today. Sometimes, you need to deal with large UI forms containing 10, 20 or even more fields. What if you want the users to be able to save the form half-way through and continue later? Doesn’t it mean the entity this form relates to will reside in an invalid state (assuming that all fields are mandatory for that entity)?

6. Summary

  • Domain classes should always guard themselves against becoming invalid.
  • Invariants define the domain class: that class is what it is because of them.
  • Input validation and domain invariants stem from the same business rules. The difference between the two is a matter of perspective:
  • - Business rules manifest themselves as invariants in the domain model.
  • - Business rules manifest themselves as input validation in application services.
  • Invariant violation is an exceptional situation; use exceptions.
  • Invalid input is not an exceptional situation; use a Result class.
  • There’s no difference between “data validation” and “business rules validation”. All validations, not matter how simple, are a result of business requirements.
  • The validation context is part of the domain knowledge and should reside in the domain model:
  • - The domain model should provide an API for the application layer to use this knowledge for input validation.
  • - The domain model should use the same API for invariant checks.
  • To remove text messages from the domain layer, use error codes instead of text and convert those code to text in the presentation layer.
  • To strengthen validation requirements, create a separate class that would handle the new set of rules. Once all existing data adheres to the new rules, remove the old class.
  • To deal with large UI forms that you need to save before submitting, create a separate entity with less strict validation rules.

Subscribe

Subscribe to read more articles like this: https://enterprisecraftsmanship.com/subscribe

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store