Home > Posts Entity aggregates

Max Heinritz

Entity aggregates

An entity aggregate is a set of tightly coupled entities that are updated transactionally. One entity within the aggregate is considered the “aggregate root”. For example, an aggregate may comprise ReceivableInvoice and ReceivableInvoiceLineItem entities, where the former is the root of that aggregate.

Concept from DDD

The concept comes from domain-driven design, described in the DDD reference:

It is difficult to guarantee the consistency of changes to objects in a model with complex associations. Objects are supposed to maintain their own internal consistent state, but they can be blind-sided by changes in other objects that are conceptually constituent parts. Cautious database locking schemes cause multiple users to interfere pointlessly with each other and can make a system unusable. Similar issues arise when distributing objects among multiple servers or designing asynchronous transactions.

Therefore:

Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate, and allow external objects to hold references to the root only (references to internal members passed out for use within a single operation only). Define properties and invariants for the aggregate as a whole and give enforcement responsibility to the root or some designated framework mechanism.

Use the same aggregate boundaries to govern transactions and distribution.

Within an aggregate boundary, apply consistency rules synchronously. Across boundaries, handle updates asynchronously.

Eric Evans later clarified his perspective – that when it comes to defining aggregate roots the “external reference holding” is less important than the transaction boundary. For example, it’s not a problem for external systems to hold references to line items even though the payable invoice is the aggregate root.

Examples

Ways in which data can be grouped

How we do we decide which data to consider part of a single entity? How do we decide which entities to group together into single aggregate root? It’s a matter of judgment and satisfying business requirements. There are various dimensions along which data can be grouped, described below.

Database table (or Prisma model)

Typically a single entity is stored in a single database table. For example, a Tenant entity and all its data can be stored in a tenant database table.

But sometimes a single entity is stored in multiple tables. For example, data for a User entity could be stored in the user table as well as role data in a “supplementary” user_role table. The mapping between database tables and entities is done through mappers in the application layer such as dbUserToDto.

Whether to split a single entity into multiple database tables is an engineering implementation consideration. For any given entity, all data could be shoved into JSONB columns on one table if we wanted. Benefits of separate tables: stricter schema, uniqueness constraints, easier analytics access.

Database tables should not be queried directly from other domains. Generally service class APIs should be used instead.

Prisma schema file

Using Prisma, we can define Prisma models in separate .prisma files sprinkled throughout the codebase. Generally it is best practice to keep these files narrowly focused on a single domain. Prisma just release preliminary official support for split schemas a few weeks ago, but we’ve been doing it for years with custom tooling called prisma-fuse.

prisma-fuse allows foreign key constraints between models within the same file but not across models in different files, and unlike the official support, it allows colocating Prisma files with domain directories throughout the codebase.

Models that are not aggregates can be part of the same schema file. This allows using foreign key constraints with cascading deletes for related models.

Entity

An entity has:

Aggregate

An aggregate has:

Over the REST API, the full aggregate is returned by default when the root entity is fetched.

All data within the aggregate can be queried in full directly from other domains for batch data loading. Root entities like shipment and payable invoice can be queried as an “entity” in isolation, without fetching the whole aggregate.