DEV Community

Cover image for How to Promote Primitive Values To Value Objects
Cesar Aguirre
Cesar Aguirre

Posted on • Originally published at canro91.github.io

How to Promote Primitive Values To Value Objects

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. If we need to enforce a domain rule or perform a business operation on a primitive value, let's use a value object.
  2. 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.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)