Enhancing Core Domain Code with Simple Updates

Senior Software Developer with over 10 years of performance-focused .NET development experience. I assist developers in solving architectural challenges and simplifying complex software projects. Writing as Dmitry Dezuk to share everyday productivity tips for developing faster and more reliable software.
Let me illustrate ideas laid out in previous articles with examples and show the actual code following all the promoted principles. You will see how easy it is to build complex systems by adhering to simple rules.
Let’s pick up a domain. I chose specifically the one I have never worked in such as Insurance Management System. You are tasked with a system maintaining the business processes of an insurance company working with real estate. We will deal with hundreds of requirements but let’s start with the easiest ones such as customer registration. That simple act of registration usually includes many rules and requirements but I will list a few basic ones.
The customer should have a unique ID generated by the System that can be used to reference this customer in the URL or do a lookup when a support agent is trying to pull the customer’s record. There are some requirements for that ID. It is a string with a length of 10 that contains alphanumeric characters.
The customer should also have a valid email, First and Last Name, and Date of Birth
We can pick a dozen other attributes for instance Primary Address, Insured Property Address, Employer, Salary, etc but we will keep it simple for now.
The Naive approach following OOP is to create a Customer with read-only properties and a constructor like below.
public class Customer
{
public string ID { get; }
public string FirstName { get; }
public string LastName { get; }
public string Address { get; }
public string Email { get; }
public DateTime DateOfBirth { get; }
const uint REASONABLE_MAX_AGE = 120;
const float AVERAGE_DAYS_IN_A_YEAR = 365.2425f;
public Customer(string id, string firstName, string lastName, string address, string email, DateTime dateOfBirth)
{
this.ID = id ?? throw new ArgumentNullException(nameof(id));
this.FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
this.LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
this.Address = address ?? throw new ArgumentNullException(nameof(address));
this.Email = email ?? throw new ArgumentNullException(nameof(email));
var years = (DateTime.Now - dateOfBirth).TotalDays / AVERAGE_DAYS_IN_A_YEAR;
if(years<0 || years> REASONABLE_MAX_AGE)
{
throw new ArgumentException("Age is not reasonable");
}
this.DateOfBirth = dateOfBirth;
}
}
OOP is happy as the state is encapsulated, and the constructor gives some protection against invalid input. You cannot create an empty Customer or a customer without any of the required fields. But this is exactly where the snowball of possible issues emerges.
A good domain model would allow for interaction with a client in a legit way and prohibit any possible deviation from the modeled restrictions (aka invariants). What can we do here?
var weirdValidCustomer = new Customer(
"!special symbol not allowed",
" start with empty characters?",
"32 last name starts with a number",
"Is PO Box OK?",
"---I don't have one--",
DateTime.Today.AddYears(30).Add(TimeSpan.FromMinutes(20)));// we know the time of birth?
You can bog down such a system by passing a string from the full content of “War and Peace” by Tolstoy. Millions of invalid inputs and their combinations can be passed resulting in a combinatorial explosion of possible buggy states.
Light restrictions cause enormous possibilities for exploitation (by hackers), confusion, or wasted debugging hours. Entropy starts thriving in such conditions inevitably leading to code degradation and chaos.
What you should do is not go to another extreme and start adding a huge number of checks to the customer constructor. But that would still be better than not having any checks. Rather think of yourself as a sculptor, focus on one thing at a time, and cut all undefined, unnecessary, irrelevant. Do it piece by piece, step by step until you face a wall of the underlying requirements that you can’t go any further.
Being a sculptor when modeling the domain means creating new abstractions and delegating all the logic restricting our states (i.e. reducing our entropy) to those abstractions till you are left with very few states just necessary to implement the given use cases from a spec.
The reality is that the subset of valid unique IDs is much smaller when a manifold of all strings. The same goes for strings, names, and emails where the number of good instances of those strings is incomparably less than the number of all the strings the string type allows for. We need an abstraction expressing those valid subsets. We can create and name them accordingly without even going deeper into how they should properly function. We will get to them later when we zoom in our focus on them.
It is so easy just to create new classes where we pretend all rules are already enforced and this is a very efficient move to remove all uncertainty and proliferation of possible invalid states. Here we create new classes CustomerId, CustomerName, Address, Email, and DateOfBirth with all needed to maintain their invariants, i.e. rules defined by the spec explicitly or implicitly. The customer has very little left to enforce rules in its internal constituents. It does nothing but ensure all the required constituents are supplied. That makes it very robust and leaves no opportunity for incorrect instantiation of a Customer object.
public class CustomerId {…}
public class CustomerName {…}
public class HomeAddress {…}
public class Email {…}
public class DateOfBirth {…}
public class Customer
{
public CustomerID ID { get; }
public CustomerName Name { get; }
public HomeAddress Address { get; }
public Email Email { get; }
public DateOfBirth DateOfBirth { get; }
public Customer(CustomerID id, CustomerName name, Address address, Email email, DateOfBirth dateOfBirth)
{
this.ID = id ?? throw new ArgumentNullException(nameof(id));
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.Address = address ?? throw new ArgumentNullException(nameof(address));
this.Email = email ?? throw new ArgumentNullException(nameof(email));
this.DateOfBirth = dateOfBirth ?? throw new ArgumentNullException(nameof(email));
}
}
Our code became simpler, much simpler and it eliminates many more invalid states than the first version. It is just verifying the fact that the parameter is supplied and it knows if it is supplied it cannot be in an invalid state. That technique combats a well-known antipattern known as Primitive Obsession where Domain classes use language primitive types (string, int, DateTime, float, etc.) rather than specialized types representing Domain concepts and that makes it hard for domain objects to ensure the correctness of its parts at all times. Think of it. If you use email or address as properties in quite a few classes you will have to re-run the same guarding checks over and over. It violates the DRY (don’t repeat yourself) rule. Even if you extract the guarding logic and put it in helper methods, it would still be easy to make a mistake. If you assign a primitive type value to the properties in many places it is just a matter of time when you forget to use the helper method to confirm validity.
Specialized types build that fortress around your preserved state that makes your model always valid. There is one characteristic of such types that simplifies their implementation and utilization. That is immutability. If specialized type states never change from their creation, we can put all the heavy guarding logic in their constructor. There won’t be any other manipulators or methods in them as they are immutable by definition. If we need a change in their state, we just create a new object with the same constructor and swap the old object with the new one.
Such types are called “value objects” because they are defined by nothing but their values and they don’t have an identity (unique features outside of the set of values of their properties). You can replace one “value object” with another as long as it the have same values in their data frames.
One way of modeling value objects is to C# structs as they have some intrinsic properties of value objects. Structs are passed by value, they are compared by internal state by default. You won’t get NRE (NullReferenceException) using them so you don’t need that guarding check for Null. But the drawback is that the struct always has a parameterless constructor so you can always create a zero object and if it is not part of a valid subset, you will have to burden the caller with additional code checking against using zero value. I would prefer using a class if there is no performance penalty here or if I have the luxury of using .net core, “record” would be a better option to model value objects.



