Value Object, Entities and validation at runtime

2026-06-14, 4 minutes reading for Software Engineers

As I'm building my personal annotation project, I come across code that the LLMs have decided to spread around and duplicate variables instead of reusing an existing one, so I step aside and look around only to see tech debt, which is asking to be refactored and I'm reminded to start using value objects.

Your 2nd rewrite will have a different type of tech debt

Value objects

The book Learning Domain-Driven Design (DDD) defines a value object as:

A value object is an object that can be identified by the composition of its values. For example, consider a color object:
-- Vlad Khononov
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}
The composition of the values of the three fields—red, green, and blue—defines a color. Changing the value of one of the fields results in a new color. No two colors can have the same values. Also, two instances of the same color must have the same values. Therefore, no explicit identification field is needed to identify colors.
-- Vlad Khononov

Let's walk through the evolution of this Release struct in my weather dashboard codebase. This Release struct is part of the configuration introduced by this commit:

Initially, these values were hardcoded in the update function itself:

const release_info_url: String,
const download_path: String,
const update_interval_days: i32,

I then identified that these are related, so I encapsulated them into a struct, but still using primitive types (primitive obsession). The book Learning DDD calls this an Anemic model (often referred to as a DTO):

pub struct Release {
    pub release_info_url: String,
    pub download_path: String,
    pub update_interval_days: i32, 
}

After refactoring with custom types and typed fields (value objects):

use std::path::PathBuf;
use url::Url;

#[nutype(
    sanitize(),
    validate(greater_or_equal = 0),
    derive(Debug, Deserialize, PartialEq, Clone, AsRef, Copy)
)]
pub struct UpdateIntervalDays(i32);

impl fmt::Display for UpdateIntervalDays {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.into_inner())
    }
}

pub struct Release {
    pub release_info_url: Url,  // no longer accepting an invalid URL
    pub download_path: PathBuf, // Downstream processing on this field will safely handle paths that start or end with slash.
    pub update_interval_days: UpdateIntervalDays, // no longer accepting negative numbers
}

Now it might seem I've added extra code over the Anemic Model, but I've reduced downstream complexity, because with the former, we had these downsides:

  1. As alluded to earlier, you are more likely to duplicate the code in multiple places
  2. Any string can be assigned, even invalid URLs or paths
  3. No clear indication of what each field represents beyond its name, you are not making use of the type system and all the benefits that come with it

Regardless of whether the language is strongly typed, using primitives makes it much harder and less elegant to enforce validation before use. With primitives, the consumer needs to remember to call validation before using the object.

After the change, instead of handing over a string into a PathBuf, I no longer have to convert the argument to PathBuf before calling functions that require a PathBuf parameter, and with Url instead of String, any function accepting a Url parameter knows it's valid, no need to check for URL syntax.

Validation at construction time helps prevent invalid state early, though in some cases (e.g., evolving business rules or mutable value objects) additional validation may still be needed downstream.

In essence, functions no longer need to convert arguments to the appropriate type before processing, the values are pre-validated and are safe to use as parameters, the validation at construction time + type system enforce correctness.

We also get the benefit that comes with the new type as it enforces a ubiquitous language, which means everyone will understand what it is regardless of whatever variable name it is assigned to.

With value objects I can also centralise the business logic within the object itself. For example, my Temperature has this implementation:

Temperature implementation
impl Temperature {
    pub fn to_celsius(self) -> Temperature {
        match self.unit {
            TemperatureUnit::C => self,
            TemperatureUnit::F => Temperature {
                value: (self.value - 32.0) * 5.0 / 9.0,
                unit: TemperatureUnit::C,
            },
        }
    }
    pub fn to_fahrenheit(self) -> Temperature {
        match self.unit {
            TemperatureUnit::C => Temperature {
                value: (self.value * 9.0 / 5.0) + 32.0,
                unit: TemperatureUnit::F,
            },
            TemperatureUnit::F => self,
        }
    }
}

Entity

In my day-to-day work, I run into Entities much more often. At first glance, an Entity may seem like a just another object, however what differentiates an Entity from a value abject is that it has an ID.

An example of an entity is a Session of a user:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    pub id: SessionID,
    pub original_url: Url,
    ...
}

Unlike value objects, in general entities have a lifecycle. The entity that was created 10 years ago and is still sitting in your database needs to still work correctly today and transition from one state to another. The business needs to be aware of the history of that entity, whereas with value objects we can create and discard them at any time.

So next time you're coding, ask yourself where you can make use of these concepts

In summary Entity vs Value object

CharacteristicValue ObjectEntity
IdentityDefined by composition of valuesDefined by separate ID field
ComparisonAll values must matchId must match
LifecycleGenerally staticGenerally dynamic
ValidationAt instantiation time, easier to reason aboutLess straightforward due to dynamic lifecycle