Benevity Tech Blog

Primitive Obsession and ValueObjects

Written by Corey Jasinski (Senior Software Developer) | Feb 6, 2024 8:12:02 PM

Avoiding the use of Primitive Obsession's by using ValueObjects.

Often during development it is easy and convenient to use the provided primitive types provided by the language to define the system. Primitive types are those that are built into the language, such as String, boolean, double, int etc.

public final class Program {
	public static void example() {
		final String email = "someone@email.com";
		final int age = 15;
		final double meters = 12.34;
		final double celsius = -10.0;
	}
}

As seen in the above example, primitive values have been used as the type, with their meaning applied through the variable name. Looking at each of the variable names, it can be seen that only a subset of values are intended to be assigned, and those values are to be only allowed in specific scenarios.

Assigning email = "Octopus" is allowed by the compiler, but does not make sense with the context the variable name implies. This nonsense value would imply that there is a restricted subset of acceptable values, and that not just any String is valid.

Similarly, adding meters + celsius is acceptable by the compiler, but again does not mean anything. This is a nonsense value, as these units are not combinable.

Trying to force a primitive type to represent a subset of a domain (whether in value, or in uses) is known as Primitive Obsession. It is using primitives for things that should be restricted in some way.

To avoid primitive obsession and build a stronger domain within the program, a design pattern known as ValueObjects can be used. At its simplest, a ValueObject is a small class that encapsulates the single primitive type, providing a clear name and new strong type within the system.

public final class Email {
	public final String value;
     
	public Email(final String value) {
		this.value = value;
	}
}

public final class Age {
	public final int value;
     
	public Age(final int value) {
		this.value = value;
	}
}

public final class Meters {
	public final double value;
     
	public Meters(final double value) {
		this.value = value;
	}
}

public final class Celsius {
	public final double value;
     
	public Celsius(final double value) {
		this.value = value;
	}
}
The above are all small examples of creating a ValueObject around a primitive. They currently do not do anything, aside from providing a new strong type, but in the next sections we can take a look at how to restrict how the value is used, as well as what values are acceptable.