I Hate Value Objects

Value Objects: A Barrier to DDD. Descriptor Data to the Rescue - Part 1

Masoud Bahrami
7 min readJan 1, 2025

Let’s start with the basics. What did the guys, pioneers of software development mean by Value Object?

1. Introduction

Value Objects, a cornerstone of DDD, were introduced with the promise of creating more expressive, maintainable, and testable domain models. They aimed to encapsulate related values, enforce immutability, and define equality based on value rather than identity.

As Eric Evans states in Domain-Driven Design: Tackling Complexity in the Heart Of Software:

“A Value Object is an object that represents a concept within a domain model that has no Inherent Identity. It is defined entirely by its attributes, and two Value Objects are considered equal if they have the same values for all their attributes. Value Objects are immutable, meaning their attributes cannot be changed once created.”

Martin Fowler further clarifies this concept, describing them as:

“a small simple object, like money or a date range, whose equality isn’t based on identity, drawing a comparison to primitive types in non-purely object-oriented languages.”

Vaughn Vernon, in Implementing Domain-Driven Design, echoes these sentiments, stating:

“Value objects are an important concept in domain-driven design. They are used to represent concepts that do not have a unique identity, such as money, color, or date. Value objects are immutable, meaning that they cannot be changed after they are created. They are also equal if they have the same attributes. By using value objects, you can create more maintainable and reusable code.”

These definitions highlight the core intentions behind Value Objects:

to model domain concepts clearly and concisely, focusing on what they are rather than who they are.

However, despite these well-intentioned goals, practical experience has revealed several inherent challenges, sometimes leading to frustration and a sense that this seemingly simple concept adds unnecessary complexity.

This article delves into these challenges, exploring why Value Objects can become a stumbling block in DDD and proposing Descriptor Data as a more pragmatic and effective approach to modeling values within a domain.

This shift in terminology and implementation seeks to bridge the gap between DDD principles and the increasingly prevalent functional programming paradigm.

2. The Intended Benefits of Value Objects

The advocate of DDD envisioned Value Objects as a powerful tool for addressing common domain modeling pitfalls. They wanted a way to represent domain concepts more accurately, concisely, and meaningfully. Among the anticipated benefits were improved clarity and readability of domain models.

2.1. Think of It as a Whole

Imagine representing an address: instead of separating individual street, city, postal code, and country properties throughout your code, a single Address Value Object could encapsulate all of them, immediately clarifying the intent and structure of the data.

public class Address : ValueObject
{
public string Street{get;set;}
public string City{get;set;}
public string PostalCode{get;set;}
public string Country{get;set;}

// override default equality comparison methods
}

2.2.2. Immutability

Immutability, a core characteristic of Value Objects, was intended to prevent accidental modification of data(aliasing), reducing the risk of bugs and enhancing code reliability.

2.2.3. Equality

By ensuring that Value Objects could not be changed after creation, developers could reason more confidently about the state of their system. Value-based equality, where two Value Objects are considered equal if their constituent values are the same, simplified comparisons and testing. This approach made unit tests more straightforward and reduced the complexity of comparison logic.

Finally, Value Objects were expected to minimize coupling between different parts of the system. Since Value Objects are immutable and compared by value, changes to one Value Object would not inadvertently affect other parts of the code, promoting modularity and maintainability.

3. Challenges and Drawbacks of Value Objects

While the theoretical benefits of Value Objects are compelling, their practical application often presents significant challenges.

The very name Value Object can be a source of confusion, especially in contexts where functional programming principles are emphasized. The term Object carries connotations of identity and mutable state, which directly contradict the core principles of functional programming, where values are immutable and computations are side-effect-free. This terminological disagreement can create a mental obstacle for developers accustomed to functional thinking.

One of the other important points about Value Objects is their immutability. Compared to its long-standing rival in DDD, the Entity, it’s this very discussion of being immutable or not. The reason behind this feature is that we call something an entity because its lifecycle is important to us, and we prefer to keep track of the changes that have occurred to that entity, but in comparison, we don’t have this sensitivity towards something that acts as a Value Object, and we are not very sensitive to its lifecycle, which is why we always create the entire Value Object from scratch and replace it with the previous one as soon as there is any change, however small, in a value object. This sensitivity in the implementation dimension causes something that was supposed to have less sensitivity in design and implementation to create, on the contrary, more sensitivity and more complexity and coding for us.

3.1. Wait(please 🤔)

Take a step back! Thanks. Give it some thought! What know do we see?

We update, or more precisely replace parts of, the state of an Entity because its lifecycle is crucial to us. We need to track changes to Entities over time:

