Domain Driven Design: Storing Value Objects in a Spring Application with a Relational Database

If you find this article too long, there's a summary at the end of how to store value objects in a relational database using spring and jpa.

A short introduction on domain driven design and value objects

If you're not familiar with the concept, "Domain Driven Design: Tackling Complexity in the Heart of Software" is a popular software design book written by Eric Evans. In the book, he describes an approach to software design that places the project's primary focus on the core domain and domain logic, and basing design on a model of the business domain.

In this article, we will only talk about a small part (but a key part) of the book. Two of the basic building blocks of a domain model are Entities and Value Objects. I've talked about the difference between the two in a previous article, but the essential thing to know if this:

  • An entity has a lifecycle and an identity that extends beyond its set of attributes in a given model. Two entities with the same attributes are not necessarily the same entity (for example : two people with the same name are still two distinct people)
  • The identity of an entity is often captured in a special technical attribute (I call it "tid"), which is also the perfect candidate for a primary key in a relational database.
  • Value Objects, on the other hand, are completely defined by their attributes. If two Value Objects have the same attributes, you can assume they're the same and can be used interchangeably. A good example would be a amount of money, with a currency and a number. If you've got the same currency and value, then it's the same amount of money.

What's the problem with storing Value Objects ?

It seems that the standard way to persist something in a Spring + JPA application is put the @Entity annotation on top of your class and create an equivalent table in your database. As the name of the annotation suggest, that's a perfectly fine and natural thing to do for an entity, but it doesn't work that well for value objects.

  • First, obviously, it's confusing to have an annotation that says "@Entity" on a class that's exactly not an entity.
  • Having the @Entity annotation on a class forces you to have one or several @Id annotation on the class' attributes to define the primary key of the table. That's easy for an entity (there is the tid attribute) but that can be a problem for value objects. In the figure 1 below, for example, defining the "Ingredient" class as an @Entity forces you to add a "recipe" attribute to put the primary key and to be able to define an inverse @OneToMany relationship. But the recipe attribute is useless on the ingredient table (we get a recipe's ingredient, not the other way around. There's no getRecipe() method on the Ingredient class). This is an exemple of a corruption of the model because of the technical concern of storing your objects. This should be avoided as much as possible!
  • Value object are often the child object in the relationships between classes in your object model. But in a One To Many association, the value object will be the parent table. That's annoying because it forces you to use inverse relations and other confusing things in JPA.

+------------------+            +-------------+        +------------------+
|Recipe            |            |Ingredient   |        |Product           |
+------------------+            +-------------+        +------------------+
|getIngredients()  |----------->|getQuantity()|------->|getName()         |
|getSomeAttribute()|        1..*|getProduct() |      1 |getSomeAttribute()|
|addIngredient(i)  |            +------+------+        +------------------+
+------------------+                   |
                                       |
                                       V
                                 +-----------+
                                 |Quantity   |
                                 +-----------+

fig. 1: A recipe contains several ingredients. An ingredient has a quantity and a product. Recipe and Product are entities, Ingredient and Quantity are value objects.

@Entity is not the only way to persist objects with Spring and JPA.

First, let's do a quick reminder: You should design your object model first, and then model your relational shema so that it works with the object model. Not the other way around (in an ideal world). The object model is where the real value lies. If it's well designed, it's what makes your application maintenable, extensible, and what makes your developpers happy. If you object model is the same as the database schema, you're forced to do Object Oriented Programming on a model that's not really object oriented.

With that in mind, let's see how I could design a relational shema that works with the object model I showed in figure 1 (fig. 2).

+------------------------------------------------+    +-------------------------------------+ 
|                     recipes                    |    |             ingredients             |
+--------+----------------+----------------------+    +------------+----------+-------------+
| tid    | Some attribute | Some other attribute |    | recipe_tid | quantity | product_tid |
+------------------------------------------------+    +------------+----------+-------------+

+------------------------------------------------+
| products |
+--------+----------------+----------------------+
| tid | name | Some other attribute |
+------------------------------------------------+

fig. 2: The relational shema corresponding to the above object model. for the ingredient table, the primary key is a multi-column key on recipe_tid and product_tid

Two things to notice:

  • There is no table for the quantity class. You could do one, but it's not necessary as it's a one-to-one relationship with ingredient. The quantity will be stored as a string in the ingredient table.
  • There is a column recipe_tid in the ingredient table but no recipe attribute in the Ingredient class. That's because object models and relational databases don't work the same. This prevents us to use @Entity for the Ingredient class, because we can't put the @Id attribute correctly.

