@Embeddable
public record Payment(BigDecimal value) {
}
@Embeddable
public record OrderItemName(String value) {
public OrderItemName {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
}
}
@Embeddable
@AttributeOverrides({
@AttributeOverride(name = "name.value", column = @Column(name = "name")),
@AttributeOverride(name = "amount.value", column = @Column(name = "amount"))
})
public record OrderItem(UUID id, @Embedded OrderItemName name, @Embedded OrderItemPaymentAmount amount) {
}
@Entity
@Table(name = "orders")
@AllArgsConstructor
@NoArgsConstructor
@Accessors(fluent = true)
@AttributeOverrides({
@AttributeOverride(name = "payment.value", column = @Column(name = "payment"))
})
public class Order {
@Id
private UUID id;
@Embedded
private Payment payment;
@ElementCollection
@CollectionTable(name = "order_items", joinColumns = @JoinColumn(name = "order_id"))
private List<OrderItem> orderItems;
}
Storing Value Objects in Postgres: A Tactical DDD Pattern Approach
Clean Persistence with Tactical Domain-Driven Design
👋 Introduction
You may know the pain, or maybe you don’t. Domain-Driven Design (DDD) is an approach that models software with a strong alignment to the business domain. In addition to strategic design, which involves defining bounded contexts—such as splitting an e-commerce shop into domains like order, inventory, and customer management—there is also tactical design. While strategic design operates on a higher level, tactical design patterns can be applied when constructing our domain model with technical resources, helping to enrich the domain model.
So, what are Value Objects, and why should we use them? I posted a note on this topic a while back:
Value Objects don’t have a conceptual identity and are immutable, so we can only reason about them within a given context. They also give you additional type safety, making it easier to validate your types. For people familiar with Value Objects, it makes a codebase easier to understand—at least for me. The whole structure of your code reflects the domain you're modeling, and you can clearly see that. On the flip side, you end up with more classes and a higher development effort, so it’s important to consider the trade-offs. For a simple CRUD app, I wouldn’t bother with Value Objects or a Ports & Adapters architecture. But if you’re dealing with a highly complex domain, it’s something I’d consider.
ℹ️ Info
Also, just to clarify, Value Objects here refer to a design pattern. There’s also a Java JEP for Value Objects, but that’s about how certain types are stored in memory—a completely different topic.
🎨 Design
We’re using Java records because they provide immutability and automatically generate equals
and hashCode
based on the values. Plus, writing validations when you construct them is pretty straightforward.
So, what’s the problem with Value Objects when it comes to storing them in a database? Well, they’re essentially objects that wrap a value, which means they’re not atomic for storage in a relational database and break the First Normal Form (1NF). We also have to differentiate between simple Value Objects that hold just one value and more complex ones, like a Location
object that has multiple values. How you store and convert them will differ based on that complexity.
We’ll go through a simple example using an Order
entity that contains a collection of Item
Value Objects and a Payment
Value Object.
🛠️ Implementation
So, how do we get the database to handle this? This article focuses on Spring Data JPA with Hibernate, and we’ll use PostgreSQL as the database, but the techniques we discuss can be applied to any JPA-compliant database. Also, we’ll specifically focus on Java Records because, in my opinion, they’re the best fit for Value Objects in Java.
For simple Value Objects, just annotate the type with @Embeddable
and the field with @Embedded
. We’ll also add @AttributeOverrides
at the top of the class to make sure we use meaningful column names like payment
instead of something generic like value
.
For more complex Value Objects, we need to add @AttributeOverrides
and @Embedded
on each field. Since we have a List
of items, we also use @ElementCollection
on the entity, as it maps to a new table. We could have used @OneToMany
for OrderItem
, but we designed OrderItem
not to be its own entity because it's tightly bound to the parent order—an OrderItem
doesn’t exist without an order. That’s why we chose @ElementCollection
.
From a performance perspective, there are trade-offs. Making OrderItem
its own entity gives you more flexibility but can lead to more complex joins and potentially slower performance. @ElementCollection
might offer better performance for small collections, but running queries could be slower for large collections due to fewer optimization opportunities. However, you can create indices manually using Flyway, giving you more flexibility in both cases.
🎯 Results
As you can see in the results, with this approach, we can store the values flat in the database while utilizing records for immutable objects.
The order items table also stores the values as we intended.
🧠 Keep in Mind
Reuse of Value Objects with Different Column Names: If you want to store the same Value Object with different column names, use
@AttributeOverride
when embedding the@Embeddable
in your entity to change the column names.Avoid Complex Hierarchies of Embeddables: It's generally a good idea to avoid complex hierarchies of
@Embeddable
classes. But if you need to use them, consider checking out@MappedSuperclass
.
🏁 Conclusion
With this approach, we can store value objects in our database according to the tactical DDD approach in a clean manner. While I would still prefer a more decoupled approach, where value objects and other domain objects can be created framework-agnostic, this offers a good tradeoff in my opinion. For MongoDB, it is straightforward to implement a listener with Spring Data that intercepts before storage and converts the objects as we desire. While Hibernate provides an AttributeConverter
, I have not found a generic way to apply this converter based on an implemented marker interface, for example. We would need to generate an AttributeConverter
per type and that is kind of tedious in my opinion. Therefore, I favor this approach.