By Dan Bergh Johnsson, Daniel Deogun, Daniel Sawano

This article delves into domain primitives: what they are, how to define them, and how they can be used to create secure software.

Save 37% off Secure by Design with code fccjohnsson at manning.com.

Domain primitives and invariants

Some of the key properties of a value object in Domain-Driven Design are its immutability and that it forms a conceptual whole. We’ve found that if you take the concept of the value object and slightly tweak it, while having security in mind, you get something called a domain primitive.

When you start using domain primitives as the smallest building blocks in your domain model you’ll be able to create code with a significantly reduced likelihood of security issues, through the way you’re designing it. You’re designing code which is precise and leaves little or no room for ambiguity. This type of code tends to contain fewer bugs and, as a consequence, fewer security vulnerabilities. The code also tends to be easy to work with, because domain primitives lower the cognitive load on developers.

Domain primitives as the smallest building blocks

A value object represents an important concept in your domain model. When modeling it, you decide how to represent the value object and what name it should have. If you take this further and also put effort into determining what it is and what it’s not; you’ll gain significantly deeper insight into that concept. You can then use that insight to introduce invariants that must be upheld in order for the value object to be considered valid.

You can continue and say that the value object not only should or can but must uphold these invariants, and that they must be enforced at the time of creation. What you end up with is a value object strict enough in its definition that if it exists, it’s also valid. If it’s invalid then it can’t exist. This type of value object is what we refer to as a domain primitive.

A value object precise enough in its definition that it, by its mere existence, manifests its validity is called a domain primitive.

Domain primitives are similar to value objects in Domain-Driven Design. Key differences are that we require invariants to exist and they must be enforced at the point of creation. We’re also prohibiting the use of simple language primitives, or generic types (including null), as representations of concepts in the domain model.

Nothing in a domain model should be represented by a language primitive or a generic type. Each concept should be modeled as a domain primitive to carry meaning when passed around, and to uphold its invariants.

Let’s say you have the concept of quantity in your domain model. The quantity is the amount a customer wants to buy of a certain item in the webshop you’re building. The quantity is a number, but instead of representing it as an integer you create a domain primitive called Quantity. While defining Quantity you discuss with the domain experts what’s considered to be a valid quantity in the context of the current domain. This discussion reveals that a valid quantity is an integer value between 1 and 200. A zero quantity isn’t valid because if the customer wants to buy zero items, then the order shouldn’t exist at all. A negative value is invalid, either because you can’t un-buy products and/or returns are handled separately. Orders for more than 200 items aren’t handled by the system at all. Such large orders are extremely rare and if they do occur they need special handling; these are dealt with via direct contact with a sales representative instead of through the online store.

You also encapsulate important behavior of the domain primitive — such as addition and subtraction of quantities. By having the domain primitive own and control domain operations, you reduce the risk of bugs caused by lack of detailed domain knowledge of the concepts involved in the operation. The further away from a concept they are, the less detailed knowledge of the concept can be expected, and it makes sense to keep all domain operations within the domain primitive itself. To give you an example, if you need to be able to add two quantities and you create a method add, then the implementation of that method needs to take into account the domain rules of a quantity — remember we’re not dealing with plain integers anymore. If you were to place the add method somewhere else in your codebase, say in a utility class called Functions, then it’d be easy for subtle bugs to creep in. If you decide to slightly change the behavior of the Quantity domain primitive, will you remember to also update the method in the utility class? Chances are you’ll forget — and this is how you introduce hard-to-find bugs that can lead to serious problems.

When you’re done, the Quantity domain primitive looks like the following listing when represented in code.

Listing 1. The Quantity domain primitive

 
 import static org.apache.commons.lang3.Validate.inclusiveBetween;
 import static org.apache.commons.lang3.Validate.notNull;
  
 public final class Quantity {
  
    private final int value;                                       
  
    public Quantity(final int value) {
       inclusiveBetween(1, 200, value);                            
       this.value = value;
    }
  
    public int value() {
       return value;
    }
  
    public Quantity add(final Quantity addend) {                   
       notNull(addend);
       return new Quantity(value + addend.value);
    }
  
    // equals() hashCode() etc...
  
 }
 

  The actual integer value

  Enforcing invariants at time of creation

  Providing domain operations to encapsulate behavior