Now, here are how we are going to store our value objects:

  • For one-to-one relationships, we're going to store value objects in a serialized form. The serialized value object will be stored in a string column of the parent table. This is the example of the relation between Ingredient and Quantity.
  • For one-to-many relationships, we're going to use two annotations that are not as well-known as @Entity and @OneToMany : @Embeddable and @ElementsCollection. This is the exemple of the relationship between Recipe and Ingredient.

Here is the code (with only the relevant parts):


@Entity
public class Recipe {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long tid;

    @Column
    private String someAttribute;

    @ElementCollection(targetClass = Ingredient.class)
    @JoinTable(name = "ingredients")
    @JoinColumn(name = "recipe_tid", referencedColumnName = "tid")
    private Set ingredients;

    public void addIngredient(Ingredient ingredient) {
        this.ingredients.add(ingredient);
    }
}

The great thing here is the @ElementCollection annotation. with just some annotations, we have a functional collection and as you can see, we can simply add ingredients in it. then, just call recipeRepository.save() like you would normally do, and a row will automagically be added in the ingredients table. For the @ElementCollection to work, the tarket class must have the @Embeddable annotation, as seen below:


@Embeddable
public class Ingredient {

    private Quantity quantity;
    
    @ManyToOne
    @JoinColumn(name="product_tid")
    private Product product;
}

Simple! We have only what we wanted. Add some getters for the attributes and you're set. No useless "recipe" attribute, no @Id element that are annoying without a tid column, no confusing @Entity and inverse @ManyToOne. Just your domain object with minimal annotations for the persistence api. Note that you can still use @ManyToOne normally to link Ingredient with Product.

As for the quantity attribute, you need to provide a way to map it to a string column in your database. This is done with JPA's @Converter system. Here is the code of the converter:


@Converter(autoApply = true)
public class QuantityConverter implements AttributeConverter {

    @Override
    public String convertToDatabaseColumn(Quantity attribute) {
        return attribute.getValue()+"|"+attribute.getUnit();
    }

    @Override
    public Quantity convertToEntityAttribute(String dbData) {
        
        String dbValue = dbData.split("\\|")[0];
        String dbUnit = dbData.split("\\|")[1];
        
        Double value = Double.parseDouble(dbValue);
        
        return new Quantity(value, unit);
    }
}

With this class, your app will know how to convert a Quantity object into a String value, and vice-versa. with autoAppy=true, you don't have to put anything on the quantity attribute of the Ingredient class, but if this doesn't work you can use the annotation @Convert(converter=QuantityConverter.class). This converter is not part of the domain model, so it belongs in the infrastructure layer of your software. This is another advantage to this approach: the domain model stays clean of infrastructure code.

It is also possible to store value objects in several columns. In the ingredients/quantity example, I could have used two columns (for example quantity_value and quantity_unit) in the Ingredient table. Then, I would have used the @Embedded annotation on on the Quantity attribute instead of a converter, like this:


@Embeddable
public class Ingredient {
    
    @Embedded
    private Quantity quantity;
    
    @ManyToOne
    @JoinColumn(name="product_tid")
    private Product product;
}

@Embeddable
public class Quantity {
    @Column 
    private double value;
    @Column 
    private String unit;
}

This works just as well. The choice will depend on the size of the target value object, your personal preference, or other constraints (like an existing relational shema for example).

For reference, here is the code of the Product class. There is nothing special about it, it's just your usual Spring/JPA @Entity.


@Entity
public class Product {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long tid;
    
    @Column
    private String name;
    
    @Column
    private String someOtherAttribute;
}

Conclusion

I know i have a tendency to write long articles, but I like to provide the context that allows my readers to fully understand the problem and its solution. If it's too long, here is the tl;dr that can act as a nice conclusion/summary.

  • Design you object model first, and the the relational shema that works with it, avoiding any corruption of the object model.
  • For Value Objects with a one-to-one relationship with other objects, use a converter to fit your value object into a string (or other type) column. If you prefer (often for more complex objects), use the @Embed annotation.
  • For value Objects with @OneToMany annotations, use the @ElementCollection annotation with the @Embeddable annotation on the target value object.

    I hope my explanation was clear, and that the solution is satisfying. I've used this in several cases and it works very well. The problem of storing value object is in fact simpler than I initially thought, especially considering the number of articles and stackoverflow questions regarding this. I didn't discover the existence of @ElementCollection that easily in the beginning, so I hope this article will help some people designing better object model and relational databases.

This article is my 5th oldest. It is 1675 words long