Advancing Enterprise DDD - Entities, Value Objects, and Identity

In the previous essay of the Advancing Enterprise DDD series, we saw how we might improve our Domain Driven Design model by making our entity classes immutable. Here, we continue to investigate immutable entities by seeing how this affects one of the core DDD building blocks: the value object.

So far in this series, we've talked extensively about two of the three core building blocks of Domain Driven Design: entities and aggregates. But we've only mentioned value objects in passing. Let's consider what value objects are used for, and how they differ from our domain entities.

In practice, the hallmark of a value object is immutability. This is in contrast to entities, which are typically implemented as mutable objects. Value objects are generally defined as objects with no conceptual identity. An entity can change over time, but its identity will stay the same. But if a value object "changes" - that is, a copy is made with a slight mutation - the copy will not have the same identity as the original.

Value objects are useful for all the same reasons immutability is useful. You can share copies, or make extra copies, and the only consequences will be performance related. (As an aside, these kinds of performance concerns used to be of much more importance when memory was less abundant, and before the advent of generational garbage collectors. Nowadays, constructs like the flyweight pattern are a thing of the past.) You can share value objects with anybody, without having to worry about them modifying them behind your back. Value objects are easy to reason about, and work well in an asynchronous (multi-threaded) environment.

In the last essay, we saw how we might migrate our entire entity model into immutability. In this situation, the mutability distinction between entities and value objects becomes moot. So the difference between entities and value objects comes down to this concept of identity. Let's investigate this concept of identity further with some examples.

Let's start with a couple examples of objects that are clearly entities. Their conceptual identity will not change, even if the object changes over time. Here's a simple Customer entity that we looked at in The Entity and the Aggregate Root:

As a Scala case class, this would look like so:

case class Customer(
  customerUri: Uri,
  firstName: String,
  lastName: String)

The Customer entity has a natural key: the customerUri. Natural keys will always be unique, so that no two Customers will ever share the same customerUri. They also never change over the lifetime of the Customer. The firstName, lastName, and any other fields of the Customer may change over time, but the Customer will remain the same. If we wanted to compare two Customer objects to see if they were the same Customer, even if one or both of them had undergone some modifications, we would do it like so:

customer1.customerUri == customer2.customerUri

Note that in Scala, the == operator is a synonym for equals. This is quite different behavior from the Java == operator.

We could consider overriding Customer.hashCode and equals so that two Customer objects are equal whenever they have the same URI, but I would recommend against this. You can always compare the URIs directly. Overriding them may be confusing, because it introduces non-standard behavior for Scala case classes. And if you do override them, the compiler won't generate the standard case class hashCode and equals for you, so you will never be able to take advantage of them.

As we discussed in The POJO Myth, this natural key is different from the primary key, which is a database-specific unique key that is not part of the domain model, and should ideally be encapsulated within the persistence framework. The application never needs to reference a Customer by its primary key, as the customerUri will always suffice.

Let's take a look at the Order entity that was also introduced in The Entity and the Aggregate Root:

A Scala case class for an Order, slightly simplified, would look something like this:

case class Order(
  customer: Assoc[Customer],
  orderDate: DateTime,
  shippingAddress: Address,
  orderItems: Seq[OrderItem])