This is a precise and strict code representation of the concept of quantity. A domain primitive like the Quantity created here removes the possibility of some dishonest user sending in a negative value, tricking the system into unintended behavior. Using domain primitives removes a security vulnerability without the use of an explicit countermeasure.

As this modeling exercise has shown, the quantity isn’t only an integer. It should be modeled and implemented as a domain primitive to carry meaning when passed around and to uphold its invariants.

Now you’ve learned the basics of what a domain primitive is. Let’s move on and look at the importance of defining the scope in which a domain primitive is valid.

Context boundaries define meaning

Domain primitives, like value objects, are defined by their value rather than an identity. This means that two domain primitives of the same type, and with the same value, are interchangeable with each other. Domain primitives are perfect for representing a wide variety of domain concepts that don’t fit into the categories of entities or aggregates. One important aspect to keep in mind when modeling a concept using a domain primitive is that it should be defined to mean exactly what the concept is in the current domain.

Say you’re building a system that allows users to choose and create their own email addresses. A user can choose the local part of the email address (the part to the left of the @), and once created they can start sending and receiving messages using that address. If a user entered jane.doe then the email address jane.doe@example.com would be created (assuming your domain name is example.com). When modeling, you realize that an email address is a perfect example of a domain primitive. It’s defined by its value, and you can come up with some constraints that you could use to assert that it’s valid. At first, you might be inclined to use the official definition of an email address to figure out what constitutes a valid address. Although this is technically correct in terms of meeting the requirements of the RFC, it might not be what’s considered a valid email address in the context of the current domain (figure 1). As an engineer this might come as a surprise to you, but remember, we’re focusing on the meaning of a concept in a specific domain, not what it might mean in some other context, like in the context of a global standard. For example, your domain might define an email address to be case insensitive, such that anything the user enters is transformed to lowercase. You could go even further and say that the only characters allowed are ASCII characters, digits, and dots ([a-z0-9.]). This is a deviation from the technical specification, but it’s a valid choice in the context of the current domain.


Figure 1. The meaning of a term is defined within the boundaries of a given context


Sometimes you’ll encounter situations where the name of the concept you’re trying to model is also used outside of the current context — and where its external definition is prevalent it can be confusing to redefine it in your domain model. An email address might be such a term, but as you learned it can make sense to redefine the term “email” in your current domain. Another example of a well-defined term is an ISBN. The ISBN is defined by an International Organization for Standardization (ISO) standard, and redefining it could cause confusion, misinterpretation, and bugs. These types of subtle differences in meaning are a common cause for security issues, and you want to avoid them, particularly when interacting with other systems or other domain contexts (figure 2).


Figure 2. Using an externally defined term without changing its meaning


A lot of times when you find yourself redefining a well-known term, the need for that redefinition is because the term is used to describe more than one thing in your current context. In those cases, try to either split the term into two distinct terms or come up with an entirely new term. This new term is unique to your current context, and you avoid any misinterpretation. It also makes it clear why certain specific invariants are used instead of those associated with the externally defined term. Another benefit of introducing a new term is that the original term can keep its crisp definition and remain a domain primitive. You’ve maintained full freedom to model important concepts in your domain without losing any of the model’s exactness.

Imagine you’re building book-managing software that uses ISBNs to identify books. Soon you’ll realize you need a way to identify and handle books that haven’t received an ISBN yet. One approach is to redefine the term “ISBN” to not only represent real ISBN numbers, but also to include internally assigned identifiers, perhaps using a magic prefix or something similar to distinguish them from the real ISBNs. To avoid the possible confusion that comes with redefining an ISO standard you could instead introduce a new term, BookId, that contains either an ISBN or an UnpublishedBookNumber (figure 3). BookId is what identifies a book, and UnpublishedBookNumber is the internally assigned identifier.


Figure 3. Introducing new terms instead of redefining existing ones


By introducing two new terms, BookId and UnpublishedBookNumber, you’re able to keep the exact and well-known definition of “ISBN” while at the same time meeting the needs of your business domain.

Building your domain primitive library

