Class AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE extends AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE>>

java.lang.Object
dk.cloudcreate.essentials.components.eventsourced.aggregates.stateful.modern.AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE>
Type Parameters:
ID - the type of id
EVENT_TYPE - the type of event
AGGREGATE_TYPE - the aggregate type
All Implemented Interfaces:
Aggregate<ID,AGGREGATE_TYPE>, StatefulAggregate<ID,EVENT_TYPE,AGGREGATE_TYPE>

public abstract class AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE extends AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE>> extends Object implements StatefulAggregate<ID,EVENT_TYPE,AGGREGATE_TYPE>
A modern opinionated interpretation of the classic AggregateRoot design, where the Event's are mutable.
The modern interpretation doesn't specify any requirement on the design of the Events, they can be Java 17+ records or simple POJO's.

Note: The AggregateRoot works best in combination with the StatefulAggregateRepository that's configured to use the StatefulAggregateInstanceFactory.reflectionBasedAggregateRootFactory(), because the AggregateRoot needs to be provided its aggregated id through the AggregateRoot(Object) constructor!

The modern AggregateRoot supports keeping the state projection and EventHandler annotated methods within the AggregateRoot instance or within an AggregateState instance.
If you wish to keep the state projection and EventHandler annotated methods within an AggregateState instance, then you only need to implement the WithState interface:

 public class Order extends AggregateRoot<OrderId, OrderEvent, Order> implements WithState<OrderId, OrderEvent, Order, OrderState> {
 public Order(OrderId orderId,
                  CustomerId orderingCustomerId,
                  int orderNumber) {
         super(orderId);
         requireNonNull(orderingCustomerId, "You must provide an orderingCustomerId");

         apply(new OrderEvent.OrderAdded(orderId,
                                         orderingCustomerId,
                                         orderNumber));
     }

     public void addProduct(ProductId productId, int quantity) {
         requireNonNull(productId, "You must provide a productId");
         if (state(OrderState.class).accepted) {
             throw new IllegalStateException("Order is already accepted");
         }
         apply(new OrderEvent.ProductAddedToOrder(aggregateId(),
                                                  productId,
                                                  quantity));
     }

     public void accept() {
         if (state(OrderState.class).accepted) {
             return;
         }
         apply(eventOrder -> new OrderEvent.OrderAccepted(aggregateId(),
                                                          eventOrder));
     }
 }
 
