Aggregates

An Aggregate is a cluster of domain objects that are treated as a single unit with regards to data changes.

The aggregate ensures that all the changes to the objects within the aggregate are performed in a consistent and transactional manner.

Key characteristics of an Aggregate are:

  1. Consistency Boundary: The aggregate defines a clear boundary for transactional consistency. All invariants and business rules within an aggregate must be satisfied as a whole, ensuring that the domain remains in a valid state at all times.

  2. Encapsulation: The internal structure and state of the aggregate's objects are hidden from the outside world. External entities can only interact with the aggregate through its root.

  3. Lifecycle: Aggregates are typically long-lived, persisting over multiple transactions. They should be designed with consideration for their lifecycle management.

  4. Isolation: Aggregates should not directly reference or hold references to other aggregates. Instead, they can reference other aggregates by their unique identifiers (IDs).

Eiffel.Modelling.Abstractions NuGet package contains base aggregate class. By using abstract Aggregate class you can define your aggregates.

/// <summary>
/// Aggregate implementation
/// </summary>
/// <typeparam name="TIdentity">The type of unique business identifier</typeparam>
/// <typeparam name="TKey">The type of database primary key</typeparam>
public abstract class Aggregate<TIdentity, TKey> : EntityBase<TKey>, IAggregate
   where TIdentity : ValueObject<TIdentity>
   where TKey : struct, IEquatable<TKey>
{
    /// <summary>
    /// Domain events collection
    /// </summary>
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.ToList();
    
    /// <summary>
    /// Business identifier
    /// </summary>
    public abstract TIdentity Identity { get; internal set; }
    
    private readonly List<IDomainEvent> _domainEvents;
    /// <summary>
    /// CTOR
    /// </summary>
    public Aggregate()
    {
        _domainEvents = new List<IDomainEvent>();
    }
    
    /// <summary>
    /// Raises/adds domain event
    /// </summary>
    /// <typeparam name="TEvent">The type of the class representing domain event</typeparam>
    /// <param name="domainEvent">Domain event</param>
    protected void Raise<TEvent>(TEvent domainEvent)
        where TEvent : IDomainEvent
    {
        _domainEvents.Add(domainEvent);
    }
    
    /// <summary>
    /// Clears all domain events
    /// </summary>
    public void ClearEvents()
    {
        _domainEvents.Clear();
    }
}

Each Aggregate is likely to contain more than one function and these functions are business needs demanded by domain experts/stakeholders. Each function usually requires notifying other members of the system that an event has occurred. These events are called Domain Events. Each Aggregate is responsible for keeping the events occurring within itself. Each Aggregate must have a uniquely defined identity type and primary key type required for the database. You can find the identity details specific to the aggregates in the Business Identifiers section. Example Booking Aggregate with BookingCancelled domain event.

public class Booking : Aggregate<BookingId, Guid>
{
    // Business identifier
    public override BookingId Identifier { get; private set; }

    // Status enum
    public BookingStatus Status { get; private set; }

    // ...
    // ...
    void Cancel(string reason)
    {
        Status = BookingStatus.Cancelled;
        Raise(new BookingCancelled(BookingId, reason));
    }
}

Last updated