Order.Status = OrderStatus.Calceled

// OR, in a more object-oriented style!
Order.SetStatus(OrderStatus newStatus)
{
this.Status = newStatus;
}

In contrast, we never change (update or replace parts of) a Value Object. We design Value Objects with the explicit understanding that their lifecycle is not significant. Instead of modifying a Value Object, we replace it with a new instance containing the desired values.

order.ShippingAddress = new Address("123 Main St", "Anytown", "12345");

// To change the address title to "456 Oak Ave":
order.ShippingAddress = new Address("456 Oak Ave", "Anytown", "12345"); // New Address instance

// OR
order.ShippingAddress = order.ShippingAddress.WithNewTitle("456 Oak Ave");



public class ShippingAddress : ValueObject
{
public string Title {get;private set;}
public string Street {get;private set;}
public string PostalCode {get;private set;}

Public ShippingAddress WithNewTitle(string newTitle)
{
// we dont change anything!
return new ShippingAddress(newTitle,this.Street, this.PostalCode);
}
}

On the ground, When it comes down to it, it seems that we do more care about the lifecycle of a Value Object than an Entity. While the definition of Value Objects emphasizes that we don’t care about their lifecycle in the same way we do for Entities, in practice, it can seem like we care about it more. This apparent contradiction arises from how we handle changes to Value Objects! (in the next part I’ll talk more about this !)

3.2. Equality Side-Effects

Practical implementation also reveals several difficulties. Implementing the equals and hashCode methods correctly is crucial for ensuring proper comparison and use of Value Objects in collections. This can become particularly intricate when dealing with nested Value Objects or collections of Value Objects. Incorrect implementations can lead to subtle and difficult-to-debug errors.

3.2.3. Impedance Mismatch

Furthermore, mapping Value Objects to relational databases and working with ORMs can be problematic. ORMs are often designed with entities (objects with identity) in mind, and adapting them to handle Value Objects seamlessly can require workarounds and custom mapping logic, adding complexity to the data access layer. Similarly, serialization and deserialization of Value Objects can require extra effort to ensure that the integrity and structure of the Value Objects are preserved.

4. Introducing Descriptor Data

Having examined the challenges associated with Value Objects, I introduce Descriptor Data as a compelling alternative. Descriptor Data represents a shift in perspective, moving away from the object-oriented connotations of Value Object and embracing a more data-centric approach that aligns seamlessly with functional programming principles. Descriptor Data emphasizes the descriptive nature of the data, focusing on its meaning and structure rather than any notion of identity or behavior.

Key characteristics of Descriptor Data include a strong focus on the data itself and its inherent meaning within the domain. Like Value Objects, Descriptor Data is immutable, ensuring data integrity and simplifying reasoning about program state.

Critically, Descriptor Data is typically represented using simple data structures, such as records, tuples, dictionaries, or plain data structures readily available in most programming languages. This contrasts with the class-based approach often used for Value Objects. This emphasis on simple data structures leads to clear semantics, where the structure and naming of the data clearly communicate its meaning within the domain, enhancing readability and understanding.

5. How Descriptor Data Addresses Value Object Challenges

Descriptor Data directly addresses the challenges we discussed earlier regarding Value Objects. By avoiding the term Object and embracing simple data structures, Descriptor Data eliminates the conceptual friction with functional programming.

Implementing equality checks and hashing becomes trivial, as these operations can be performed directly on the underlying data structures using built-in language features.

This simplifies the implementation significantly and reduces the risk of errors. Descriptor Data also tends to map more naturally to database schemas, making it easier to work with ORMs and persistence mechanisms.

6. Comparing Value Objects and Descriptor Data

While both Value Objects and Descriptor Data aim to represent immutable domain concepts based on value, they differ significantly in their approach. Value Objects, rooted in object-oriented principles, are typically implemented as classes with methods and potentially internal logic. This can lead to increased complexity, especially when implementing equality checks, hashing, and persistence. Descriptor Data, on the other hand, embraces a data-centric approach, leveraging simple data structures readily available in most programming languages.

7. Benefits of Descriptor Data

  • Simplicity and Readability: The Descriptor Data representation is much more concise and easier to understand.
  • Better Functional Programming Alignment: Descriptor Data naturally aligns with functional programming principles by using immutable data structures and avoiding object-oriented concepts like methods and internal state.
  • Simplified Implementation: No need for complex equals and hashCode implementations, ORM mapping logic, or custom serialization.

--

--

Masoud Bahrami
Masoud Bahrami

Written by Masoud Bahrami

DDD teacher and practitioner, software engineer, architect, and modeler. Specialized in building autonomous teams and services.

No responses yet