Persistence Strategies for Aggregates at DDD Europe 2025
Yesterday I had the pleasure of hosting a hands-on workshop titled "Persistence strategies for aggregates: exploring the trade-offs" at the Domain Driven Design Europe Conference 2025 in Antwerp, Belgium. It was a fantastic session without any slides in a packed room, filled with engaging discussions and insightful contributions from the participants.
The core of the workshop revolved around a fundamental concept in Domain-Driven Design: Aggregates. Specifically, we explored how to effectively persist these building blocks while adhering to one of their foundational principle – that from the outside, only the Root Entity of an aggregate may be referenced.
We looked into six different approaches for persisting aggregates, examining their advantages and disadvantages of each. The overall goal of the workshop was to enable the participants to navigate trade-off decisions in this area because I firmly believe that there are no one-size-fits-all silver bullets out there. For five of these strategies, I showcased practical code examples, which you can find in a dedicated GitHub repository. After each explanation and demonstration, the floor was open for group discussions, where attendees shared their experiences and distilled their collective impressions and opinions into the pros and cons for each approach. We gathered them for four approaches on flipcharts.
It's important to note that the trade-offs discussed below represent the distilled opinion of the group of people who attended the workshop. This collaborative effort truly highlighted a key message: there is no silver bullet. Each persistence strategy comes with its own set of pros and cons, and the optimal choice heavily depends on the specific context and requirements of your bounded context. It’s perfectly acceptable, and often advisable, for different bounded contexts to adopt different persistence approaches.
Let's dive into the strategies we explored:
1. Memento Pattern
The Memento Pattern is a design pattern that allows an object's state to be saved and restored at a later time without violating its encapsulation. In the context of persistence, a memento object captures the aggregate's state and is then persisted, allowing the aggregate to be recreated from this saved state.
Pros:
High Decoupling from database: The aggregate itself doesn't need to know anything about the persistence mechanism.
Immutability in Java: Memento objects can be designed as immutable, which aligns well with functional programming principles and can lead to more robust code.
Very Straight Forward: The concept of saving and restoring a snapshot of an object's state is generally easy to understand and implement.
Consistency Boundary is kept: The aggregate's internal invariants are protected as the memento represents a consistent snapshot.
Cons:
No Database immutability (compared to Event Sourcing): While the Memento object itself might be immutable, the database record representing it is typically mutable, unlike the append-only nature of an event store.
Lot's of mapping and boilerplate code: Creating and managing memento objects and mapping them to and from the aggregate's internal state can involve significant manual effort. GenAI may speed things up but especially for complex aggregates this may become a burden.
Read-Only Model could be misused in services: If the memento is exposed too broadly, there's a risk that services might incorrectly use it to gain access to internals which leads to a higher degree of coupling and undermines that the Aggregate’s root entity should act like a Facade.
Lack of versioning: Evolving the structure of the aggregate or its memento can be challenging without a built-in versioning mechanism, making historical queries or migrations difficult.
2. Aggregate annotated with JPA Annotations or XML Mapping
This approach leverages Object-Relational Mapping (ORM) frameworks like JPA (Java Persistence API) to map aggregate roots and their internal structure directly to relational database tables. Annotations or XML configurations define how Java objects relate to database schemas.
Pros:
Framework and IDE support: Modern ORM frameworks come with extensive tool support, including IDE integrations, scaffolding, and query builders.
No manual mapping: The ORM handles the tedious work of mapping object fields to database columns.
No additional classes for the persistence part: The domain objects themselves are often annotated, reducing the number of separate persistence-specific classes.
Easy to grasp for developers: Developers familiar with relational databases and object-oriented programming often find this approach intuitive.
Quick and easy for simple aggregates: For aggregates with straightforward structures, this can be a very productive way to get started.
Cons:
Tight Coupling to the database: The aggregate's design can become intertwined with the database schema, making schema changes more impactful on the domain model.
JPA Lifecycle mixed with domain lifecycle: JPA's lifecycle management (e.g., entity states, flushing) can bleed into the domain logic, leading to unexpected behavior.
If aggregate stays detached outside of repository there is a risk of violating the consistency boundary: Once an aggregate is loaded and detached, changes made outside the repository's control can lead to inconsistencies when persisting.
No technology free domain model: technical and domain aspects mix, domain purity is gone: The presence of JPA annotations directly on domain classes introduces technical concerns into the core domain model, compromising its purity.
Invasiveness of JPA (flushing behavior, lazy loading), risk of JPA behaviors leaking into domain code: Specific JPA behaviors like automatic flushing or lazy loading can influence how domain logic is written, potentially leading to performance issues or subtle bugs.
3. Document Database / Mongo DB (in my example)
This strategy involves persisting the entire aggregate as a single document in a document-oriented database like MongoDB. The aggregate's state is serialized into a format like JSON or BSON and stored as a self-contained unit.
Pros:
No explicit schema: Document databases are often schema-less, offering flexibility in evolving the data model without rigid migrations.
Easy to apply: Storing an entire aggregate as a document is conceptually straightforward.
Domain Model stays pure (free of persistence related concerns): The aggregate doesn't need to contain any database-specific annotations or code.
Easy to store complex aggregates and maintain the complete state of the aggregate: Nested structures within an aggregate map naturally to nested documents, simplifying persistence.
Consistency of aggregate is protected at any stage: By storing the entire aggregate as a single document, atomicity of updates is often guaranteed by the database itself, ensuring consistency.
Cons:
More difficult querying: While simple queries on the top-level document are easy, complex queries involving nested fields or relationships across documents can become challenging and less efficient.
Evolution of model is harder, versioning of aggregate versions may be required when the model evolves over time: As the aggregate's structure changes, handling older versions of documents can become complex, potentially requiring explicit versioning mechanisms within the documents.
Too easy to store complex objects (you lose an indicator that your aggregate grows too complex): The ease of storing complex, nested documents can mask an aggregate that is becoming too large or encompassing too much responsibility, which can lead to performance issues or reduced maintainability.
4. Event Sourced Aggregate
Event Sourcing is a persistence pattern where every change to the state of an application is captured as a sequence of immutable events. Instead of storing the current state, we store the stream of events that led to the current state. The aggregate's current state is then derived by replaying these events.
Pros:
Acts as a time machine: history is preserved, you can easily go back in time: The immutable event log provides a complete audit trail and allows for easy reconstruction of past states.
Very clean aggregate design: Aggregates often become simpler, focusing on emitting events rather than managing complex state transitions internally.
Feels very natural: Thinking in terms of events that have happened can align well with how business processes naturally unfold.
Very scalable: Event stores are often optimized for append-only operations, which can be highly scalable.
Someone joked: "makes you look smart": This approach is often seen as advanced and can demonstrate a deep understanding of architectural patterns… we all had a fair amount of laughter about that one. Please mind the irony and sarcasm here.
Cons:
Comes with a learning curve: Event Sourcing is a paradigm shift and requires developers to adopt a new way of thinking about persistence and state.
Has an impact on the modeling of the aggregates: Aggregates need to be designed to emit events, which can influence their internal structure and behavior.
More complex code: While the aggregate design can be cleaner, the overall system can become more complex due to the need for event stores, event processors, and potentially projections.
You may need CQRS because querying is hard: Replaying events for every query is inefficient. Often, Command Query Responsibility Segregation (CQRS) is used to create read-optimized models (projections) from the event stream.
You may need to mind versioning of your events if the aggregate evolves: As your domain model evolves, the structure of your events might change, requiring careful handling of older event versions during replay or projection.
Needs more space in the database: Storing a full history of events can consume significantly more storage than just storing the current state. While this is true, we all agreed that this should not be a huge show-stopper anymore in 2025.
5. Loosening the Aggregate's Contract by Exposing Internals with Getters to the Outside
This approach deviates from the strict aggregate boundary by allowing external code to access internal properties of the aggregate or its value objects directly via getters. While seemingly simple, it can have profound implications for consistency and maintainability.
Pros:
Feels straightforward to developers: Developers accustomed to traditional object-oriented programming might find this approach intuitive.
Very simple: The initial implementation might appear to be the quickest and easiest.
Cons:
Similar to Memento Pattern but more fine-grained and with a higher impact on the internal design (leaks into value objects): Unlike a memento that captures a snapshot, directly exposing getters creates a live dependency, potentially exposing internal details down to value objects.
Violation of original aggregate idea: This directly contradicts the principle that an aggregate's internals are encapsulated and only accessible via the root entity's commands.
Risk of getters being misused and higher coupling being the result: External code can directly read and then implicitly (or explicitly via setters, if present) modify the aggregate's state without going through the aggregate's defined behaviors, leading to inconsistent states and tight coupling.
Feels like a hack: This approach often feels like a shortcut that undermines the benefits of aggregate encapsulation.
6. Repository and Aggregate are in the same package but in different Maven modules
This approach attempts to provide a clean separation of concerns at a higher level (Maven modules) while leveraging Java's package-private visibility. The idea is that the repository, residing in a different module but the same package, can access package-private methods or fields of the aggregate, thus avoiding public exposure while maintaining separation. (Note: This specific example was discussed in the workshop but not demonstrated in the GitHub repository.)
Pros:
No impact on Aggregate Modeling, Design and Code at all: The aggregate itself can remain pure, free of any persistence-specific code or annotations.
Easy to access package private classes and attributes: Java's access modifiers facilitate this internal access without making fields public.
Cons:
THIS IS A HACK! The group was unanimous in labeling this approach as a hack due to its reliance on implicit conventions and brittle coupling.
Breaks easily with someone forgets about the package naming convention...errors are hard to debug: If a developer inadvertently changes the package name or moves classes, the "hidden" access mechanism breaks, leading to cryptic compilation or runtime errors.
Every developer must know about this implicit detail: This "architectural decision" is not explicit in the code or configuration and relies on tribal knowledge, making onboarding new team members or understanding the system more difficult.
Final remarks:
Hosting this workshop was an absolute blast for me. While I usually find myself discussing broader, less technical topics, diving deep into the code and exploring these fascinating persistence trade-offs was a truly refreshing and enjoyable experience which went back to my early days of conference presentations where I talked A LOT about Hibernate and Java persistence related topics.
My sincere thanks go out to the DDD Europe organizers for the opportunity, and a special shout-out to the local staff in Antwerp who set everything up so perfectly and professionally. Your dedication and love for all the small details made the workshop run incredibly smoothly!