I originally posted this post on my blog a long time ago in a galaxy far, far away.
Not every primitive value deserves to be promoted to a value object.
Some time ago, at a past job, I reviewed a pull request that triggered a discussion about when to use value objects instead of primitive values.
If you're not familiar with Domain-Driven Design and its artifacts:
Value Objects Represents Concepts That Don't Have an Identifier
Value objects are immutable and compared by value.
Two value objects with the same values are considered the same. Two dates are the same if they point to the same year, month, and day combination.
Value objects represent elements of "broader" concepts. For example, in a Reservation Management System, we can use a value object to represent the payment method and the arrival and departure dates of a reservation.
Choosing Between TimeStamp and DateTime
Here's the piece of code that triggered my comment during the code review:
public class DeliveryNotification : ValueObject
{
public Recipient Recipient { get; init; }
public DeliveryStatus Status { get; init; }
public TimeStamp TimeStamp { get; init; }
// πππ
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Recipient;
yield return Status;
yield return TimeStamp;
}
}
public class TimeStamp : ValueObject // π
{
public DateTime Value { get; }
private TimeStamp(DateTime value)
{
Value = value;
}
public static TimeStamp Create() // π
{
return new TimeStamp(SystemClock.Now);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
public enum DeliveryStatus
{
Created,
Sent,
Opened,
Failed
}
We wanted to record when an email was sent, opened, and clicked.
We relied on a third-party Email Provider to notify our system about these email events. The DeliveryNotification
has an email address, status, and timestamp.
By the way, the ValueObject
base class is Vladimir Khorikov's ValueObject implementation.
Notice the TimeStamp
class. It's only a wrapper around the DateTime
class. Mmmm...
How to Promote Primitive Values to Value Objects
Using a TimeStamp
instead of a simple DateTime
in the DeliveryNotification
class was an overkill.
To choose between value objects and primitive values:
- If we need to enforce a domain rule or perform a business operation on a primitive value, let's use a value object.
- If we only pass a primitive value around and it represents a concept in the language domain, let's wrap it around a record to give it a meaningful name.
- Otherwise, let's stick to the plain primitive values.
In our TimeStamp
class, apart from Create()
, we didn't have any other method. We might validate if the inner date is in this century. But that won't be a problem. I don't think that code will live that long.
And, there are cleaner ways of writing tests that use DateTime than using a static SystemClock
. Maybe, it would be a better idea if we can overwrite the SystemClock
internal date.
A simpler route is to use a plain DateTime
value.
Apart from receiving it from a third-party service, we didn't run any business method on that property. We only stored it. There's no business case for TimeStamp
here.
public class DeliveryNotification : ValueObject
{
public Recipient Recipient { get; init; }
public DeliveryStatus Status { get; init; }
public DateTime TimeStamp { get; init; }
// πππ
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Recipient;
yield return Status;
yield return TimeStamp;
}
}
// Or alternative, to use the same domain language
//
// public record TimeStamp(DateTime Value);
// πππ
public enum DeliveryStatus
{
Created,
Sent,
Opened,
Failed
}
If in the "email sending" domain, business analysts or stakeholders use "timestamp," for the sake of a ubiquitous language, we can add a simple record TimeStamp
to wrap the date. Like record TimeStamp(DateTime value)
.
VoilΓ ! That's a practical option to decide when to use Value Objects and primitive values. The key is asking if there's a meaningful domain concept or operation behind the primitive value. Otherwise we would end up with too many value objects or obsessed with primitive values.
Starting out or already on the software engineering journey? Join my free 7-day email course to refactor your coding career and save years and thousands of dollars' worth of career mistakes.