And state class:

 public class OrderState extends AggregateState<OrderId, OrderEvent, Order> {
     Map<ProductId, Integer> productAndQuantity;
     boolean                 accepted;

     @EventHandler
     private void on(OrderEvent.OrderAdded e) {
         productAndQuantity = new HashMap<>();
     }

     @EventHandler
     private void on(OrderEvent.ProductAddedToOrder e) {
         var existingQuantity = productAndQuantity.get(e.productId);
         productAndQuantity.put(e.productId, e.quantity + (existingQuantity != null ? existingQuantity : 0));
     }

     @EventHandler
     private void on(OrderEvent.ProductOrderQuantityAdjusted e) {
         productAndQuantity.put(e.productId, e.newQuantity);
     }

     @EventHandler
     private void on(OrderEvent.ProductRemovedFromOrder e) {
         productAndQuantity.remove(e.productId);
     }

     @EventHandler
     private void on(OrderEvent.OrderAccepted e) {
         accepted = true;
     }
 }
 
  • Constructor Details

    • AggregateRoot

      protected AggregateRoot()
      Used for AggregateSnapshot deserialization
    • AggregateRoot

      public AggregateRoot(ID aggregateId)
      Used for rehydration
      Parameters:
      aggregateId - the id of the aggregate being initialized during the rehydration flow
  • Method Details

    • aggregateId

      public ID aggregateId()
      Description copied from interface: Aggregate
      The id of the aggregate (aka. the stream-id)
      Specified by:
      aggregateId in interface Aggregate<ID,EVENT_TYPE>
    • getUncommittedChanges

      public EventsToPersist<ID,EVENT_TYPE> getUncommittedChanges()
      Description copied from interface: StatefulAggregate
      Query any changes to the Aggregate, i.e. Events applied as the result of calling command methods on the aggregate instance,
      Specified by:
      getUncommittedChanges in interface StatefulAggregate<ID,EVENT_TYPE,AGGREGATE_TYPE extends AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE>>
      Returns:
      the changes to the aggregate
    • markChangesAsCommitted

      public void markChangesAsCommitted()
      Resets the getUncommittedChanges() - effectively marking them as having been persisted and committed to the underlying EventStore
      Specified by:
      markChangesAsCommitted in interface StatefulAggregate<ID,EVENT_TYPE,AGGREGATE_TYPE extends AggregateRoot<ID,EVENT_TYPE,AGGREGATE_TYPE>>
    • hasBeenRehydrated

      public boolean hasBeenRehydrated()
      Description copied from interface: Aggregate
      Has the aggregate been initialized using previously recorded/persisted events (aka. historic events) using the Aggregate.rehydrate(AggregateEventStream) method
      Specified by:
      hasBeenRehydrated in interface Aggregate<ID,EVENT_TYPE>
    • state

      protected <STATE extends AggregateState<ID, EVENT_TYPE, AGGREGATE_TYPE>> STATE state(Class<STATE> stateClass)
      Variant of the state() where the concrete State type is specified in the argument
      If the aggregate implements WithState then you can call this method to access the state object
      Type Parameters:
      STATE - the type of state object
      Returns:
      the state object
      Throws:
      AggregateException - if the aggregate doesn't implement WithState
    • state

      protected <STATE extends AggregateState<ID, EVENT_TYPE, AGGREGATE_TYPE>> STATE state()
      If the aggregate implements WithState then you can call this method to access the state object
      This method requires that the return type information is available at compile time:
      OrderState state = state();
      If this isn't the case, then you can use state(Class):
      
           public void accept() {
               if (state(OrderState.class).accepted) {
                   return;
               }
               apply(eventOrder -> new OrderEvent.OrderAccepted(aggregateId(),
                                                                eventOrder));
           }

      Alternatively you can override the state() within your concrete Aggregate class and use Java's feature of overriding methods being allowed to define a covariant return type:
      
       public class Order extends AggregateRoot<OrderId, OrderEvent, Order> implements WithState<OrderId, OrderEvent, Order, OrderState> {
           protected OrderState state() {
               return super.state();
           }
       }
       
      Type Parameters:
      STATE - the type of state object
      Returns:
      the state object
      Throws:
      AggregateException - if the aggregate doesn't implement WithState
    • initialize

      protected void initialize()
      Initialize the aggregate, e.g. setting up state objects, PatternMatchingMethodInvoker, etc.
    • resolveStateImplementationClass

      protected Class<?> resolveStateImplementationClass()
      Override this method to provide a non reflection based look up of the Type Argument provided to the AggregateRoot class
      Returns:
      the AggregateState implementation to use for the state instance
    • rehydrate

      public AGGREGATE_TYPE rehydrate(AggregateEventStream<ID> persistedEvents)
      Effectively performs a leftFold over all the previously persisted events related to this aggregate instance
      Specified by:
      rehydrate in interface Aggregate<ID,EVENT_TYPE>
      Parameters:
      persistedEvents - the previous persisted events related to this aggregate instance, aka. the aggregates history
      Returns:
      the same aggregate instance (self)
    • eventOrderOfLastRehydratedEvent

      public EventOrder eventOrderOfLastRehydratedEvent()
      Description copied from interface: Aggregate
      Get the eventOrder of the last event during aggregate hydration (using the Aggregate.rehydrate(AggregateEventStream) method)
      Specified by:
      eventOrderOfLastRehydratedEvent in interface Aggregate<ID,EVENT_TYPE>
      Returns:
      the event order of the last applied Event or EventOrder.NO_EVENTS_PERSISTED in case no events has ever been applied to the aggregate
    • rehydrate

      public AGGREGATE_TYPE rehydrate(Stream<EVENT_TYPE> persistedEvents)
      Effectively performs a leftFold over all the previous events related to this aggregate instance
      Parameters:
      persistedEvents - the previous events related to this aggregate instance, aka. the aggregates history
      Returns:
      the same aggregate instance (self)
    • apply

      protected void apply(Function<EventOrder,EVENT_TYPE> eventSupplier)
      Apply a new non persisted/uncommitted Event to this aggregate instance. If you don't need to store the EventOrder for the event being applied, then you can just call the apply(Object) function instead.
      Parameters:
      eventSupplier - function that as argument/input receives the EventOrder for the next event to be applied (in case you want to store the EventOrder with the event being produced by the eventSupplier
      The result of the event supplier will be forwarded to the apply(Object) method.
    • eventOrderOfLastAppliedEvent

      public EventOrder eventOrderOfLastAppliedEvent()
    • apply

      protected void apply(EVENT_TYPE event)
      Apply a new non persisted/uncommitted Event to this aggregate instance.
      Parameters:
      event - the event to apply to the state of the aggregate
    • applyEventToTheAggregateState

      protected void applyEventToTheAggregateState(Object event)
      Apply the event to the aggregate instance to reflect the event as a state change to the aggregate
      The default implementation will automatically call any (private) methods annotated with EventHandler
      Parameters:
      event - the event to apply to the aggregate
      See Also:
    • isRehydrating

      protected final boolean isRehydrating()
      Is the event being supplied to applyEventToTheAggregateState(Object) a previously persisted event (i.e. a historic event)