The idea behind aggregate is to enforce consistency and invariants. An aggregate is a place where invariants are enforced and act as a consistency boundary. When one part of the aggregate is updated, other parts might also need to be updated to ensure consistency.
One misconception that I had while mapping concepts like aggregate in functional programming (FP) from object-oriented (OO) was to only think in terms of only because data and behavior always co-exist in OO. However, in FP you tend to keep your data and functions separate.
Ubiquitous language is not only the collection of nouns in any domain but also the verbs, processes and constraints. Nouns correspond to data structure and verbs correspond to operation in your domain. Identifying verbs was also an important part because it decides which operation should be in the domain.
The distinction between value types and entities in functional programming
Classical (read object oriented) implementation of DDD differentiates between value types and entity types, based on their mutability and notion of identity. Value types are immutable and do not convey enough information on their own, for example, color could be a value type where color type does not hold any meaning in itself but when attached to an entity like a shirt or a car (for example a red shirt or a black car) they mean something in the domain.
On the contrary, an entity has a lifecycle. These are the types that are mutable and go through changes through different lifecycle events. For instance, order could be an entity that goes through different lifecycle events such as item added to order or item removed from order. Each lifecycle event mutates the entity.
In functional programming, everything is immutable by default, which leads us to wrongly believe that we don’t need a distinction between value types and entities. But the concept of value and entity types are based on the lifecycle of the domain model and therefore can be equally applied in functional languages.
Modeling aggregates
When a software application grows, you might end up partitioning your database or using a distributed database, which essentially means that the entities/aggregates which used to live in the same machine now live on different machines. Any assumption about the location of an entity in the codebase may not be valid anymore; any attempt to update more than one entity in a single transaction will enter the shaky realm of distributed transactions. So to avoid these pitfalls follow these three rules.
The aggregate as transaction boundaries: Each aggregate serves as a transaction boundary. This uniquely identified aggregate is the scope of the transaction, do not try to put more than one aggregate in one single transaction scope, as you can’t guarantee the success of a transaction if those aggregates move to different machines.
Messages are addressed to aggregate: Whether you are building microservices or a monolith application, you should not make any assumption about where the other aggregate lives. Each aggregate talks to another aggregate by sending a message to its address — a unique identity of the aggregate.
Aggregate represents the disjoint set of data: Don’t let different aggregates share models because they look the same or it is convenient. Don’t build a persistence layer to connect these disjoint aggregates just because you can. Don’t create a library to share models from different aggregates because it’s DRY, instead go the extra mile, to make sure that these aggregates represent a disjoint set of data.
More functional patterns in domain driven design:
- Lenses to update aggregates: In functional programming, it could be annoying to update a deeply nested aggregate since data is immutable. This is where lenses come into play. Lens allows you to update a deeply nested value, and get the whole updated aggregate back.
- Use monoids to represent value objects: This document does a great job of explaining monoids in DDD context.
- Use property based testing for testing domain invariants.
- If you want to go fancy, Use reader monad for dependency injection.
- Keep side effects at the edge by following Imperative shell and functional core pattern or using free monads.
DDD design principles might appear to conflict with some functional programming good practices but it is an important tool to model complex business domains. I think the key is to understand the essence of the DDD patterns and then find an appropriate construct/abstraction to represent them.