We introduced the concept of Assoc in Reinstating the Aggregate. It represents an association to an entity outside the current aggregate. (We called it Association in Java, but we'll call it Assoc in Scala.)

Suppose a Customer placed an Order, then realized the selected shipping address was wrong. She might update the Order to have the correct shipping address before the Order is processed. In this case, the Order has changed, but it is still conceptually the same Order. So an Order also has conceptual identity.

In a real application, an Order would probably have some kind of orderNumber or orderUri, to uniquely identify the Order. But let's suppose it did not, for the sake of example. Let's also suppose that the orderDate property just captured the date, and was not a complete timestamp. In this situation, it is perfectly plausible that two Orders looked exactly the same, but had unique identities. I'm sure it's happened many thousands of times in real life that a Customer intentionally placed two Orders of exactly the same items on the same day. In this case, we would want to consider them as distinct Orders, and process them both independently.

As modeled here, we would never be able to look up a unique Order. We could only do range searches, such as all the Orders made by a particular Customer in a particular timeframe. This is not very plausible, and in any storefront application, we would want unique identifiers for each Order.

It's hard to imagine situations where an entity would not have a natural key, but not impossible. For instance, suppose we were writing an application to process log entries in a distributed system. We might do a range search, such as finding all the log entries produced by the order processing module, with log level INFO or higher, within a specified time frame. It might be difficult or awkward to construct a natural key for a log entry in a distributed system, and we might never have a need to look up individual log entries. So it is at least hypothetically possible to have an entity without a natural key.

What about the Order Item? Is it an entity or a value object? The Order Item case class looks like this:

case class OrderItem(
  retailItem: Assoc[RetailItem],
  quantity: Int,
  price: Amount)

If a Customer updates the quantity of an Order Item in an Order, is it still the same Order Item, or a new Order Item? If somebody removes an Order Item from an Order, and then adds back the exact same item, is it the same Order Item, or a new one? Are these questions simply philosophical, or do they have practical implications?

In RDB (relational databases), whether or not the Order Item has a conceptual identity does have practical implications. If an Order Item does have conceptual identity, then our ORDER_ITEM table would contain a primary key such as ORDER_ITEM_ID:

Without conceptual identity, the ORDER_ITEM table has no primary key:

The respective JPA annotations would look quite different in these two scenarios as well. In the former case, OrderItem would be annotated with @Entity, and Order.orderItems would be @OneToMany. In the latter case, OrderItem would be an @Embeddable, and Order.orderItems would be annotated @ElementCollection.

However, if we are using MongoDB, and mapping our DDD aggregates into documents, as we discussed in Documents as Aggregates, the distinction has no practical importance. Our Order aggregates look something like this in JSON:

{ _id = ObjectId("54a1c9ed726d9169d5c51d3e"),
  customer = ObjectId("54a1c9ed726d9169d5c51d48"),
  orderDate = "Sun May 30 18:47:06 +0000 2010",
  shippingAddress = {
    street1 = "Humboldt General Hospital",
    street2 = "3100 Southwest 62nd Avenue",
    city = "Miami",
    state = "FL",
    zipcode = "33155"
  orderItems = [
    { retailItem = ObjectId("54a1c9ed726d9169d5c51d27"),
      quantity = 1,
      price = "$17.95"
    { retailItem = ObjectId("54a1c9ed726d9168d5c51d22"),
      quantity = 2,
      price = "$5.95"

If we are implementing our entities as immutable objects, and storing our aggregates in a document database, there is no practical difference between an Order Item being an entity or a value object. It is simply a philosophical question.

In Eric Evans' seminal book on Domain Driven Design, he dedicates three paragraphs to the question of whether an address is or is not a value object (sidebar, page 98). Depending on the circumstances, it could be modeled either way. But unless an address is an aggregate root, (such as in an application for a postal service to organize delivery routes), it won't matter to us. The addresses will be immutable objects in Scala in either case, and they will never need a database ID in a document database.

Is this idea of "conceptual identity" a real concern, or is it just a fancy OO jargon to justify our use of immutability in some circumstances, but not in others? Is it sometimes used as a way to rationalize when we should put an ID column into an RDB table, and when we should not? Whatever the case, it seems like a point of complexity in the DDD mindset that, to some extent, is a result of our thinking being affected by the tools we are using. With immutable objects and document databases, the issue becomes entirely moot, and we can simplify our lives by just talking about entities, and not worrying about distinguishing between entities and value objects.

In these last few posts, we've seen how immutability can make our lives easier when doing Domain Driven Design. We've learned how it can help us enforce intra-aggregate constraints. We've seen how we could encapsulate persistence concerns, preventing persistence data from infecting our domain classes. And we've simplified our modeling thought process by removing concerns about entities versus value objects. This concludes the technical portion of this series of essays. In the next and final essay, we'll wrap things up by putting all these technical approaches back into a larger picture.

1 comment:

  1. Your article is very interesting.
    Often when we break into a paradigm without understanding its philosophical dimension, we may lose interesting solutions that lie outside of it