Aggregate

8 min read

In its simplest form, an Aggregate is a collection of Entities and Value Objects. They are not just scattered, living their own life - they are owned by one particular Entity called an Aggregate Root (AG). What is it exactly? For this moment let me call it a “facade” which manages underlying objects, their state, consistency and behaviours. All these managed Entities and Value Objects are invisible to other Aggregates, they are composition of the specific Aggregate Root when considered in terms of UML. Some people also call them Child Entities (CE), so it will be called by me.

Deciding What Is What

I’m almost sure that the first question which came into your mind was when given Entity is root one or not. If you did so, it is perfectly fine. Concepts presented in this article probably won’t be clear at first glance, don’t worry. Thankfully, the decision process can be break down into making a few decisions.

This Is An Aggregate Root

First and foremost, an Aggregate Root manages some Child Entities. Let’s take a Cart and Cart Product in that matter. Adding and removing Products will take place only via Cart Entity, recalculation of total value and some other invariants are “black-boxed” somewhere in behaviour methods. One point for Aggregate Root.

Another indicator of AG is getting an Entity by its Identifier. In most cases this is a clear sign to make the Entity an AG. When talking about an Identifier we cannot forget that some other Aggregates may reference our Entity. If this is true, it’s another check on our list of root symptoms. Generally, an AG is an independent unit, which logic is not an interest of other Entities, all calculations and state changes are encapsulated within our Aggregate Root Entity.

This Is Child Entity

In most cases, a Child Entity is when there is no use for it to live without its managing Entity. Is there any reason for Cart Product to exist when there is no Cart? Surely not, because it wouldn’t make any sense. Or list of applied Promotions. Since there is no place for user to put his purchased products, there is no place for promotions either.

Dealing With State Change

During application lifecycle you will surely perform some operation altering values of Aggregate Root together with its Child Entities. Values will have its content changed, Lists will get smaller or bigger. Such operations may include all CE what leads to the question: “how to orchestrate and validate all invariants?”. There are few options.

Via Aggregate Root

All calculations and alterations are done inside called method. Advantage of such an approach is that you have a total control over transaction, and you can enforce all invariants in single place. Also, you work in an AG context, what allows you to run some other methods in the scope.

Via Child Entity

In this situation we allow a Child Entity to decide about itself. AG method which is calling its Child methods have no idea what exactly happen there, it only requires some kind of confirmation, that the state was changed, and it contains a valid state.

Via On-demand Calculation

Sometimes we need to check some business rules before we can perform state change. In our Cart example we have an option to add a particular product to it. Inside add_product() method we check if the quantity of Cart Products is not bigger than 50. If so, we do not allow addition of another product until one from the List is not removed.

Choosing Right Approach

If you asked me: “what option should I use?”, I would tell you: it depends. It’s not like some methods above are better that others, it all depends on the use case you have. I’m pretty sure that in most cases all of them will be a valid solution for a problem you will face. Let’s see how it may relate to the code.

Product is a separate Aggregate Root, that’s why Cart Product keeps the reference to it by ValueObject containing Product’s identifier. This helps to prevent fetching one AG inside another by design. Another advantage is that ORM will not eagerly join another Entities what would cost your system increased data throughput, network, memory and processor time.

How To Make A Good Aggregate

You probably still wonder what are design rules for a good Aggregate. Well, nothing actually was written in stone, so we cannot tell about rules, but rather tips or good practices when it comes to making Aggregates. All these guidelines are a logic consequence of applying DDD into your project.

The Smaller, The Better

I think it’s a common sense, that everything what is small and simple is easier to manage than big and complex. It’s the same when it comes to Aggregates, the smaller responsibility and related data, the less effort when it comes to enforce business rules or to deal with many Children.

Persist In Small Chunks

The good system design will result in one DBMS (Database Management System) transaction per changes done inside AG. If there are other AGs involved in the whole operation, they might to be informed via the Domain Events about what exactly have happened. More on Domain Events in the next article. Such event may be dispatched locally, implementing Observer design pattern or may be sent via network to some remote querying system and then be received by some other process.

Another aspect connected with persistence is a problem called race condition. Imagine a situation when the user of your system runs the application on two devices, one on the laptop using web browser, second on the dedicated app on a smartphone. The user’s action will be to simultaneously fire HTTP request which will exchange loyalty points to some real products. The backend application responsible for this functionality is not well-designed, so it will be exploited by our user. How?

User Aggregate is fetched together with Bonus Points quantity which is stored in numeric property. If user has collected enough of them, they will be exchanged for some products. After sending a request from his mobile device (R1), user initiated the same operation from his laptop (R2). When R2 started being processed by application’s controller, R1 had an Aggregate loaded to the memory and all Bonus Point became a new SD cards reader. The problem is, it wasn’t persisted yet and R2 has just loaded the same Aggregate the R1 did. Now R2 has the old data, where points exchanged a few milliseconds ago are there again, what gives our user another memory card reader and some serious head-ache to the business owner.

Keep Aggregates Funky Fresh

The bad news is, there is no way to keep Aggregates consistent in some programmable manner. Creating a local copy of data fetched from DBMS means that this data must be considered obsolete at the moment it was asked for. But do we have to persist the whole Aggregate at once via ORM? Not necessarily, we can utilize our DBMS’s native calls/queries to ensure we are working with the latest version of an Aggregate. Did I say version?

The pattern we are going to implement is called an optimistic locking. Unlike pessimistic locking, it doesn’t require blocking whole record, what may result in making given data row unavailable indefinitely. Sure, handling such issues with DBMS solely is comfortable, but this case requires special treatment.

In order to start versioning our Cart, we need an additional property in its class which will be called a version. Of course, it’s weak we have to have our Entity polluted with persistence-related data, but this is still better than unintentionally give away some merchandise.

Class above gives versioning abilities to Cart Entity. Now let’s go back to our backend-based API and fix it, so further exploiting of the system will be impossible.

Once again, R1 is dispatched, it hits an API endpoint and fetches Aggregate which now has a version equal to 1. During operations on an Aggregate in R1 context, R2 has also reached an API, fetched data with version also equal to 1. In the same time, R1 has successfully exchanged bonus points for a product, meaning transaction was committed. How R1 knew that it is safe to persist?

Most DBMS will return a number of rows which where affected during an UPDATE. If returned number is equal to 1, it means that no process have changed this particular record. Also, version was bumped to 2, so when R2 will try to update, it will receive information that no rows were affected. It must lead to fetching an Aggregate data once again, but this time we have an updated version number and data itself, so DBMS update should be successful this time. Having solution like this for race conditions also requires some smart way to manage retries.

Summary

Now you know how helpful can Aggregates be. They are small, manageable and transaction-safe constructs, which helps you to organize code and apply complex business rules with ease. In the next post we will learn how Aggregates communicates with each other, even when they have no idea about their existence.