About a year ago I attended a workshop held by Dan Bergh Johnsson and Daniel Deogun, authors of the book Secure By Design. They presented the topics of their book and on three additional occasions we met in smaller groups and had guided discussions on the topics.
I like the mix of Domain Driven Design and cybersecurity a lot, it gave me a whole new perspective (and justification!) on why to apply Domain Driven Design. One concept I have been using extensively since I read the book is Domain Primitives and I want to share how I utilize it.
Domain Primitives are described as the smallest building blocks in a domain model. A Domain Primitive is a small Value Object, a type that is primarily defined by its attributes rather than by an identity.
According to the authors nothing in a domain model should be represented by a primitive type (like int, string, double..). Everywhere you’d use a primitive type you replace it with a Domain Primitive. The primitive type then becomes the Domain Primitive’s only attribute.
Conceptually a Domain Primitive is a Value Object. The name Domain Primitive just makes the intention clear: to replace primitive types with Value Objects. I have found that having a name for this concept makes it easier to apply it consistently and to communicate the pattern to team members.
The first time I got the chance to apply this concept, I was working on an ASP.NET Core Web API that was meant to track projects through their lifecycle following a rather complicated process. The Project Entity might have looked something like this (simplified and generalized):
public class Project
{
public int Id { get; }
public ProjectStatus ProjectStatus { get;}
public string Name { get; }
public string Description { get; }
public Project(int id, ProjectStatus projectStatus, string name, string description)
{
Id = id < 0 ? throw new ArgumentOutOfRangeException(nameof(id)) : id;
ProjectStatus = projectStatus;
Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Name cannot be empty") : name;
Description = description;
}
}
Turning all the primitive types into Domain Primitives the code becomes:
public class Project(ProjectId id, ProjectStatus projectStatus, ProjectName name, Description description)
{
public ProjectId Id { get;} = id;
public ProjectStatus ProjectStatus { get;} = projectStatus;
public ProjectName Name { get; } = name;
public Description Description { get;} = description;
}
I would argue that the code is better readable, especially for people who don’t read code every day. Someone instantiating this class will no longer accidentally switch the name and the description, the Domain Primitives have introduced type safety to the constructor.
Notice how the validation for the Id and Name is gone. It was moved into the Domain Primitive. A Domain Primitive is responsible for its own validation. The validation should be done directly in the constructor, so that it is impossible to create an instance that is in an invalid state.
Let’s look at the ProjectName Domain Primitive (it’s the same concept for the Description). In its simplest form the code looks like this:
public record ProjectName
{
public string Value { get; }
public ProjectName(string value)
{
Value = string.IsNullOrWhiteSpace(value) ? throw new ArgumentException("Name cannot be empty") : value;
}
}
I really like that the file is so small now and that I can concentrate on it without being distracted by the surrounding code. Because of that I started thinking about the validation a lot more. There is probably a sensible maximum length for a ProjectName. And maybe we should not allow all characters? Setting sensible limits can contribute to defense against injection attacks (see OWASP A03 Injection), as the characters needed for such an attack are rejected by the validation.
After adding the missing validation the ProjectName now looks something like this:
public record ProjectName
{
public string Value { get; }
public ProjectName(string value)
{
var validationResult = Validate(value);
if (validationResult is ValidationResult.Failure failure)
throw new DomainValidationException(failure.Error);
Value = value;
}
public static ValidationResult Validate(string value)
{
if (string.IsNullOrWhiteSpace(value))
return new ValidationResult.Failure("Name cannot be empty");
if (value.Length > 100)
return new ValidationResult.Failure("Name cannot exceed 100 characters");
if (!IsValidCharacters(value))
return new ValidationResult.Failure("Invalid characters in Name, only letters, number and whitespace allowed");
return new ValidationResult.Success();
}
private static bool IsValidCharacters(string value) => value.All(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c));
}
The ValidationResult type looks like this:
public abstract record ValidationResult
{
public record Success : ValidationResult;
public record Failure(string Error) : ValidationResult;
}
The validation logic was extracted into a separate static method, so there is a way to shut the program down more gracefully, but also to reuse it and test it independently.
Note that you want to validate as soon as possible to stop malicious input, which means that syntactic validation should be done before any other of your code is executed.
In ASP.Net Core projects, I usually validate the Request Models with Attribute Validation and validate against the same rules as in the Domain Primitives. This leads to duplication, but is straight forward, easy to use and readable. The validation will also be shown in OpenAPI, which means that the frontend can reuse it.
In Android Compose, I reuse the validation for input validation. As soon as the user changes an input field, I validate it with the static validation method in the Domain Primitive.
The validation in the Domain Primitive serves as a safety net and canonical validation, but it is not a replacement for early syntactic input validation.
You generally always start with syntactic validations (format, length, allowed characters), then do semantic validations (business rules that require additional context or data to be loaded), as semantic validations can themselves be a cause of security issues. For example, if you need to check whether a username already exists in the database, performing this check on unsanitized input could expose you to SQL injection attacks.
Semantic checks that can be done in a Domain Primitive are usually static validations, like the IsValidCharacters check in the code above. If it is possible to solve the validation in another way, I avoid using RegularExpressions as they can be problematic.
When an invalid input reaches the constructor of a Domain Primitive it should already be valid, as it was possible to validate it earlier. So something has seriously gone wrong (or even tampered with) and I throw an exception (and log it) instead of for example returning a Result Wrapper.
Known advantages
The advantages that are listed in the book are that Domain Primitives increase security by immutability, failing fast and validation which makes it impossible to get to an invalid state. Domain Primitives force us to think more about validation. Other advantages I see are stronger type safety, better readability (in the classes using the Domain Primitives) and testability. It is also much easier to keep all the validation in one place which reduces defensive coding, if you have an instance of a Domain Primitive you know that you don’t have to check if it is valid.
Hidden advantages
Apart from those advantages I noticed another hidden advantage of using Domain Primitives instead of Primitive types. The code has more meaning, the ubiquitous language comes more alive in the code. And because I could not know what makes a ProjectName syntactically and semantically correct, I asked the other stakeholders and domain experts. I thought I would get a quick answer, but the question actually led to some discussions and more people were asked. It turned out that not only were there rules on the validation but the ProjectName was some kind of identifier for a Project, there were rules for duplicate names that were dependent on the state the Project was in. Rules that were completely implicit. Eventually, we ended up writing Domain Service logic.
Using Domain Primitives made me really think about the small concepts in the models I was working with, and that makes it easier to start asking questions. In the above example the team discovered the rules around ProjectNames. I noticed this pattern several times when I transformed primitive types to ValueObjects. Once I made the type explicit I started thinking about its semantics a lot more and could make informed decisions about what was important and what could be neglected. This was especially remarkable when I transformed a Swedish Social Security number (Personnummer) from a String to a ValueObject. There is a lot of logic in these numbers, so much that a Domain Primitive wasn’t enough. If you are interested here is some code on GitHub you can check out. Even a simple telephone number contains a lot of information if you start dissecting it.
Domain Primitives are easy to implement even in legacy systems and big balls of mud. I found them to be a powerful tool to start applying Domain Driven Design concepts, to start asking meaningful questions and making implicit concepts explicit, all while increasing the safety of the codebase. So next time you come across a primitive type in your business logic I hope you give them a try!