Now that you’ve expanded your toolbox with the versatility of domain primitives, you should strive to use them as much as you can in your code. These are the smallest building blocks and form the basis of your domain model. As such, almost every concept in your model, regardless of how small it is, is based on one or more domain primitives. When you’re done modeling you’ll have a collection of domain primitives that you can view as your domain primitive library. This library isn’t a collection of generic utility classes and methods, but rather a well-defined, ubiquitous set of domain concepts. And because they’re domain primitives, they’re safe to pass around as arguments in your code like regular value objects.

Domain primitives lower the cognitive load on developers because there’s no need to understand their inner workings in order to use them. You can safely use them with the confidence that they always represent valid values and well-defined concepts. If they’re invalid they won’t exist. This also removes the need to constantly revalidate data in order to make sure it’s safe to use. If it’s defined in your domain, you can trust it and use it freely.

Hardening your APIs by using your domain primitive library

You should always strive to use domain primitives in your programmatic APIs. If every argument and return value of a method is valid by definition, you’ll have input and output validation in every single method in your codebase — without any extra effort. The way you’re using domain design enables you to create code which is extremely resilient and robust. A positive side effect of this is that the number of security vulnerabilities caused by invalid input data will drastically decrease.

Let’s examine this more closely with a code example. Say you’re given the task of sending the audit logs of your system to a central audit log repository. Audit logs contain sensitive data, and it’s important that they’re sent to a designated place to be stored and protected properly. Sending the data to the wrong place can have a significant negative business impact.

If you create a method in your API that takes the current audit logs and sends them to a log repository located at a given server address, it could end up looking like this:

 
void sendAuditLogsToServerAt(java.net.InetAddress internalIpAddress);
 

The issue here’s that a method signature like this allows for any IP address to be the destination for the logs. If you fail to properly validate the address before sending the logs you could potentially send them to an insecure location and reveal sensitive data. If you instead define a domain primitive, InternalIPAddress, that strictly defines what an internal IP address is, you can have that be the type of the input parameter in your method. Applying this to the sendAuditLogsToServerAt method leads to the code in the following listing instead.

Listing 2. Hardening the API with domain primitives

 
 void sendAuditLogsToServerAt(InternalAddress serverAddress) {     
    notNull(serverAddress);
  
    // Retrieve logs and send them to server
 }
 

  The only input validation left to perform is a null check

Now you’ve designed your method making it impossible to pass invalid input to it. The only form of validation left to do, in terms of verifying that the IP address is internal, is to make sure it’s not null.

Avoid exposing your domain publicly

One thing to remember when hardening your API is that if you have an API that acts as an interface to a different domain, you should avoid exposing your domain model’s objects in that API. If you do, you instantly make your domain model part of your public API. As soon as other domains start using your API it quickly becomes difficult to change and evolve your domain independently.

An example of a public API facing a different domain is a REST API exposed on the internet for others to consume via client software. If you expose your internal domain in the REST API then you can’t evolve your domain without forcing the clients to evolve with you. If your business depends on those clients then you can’t ignore them and have no other option but to evolve at the same pace as your consumers can adapt their clients. To make things even worse, if you have multiple consumers then you’re not only tying yourself together with each consumer, but you’re tying the consumers together with each other. This is a less than ideal situation to be in, and you can avoid it by shielding your domain publicly.

What you want to do instead is to use a different representation of each of your domain objects. This can be viewed as a type of data transfer object (DTO) used to communicate with other domains. You can place invariants in those DTOs, but those won’t be the same constraints that exist in your domain. Rather, they can for example be constraints relevant to the communication protocol defined by the API. The first thing you do in an API method like this is to convert the DTO into the corresponding domain primitive(s) in order to ensure its data is valid.

By using this layer of translation between the concepts in your public API and your domain, you’re able to uncouple the two. This allows you to evolve the API and your domain independently.

We’ve covered a lot of important aspects of domain primitives in this section. Before we move on, let’s review the key points:

  • Their invariants are checked at the time of creation.
  • They can only exist if they’re valid.
  • They should always be used instead of language primitives or generic types.
  • Their meaning’s defined within the boundaries of the current domain, even if the same term exists outside of the current domain.
  • You should use your domain primitive library to create secure code.

You’ve learned about immutability, failing fast, validation, and domain primitives, and how these concepts promote security by design.

If you want to learn more about the book, read the first chapter on liveBook for free here and see this slide deck.