Classes internal to an aggregate: entities or value objects?

While such classes as Person and Money are pretty intuitive and can be easily attributed to either entities or value objects, the choice isn’t so obvious when it comes to classes that are internal to an aggregate. That is classes that aren’t roots of their own aggregates but rather parts of existing ones.

Internal classes: examples

This post is an answer to an interesting question I was asked on my Pluralsight course's discussion board.

To give you some context, the sample project I used in the course has roughly the following object model:

public class SnackMachine
{
    private List<Slot> Slots { get; set; }
}
public class Slot
{
    public int Quantity { get; set; }
    public Snack Snack { get; private set; }
    public decimal Price { get; private set; }
    public int Position { get; private set; }
}

Each snack machine contains a fixed set of slots, and each slot has a number of snacks which are being sold at some price. When a user buys a snack, Quantity is decreased by one, that’s why it has a public setter; other properties stay unchanged. The Slot class is a part of the Snack Machine aggregate.

The question goes as this (paraphrased):

Should we treat Slot as an Entity or as a Value Object?

The issue with the code above is that while the snack machine class is clearly an entity, it is not so obvious where to attribute the Slot class. On one hand, it is mutable as we can change the number of snacks left, so it should be an entity.

On the other, do we really care about its identity? As I wrote previously, the way we compare objects to each other is essential. Each snack machine is unique in a sense that we don’t treat them interchangeably even if they contain the same set of slots. Slots themselves, however, aren’t unique. If a machine has two slots with the same sets of properties, we can very well replace one by another. Doesn’t it mean Slot is a value object?

This dichotomy is a common issue with classes that are internal to an aggregate. People (me included) often struggle about whether they should treat them as entities or value objects. Here’s an example with an Order class (taken from this article):

public class Order
{
    private List<OrderLine> Lines { get; set; }
}
 
public class OrderLine
{
    public int Position { get; set; }
    public bool IsCanceled { get; set; }
    public int Quantity { get; private set; }
    public Guid ProductId { get; private set; }
    public decimal Price { get; private set; }
}

There’s essentially the same problem here. While we do need to distinguish orders regardless of their content, we don’t really care that much about order lines. Two lines of the same product, quantity and price are indiscernible for the end user. We can replace one with another of the same properties and nobody will notice.

So, how to resolve this problem? Are such classes really value objects? Or are they entities?

The concept of Local Identity

While it’s true that instances of the Slot and OrderLine classes are indistinguishable to an external user, they still possess an identity of a different kind: Local Identity.

The Slot class does have an individuality but that individuality is meaningful only within the boundaries of its aggregate. A snack machine needs to distinguish its slots somehow in order to decide which of them it has to work with in a given transaction. It does it by locating a slot by its position. The Position property essentially acts as a local identity here.

The same is applicable to the OrderLine class. The aggregate must map the user’s commands to a particular line when it comes to editing an order on the screen, and it does it using the position of that line inside the order. The Position property provides the identity needed to make a decision which line to update or delete.

This makes both Slot and OrderLine classes entities. But there’s more in that.

If we look at the use cases for the Slot entity, we will notice that the fields Snack, Price, and Quantity are always used together. The same is true for OrderLine: whenever we need to display a line, not only do we show what the product that is, but we also indicate its price and the quantity the user ordered.

This makes these properties a cohesive whole inside their entities. It means that we can define a new concept for them. Here is an example for the Slot class:

public class SnackMachine : AggregateRoot
{
    private List<Slot> Slots { get; set; }
        
    public void BuySnack(int position)
    {
        Slot slot = GetSlot(position);
        slot.SnackPile = slot.SnackPile.SubtractOne();
    }
}
public class Slot : Entity
{
    public SnackPile SnackPile { get; set; }
    public int Position { get; }
}
public class SnackPile : ValueObject<SnackPile>
{
    public Snack Snack { get; private set; }
    public int Quantity { get; private set; }
    public decimal Price { get; private set; }
 
    public SnackPile SubtractOne()
    {
        return new SnackPile(Snack, Quantity - 1, Price);
    }
}

Note that the new concept - SnackPile - is a value object. It’s immutable (we don’t mutate the existing instances but rather create new ones using the SubtractOne method) and it can be treated interchangeably. The same technique is applicable to the OrderLine entity: we can extract a ProductPile value object out of it.

This is an example of the guideline I wrote about previously: we should always try to move as much logic from entities to value objects as possible. The Slot entity here acts as a thin wrapper on top of the value object. The only thing it does is carries an identity - the Position property. All the actual work is delegated to the SnackPile class. It’s a quite powerful technique as it allows us to significantly simplify the domain model.

Summary

It’s not always clear how to categorize classes that are internal to an aggregate. It might be tempting to attribute them to value objects as they don’t have an obvious identity that is seen from the outside world. However, they do possess a special kind of it - Local Identity. That is an identity which is meaningful only within the aggregate’s boundaries.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus