This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Domain

Building blocks for your domain model.

1 - Aggregate

Aggregate: consistency boundaries

Concept

Aggregate is probably the most important tactical pattern in Domain-Driven Design. It is a building block of the domain model. An Aggregate is a model on its own, a model of a particular business objects, which can be uniquely identified and by that distinguished from any other object of the same kind.

When handling a command, you need to ensure it only changes the state of a single aggregate. An aggregate boundary is a transaction boundary, so the state transition for the aggregate needs to happen entirely or not at all.

Traditionally, DDD defines three concepts, which are related to aggregate:

  • Entity - a representation of a business object, which has an identifier
  • Aggregate Root - an entity, which might aggregate other entities and value objects
  • Aggregate - the Aggregate Root and all the things inside it

The idea of an aggregate, which holds more than one entity, seems to be derived from the technical concerns of persisting the state. You can imagine an aggregate root type called Booking (for a hotel room), which holds a collection of ExtraService entities. Each of those entities represent a single extra service ordered by the guest when they made this booking. It could be a room service late at night, a baby cot, anything else that the guest needs to order in advance. Since those extra services might be also cancelled, we need to have a way to uniquely identify each of them inside the Booking aggregate, so those are entities.

If we decide to persist the Booking state in a relational database, the natural choice would be to have one table for Booking and one table for ExtraService with one-to-many relationship. Still, when loading the Booking state, we load the whole aggregate, so we have to read from the Booking table with inner join on the ExtraService table.

Those entities might also have behaviour, but to reach out to an entity within an aggregate, you go through the aggregate root (Booking). For example, to cancel the baby cot service, we’d have code like this:

var booking = bookingRepository.Load(bookingId);
booking.CancelExtraService(extraServiceId);
bookingRepository.Save(booking);

In the Booking code it would expand to:

void CancelExtraService(ExtraServiceId id) {
    extraServices.RemoveAll(x => x.Id == id);
    RecalculateTotal();
}

So, we have an entity here, but it doesn’t really expose any behaviour. Even if it does, you first call the aggregate root logic, which finds the entity, and then routes the call to the entity.

In Eventuous, we consider it as a burden. If you need to find the entity in the aggregate root logic, why can’t you also execute the operation logic right away? If you want to keep the entity logic separated, you can always create a module with a pure function, which takes the entity state and returns an event to the aggregate root.

The relational database persistence concern doesn’t exist in Event Sourcing world. Therefore, we decided not to implement concepts like Entity and AggregateRoot. Instead, we provide a single abstraction for the logical and physical transaction boundary, which is the Aggregate.

Implementation

Eventuous provides three abstract classes for the Aggregate pattern, which are all event-sourced. The reason to have three and not one is that all of them allow you to implement the pattern differently. You can choose the one you prefer.

Aggregate

The Aggregate abstract class is quite technical and provides very little out of the box.

Member Kind What it’s for
Original Read-only collection Events that were loaded from the aggregate stream
Changes Read-only collection Events, which represent new state changes, get added here
Current Read-only collection The collection of the historical and new events
ClearChanges Method Clears the changes collection
OriginalVersion Property, int Original aggregate version at which it was loaded from the store
Version Property, int Current aggregate version after new events were applied
AddChange Method Adds an event to the list of changes

It also has two helpful methods, which aren’t related to Event Sourcing:

  • EnsureExists - throws if Version is -1
  • EnsureDoesntExist - throws if Version is not -1

All other members are methods. You either need to implement them, or use one of the derived classes (see below).

Member What it’s for
Load Given the list of previously stored events, restores the aggregate state. Normally, it’s used for synchronous load, when all the stored events come from event store at once.
Fold Applies a single state transition event to the aggregate state and increases the version. Normally, it’s used for asynchronous loads, when events come from event store one by one.

When building an application, you’d not need to use the Aggregate abstract class as-is. You still might want to use it to implement some advanced scenarios.

Aggregate with state

Inherited from Aggregate, the Aggregate<T> adds a separate concept of the aggregate state. Traditionally, we consider state as part of the aggregate. However, state is the only part of the aggregate that mutated. We decided to separate state from the behaviour by splitting them into two distinct objects.

The aggregate state in Eventuous is immutable. When applying an event to it, we get a new state.

The stateful aggregate class implements most of the abstract members of the original Aggregate. It exposes an API, which allows you to use the stateful aggregate base class directly.

Member Kind What it’s for
Apply Method Given a domain event, applies it to the state. Replaces the current state with the new version. Adds the event to the list of changes. Returns a tuple with the previous and the current state versions.
State Property Returns the current aggregate state.

As we don’t know how to extract the aggregate identity from this implementation, you still need to implement the GetId function.

The Apply function is virtual, so you can override it to add some contract-based checks (ensure if the state is valid, or the state transition was valid).

Aggregate state

We have an abstraction for the aggregate state. It might seem unnecessary, but it has a single abstract method, which you need to implement for your own state classes. As mentioned previously, we separated the aggregate behaviour from its state. Moving along, we consider event-based state transitions as part of the state handling. Therefore, the state objects needs to expose an API to receive events and produce a new instance of itself (remember that the state is immutable).

To support state immutability, AggregateState is an abstract record, not class. Therefore, it supports immutability out of the box and supports with syntax to make state transitions easier.

A record, which inherits from AggregateState needs to implement a single function called When. It gets an event as an argument and returns the new state instance. There are two ways to define how events mutate the state, described below.

Using pattern matching

Using pattern matching, you can define how events mutate the state with functions that return the new AggregateState instance.

For example:

record BookingState : AggregateState<BookingState> {
    decimal Price { get; init; }

    public override BookingState When(object @event)
        => @event switch {
            RoomBooked booked        => this with { Price = booked.Price },
            BookingImported imported => this with { Price = booked.Price },
            _                        => this
        };
}
Using explicit handlers

You can also use explicit event handlers, where you define one function per event, and register them in the constructor. In that case, there’s no need to override the When function.

The syntax is similar to registered command handlers for the application service:

public record BookingState : AggregateState<BookingState, BookingId> {
    public BookingState() {
        On<RoomBooked>((state, booked) => state with { Price = booked.Price });
        On<BookingImported>((state, booked) => state with { Price = booked.Price });
        On<BookingPaymentRegistered>(
            (state, paid) => state with {
                PaymentRecords = state.PaymentRecords.Add(
                    new PaymentRecord(paid.PaymentId, paid.AmountPaid)
                ),
                AmountPaid = paid.FullPaidAmount
            }
        );
    }

    decimal Price          { get; init; }
    decimal AmountPaid     { get; init; }

    ImmutableList<PaymentRecord> PaymentRecords { get; init; } =
        ImmutableList<PaymentRecord>.Empty;
}

The default branch of the switch expression returns the current instance as it received an unknown event. You might decide to throw an exception there.

Aggregate identity

Use the AggregateId abstract record, which needs a string value for its constructor:

record BookingId(string Value) : AggregateId(Value);

The abstract record overrides its ToString to return the string value as-is. It also has an implicit conversion operator, which allows you to use a string value without explicitly instantiating the identity record. However, we still recommend instantiating the identity explicitly to benefit from type safety.

The aggregate identity type is only used by the application service and for calculating the stream name for loading and saving events.

Aggregate factory

Eventuous needs to instantiate your aggregates when it loads them from the store. New instances are also created by the ApplicationService when handling a command that operates on a new aggregate. Normally, aggregate classes don’t have dependencies, so it is possible to instantiate one by calling its default constructor. However, you might need to have a dependency or two, like a domain service. We advise providing such dependencies when calling the aggregate function from the application service, as an argument. But it’s still possible to instruct Eventuous how to construct aggregates that don’t have a default parameterless constructor. That’s the purpose of the AggregateFactory and AggregateFactoryRegistry.

The AggregateFactory is a simple function:

public delegate T AggregateFactory<out T>() where T : Aggregate;

The registry allows you to add custom factory for a particular aggregate type. The registry itself is a singleton, accessible by AggregateFactoryRegistry.Instance. You can register your custom factory by using the CreateAggregateUsing<T> method of the registry:

AggregateFactoryRegistry.CreateAggregateUsing(() => new Booking(availabilityService));

By default, when there’s no custom factory registered in the registry for a particular aggregate type, Eventuous will create new aggregate instances by using reflections. It will only work when the aggregate class has a parameterless constructor (it’s provided by the Aggregate base class).

It’s not a requirement to use the default factory registry singleton. Both ApplicationService and AggregateStore have an optional parameter that allows you to provide the registry as a dependency. When not provided, the default instance will be used. If you use a custom registry, you can add it to the DI container as singleton.

Dependency injection

The aggregate factory can inject registered dependencies to aggregates when constructing them. For this to work, you need to tell Eventuous that the aggregate needs to be constructed using the container. To do so, use the AddAggregate<T> service collection extension:

builder.Services.AddAggregate<Booking>();
builder.Services.AddAggregate<Payment>(
    sp => new Payment(sp.GetRequiredService<PaymentProcessor>, otherService)
);

When that’s done, you also need to tell the host to use the registered factories:

app.UseAggregateFactory();

These extensions are available in the Eventuous.AspNetCore (DI extensions and IApplicationBuilder extensions) and Eventuous.AspNetCore.Web (IHost extensions).

2 - Domain events

Domain events: persisted behaviour

Concept

If you ever read the Blue Book, you’d notice that the Domain Event concept is not mentioned there. Still, years after the book was published, events have become popular, and domain events in particular.

Eric Evans, the author of the Blue Book, has added the definition to his Domain-Design Reference. Let us start with Eric’s definition:

Model information about activity in the domain as a series of discrete events. Represent each event as a domain object. […] A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state changes in the other model objects.

When talking about Event Sourcing, we focus on the last bit: “making explicit the events […], which are associated with state changes.” Event Sourcing takes this definition further, and suggests:

Persist the domain objects state as series of domain events. Each domain event represents an explicit state transition. Applying previously recorded events to a domain objects allows us to recover the current state of the object itself.

We can also cite an article from Medium (a bit controversial one):

In the past, the goto statement was widely used in programming languages, before the advent of procedures/functions. The goto statement simply allowed the program to jump to any part of the code during execution. This made it really hard for the developers to answer the question “how did I get to this point of execution?”. And yes, this has caused a large number of bugs. A very similar problem is happening nowadays. Only this time the difficult question is “how did I get to this state” instead of “how did I get to this point of execution”.

Event Sourcing effectively answers this question by giving you a history of all the state transitions for your domain objects, represented as domain events.

So, what this page is about? It doesn’t look like a conventional documentation page, does it? Nevertheless, let’s see how domain events look like when you build a system with Eventuous.

public static class BookingEvents {
    public record RoomBooked(
        string RoomId,
        LocalDate CheckIn,
        LocalDate CheckOut,
        decimal Price
    );

    public record BookingPaid(
        decimal AmountPaid,
        bool PaidInFull
    );

    public record BookingCancelled(string Reason);

    public record BookingImported(
        string RoomId,
        LocalDate CheckIn,
        LocalDate CheckOut
    );
}

Oh, that’s it? A record? Yes, a record. Domain events are property bags. Their only purpose is to convey the state transition using the language of your domain. Technically, a domain event should just be an object, which can be serialised and deserialized for the purpose of persistence.

Eventuous do’s and dont’s:

  • Do make sure your domain events can be serialised to a commonly understood format, like JSON.
  • Do make domain events immutable.
  • Do implement equality by value for domain events.
  • Don’t apply things like marker interfaces (or any interfaces) to domain events.
  • Don’t use constructor logic, which can prevent domain events from deserializing.
  • Don’t use value objects in your domain events.

The last point might require some elaboration. The Value Object pattern in DDD doesn’t only require for those objects to be immutable and implement equality by value. The main attribute of a value object is that it must be correct. It means that you can try instantiating a value object with invalid arguments, but it will deny them. This characteristic along forbids value objects from being used in domain events, as events must be unconditionally deserializable. No matter what logic your current domain model has, events from the past are equally valid today. By bringing value objects to domain events you make them prone to failure when their validity rules change, which might prevent them from being deserialized. As a result, your aggregates won’t be able to restore their state from previously persistent events and nothing will work.