Skip to content

i. ✨Aggregates

CØDE N!NJΔ edited this page Jul 27, 2025 · 6 revisions

💫 Aggregate Root Framework

Domain-Driven Design (DDD) introduces several key tactical and strategic core concepts to help model complex business domains effectively.

Entities

Entities are objects that have a distinct identity that persists over time, even as their attributes change. They are defined by their identity rather than their attributes. For example, a Customer entity might have an ID of 12345 - even if the customer changes their name, address, or email, they remain the same customer because their identity remains constant.

Key characteristics:

  • Have a unique identifier (ID)
  • Mutable - their attributes can change
  • Identity equality - two entities are equal if they have the same ID
  • Have a lifecycle - they can be created, modified, and potentially deleted

Value Objects

Value objects represent concepts that are defined entirely by their attributes rather than having a distinct identity. They are immutable and interchangeable with other value objects that have the same values. A classic example is Money - two $20 bills are equivalent regardless of their serial numbers, or an Address where "123 Main St, New York, NY 10001" is the same wherever it appears.

Key characteristics:

  • No identity - defined by their attributes
  • Immutable - cannot change once created
  • Structural equality - two value objects are equal if all their attributes are equal
  • Side-effect free behavior - operations don't modify the object but return new instances

Aggregate Root

An aggregate is a cluster of related entities and value objects that are treated as a single unit for data consistency. The aggregate root is the only entity within the aggregate that external objects can hold references to. It acts as the gatekeeper, ensuring that all business rules and invariants within the aggregate are maintained.

For example, an Order aggregate might contain OrderItem entities and ShippingAddress value objects. External code can only access the order items through the Order aggregate root, not directly.

Key characteristics:

  • Single entry point for accessing the aggregate
  • Enforces business invariants across the entire aggregate
  • Controls the lifecycle of entities within the aggregate
  • Only aggregate roots can be retrieved from repositories
  • Changes to the aggregate are committed as a single transaction

Bounded Context

A bounded context is a strategic design pattern that defines explicit boundaries within which a particular domain model applies. It's essentially a conceptual boundary where specific terms, definitions, and rules have consistent meaning. Different bounded contexts can have different models for the same real-world concept.

For example, a Customer in a Sales context might focus on purchase history and preferences, while a Customer in a Support context might emphasize ticket history and service level agreements. These are different models of the same concept, each valid within its bounded context.

Key characteristics:

  • Provides explicit boundaries for domain models
  • Ensures ubiquitous language consistency within the boundary
  • Allows different models of the same concept in different contexts
  • Helps manage complexity in large systems
  • Guides team organization and service boundaries

How They Work Together

These concepts work together to create a cohesive domain model:

  1. Bounded contexts define the overall boundaries and scope
  2. Aggregate roots organize entities and value objects into consistency boundaries
  3. Entities represent important business objects with identity
  4. Value objects capture important concepts without identity

This layered approach helps manage complexity by providing clear boundaries, consistent language, and well-defined responsibility areas within your domain model.

💫 SourceFlow: How to Define an Aggregate

Aggregate Definition

To define a root aggregate you need to derive from Aggrgate<TEntity> base class. The TEntity is the root entity that is represented by the Aggregate. Example: Suppose when we need to define a root aggregate that can encapsulate the business functions or behaviour applicable to a bank account entity. Then we first need to define the BankAccount entity that represents the domain object that stores the state of the account. To define an Entity we need to derive from IEntity interface.

 public class BankAccount : IEntity
 {
     public int Id { get; set; }
     public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
     public string AccountName { get; set; } = string.Empty;
     public decimal Balance { get; set; }
     public bool IsClosed { get; set; }
     public string ClosureReason { get; internal set; }
     public DateTime ActiveOn { get; internal set; }
 }

Once you define the root entity, you can proceed to implement the Aggregate class to define the business operation that could change the state of the aggregate entity. To define the Bank Acount root aggregate. You need to implement from Aggregate<BankAccount base class.

    public class AccountAggregate : Aggregate<BankAccount>
    {
        public void CreateAccount(int accountId, string holder, decimal amount)
        {
            Send(new CreateAccount(new Payload
            {
                Id = accountId,
                AccountName = holder,
                InitialAmount = amount
            }));
        }

        public void Deposit(int accountId, decimal amount)
        {
            Send(new DepositMoney(new TransactPayload
            {
                Id = accountId,
                Amount = amount,
                Type = TransactionType.Deposit
            }));
        }

        public void Withdraw(int accountId, decimal amount)
        {
            Send(new WithdrawMoney(new TransactPayload
            {
                Id = accountId,
                Amount = amount,
                Type = TransactionType.Withdrawal
            }));
        }

        public void Close(int accountId, string reason)
        {
            Send(new CloseAccount(new ClosurePayload
            {
                Id = accountId,
                ClosureReason = reason
            }));
        }
    }
}

Important: To instantiate an Aggregate, you need to use the Create<TAggregate>() method on IAggregateFactory. You need to inject the factory interface in your service and call the create method to get the desired aggregate (TAggregate).

public class AccountService : IAccountService
{
    private IAggregateFactory aggregateFactory;
    public AccountService (IAggregateFactory aggregateFactory)=> this.aggregateFactory = aggregateFactory;

    public async Task<int> CreateAccountAsync(string accountHolderName, decimal initialBalance)
    {
        if (string.IsNullOrEmpty(accountHolderName))
            throw new ArgumentException("Account create requires account holder name.", nameof(accountHolderName));

        if (initialBalance <= 0)
            throw new ArgumentException("Account create requires initial amount.", nameof(initialBalance));

        var account = await aggregateFactory.Create<AccountAggregate>();
        if (account == null)
            throw new InvalidOperationException("Failed to create account aggregate");

        var accountId = new Random().Next(); // Simulating a unique account ID generation

        account.CreateAccount(accountId, accountHolderName, initialBalance);

        return accountId;
    }
  }

Commands

Each operation on the root aggregate is performed by sending a command. A command is an implementation of Command<TPayload> base class with TPayload that contains the data required to perform the business operation. The command when handled could change the state of the aggregate entity. In the above aggregate, the business operations for create account, deposit and withdrawal of money, closure of account are performed by raising relevant commands with appropriate payloads.

public abstract class Command<TPayload> : ICommand
    where TPayload : class, IPayload, new()
{
    public Command(TPayload payload)
    {
        Metadata = new Metadata();
        Name = GetType().Name;
        Payload = payload;
    }

    public string Name { get; set; }
    public Metadata Metadata { get; set; }    
    public TPayload Payload { get; set; }
}

Example: CreateAccount Command.

public class CreateAccount : Command<AccountPayload>
{
    public CreateAccount(AccountPayload payload) : base(payload)
    {
    }
}

Domain Events

An aggregate can subscribe to domain events raised in the system. An Event is an Implementation of Event<TPayload> base class. The payload is basically the encapsulated entity (instance of IEntity) affected by the event.

To subscribe to an event, the aggregate needs to implement ISubscribes<TEvent> interface.

Example: AccountAggregate below.

 public class AccountAggregate : Aggregate<BankAccount>,
                                 ISubscribes<AccountCreated>

 {
    public Task Handle(AccountCreated @event)
    {
      return Send(new ActivateAccount(new ActivationPayload
      {
         Id = @event.Payload.Id,
         ActiveOn = DateTime.UtcNow,
      }));
    }
 }

Command Stream

The sequence of commands raised by the aggregate are stored in the command store. The Aggregate could choose to replay the stream of commands.

Note: You need to provide the implementation of ICommandStore interface to manage the stream of commends.

public class AccountAggregate : Aggregate<BankAccount>
{
    public void Replay(int AggregateId)
    {
        base.Replay(AggregateId);
    }
}
Clone this wiki locally