dk.cloudcreate.essentials.components.eventsourced.aggregates.stateful.classic.AggregateRoot Maven / Gradle / Ivy
Show all versions of eventsourced-aggregates Show documentation
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dk.cloudcreate.essentials.components.eventsourced.aggregates.stateful.classic;
import dk.cloudcreate.essentials.components.eventsourced.aggregates.*;
import dk.cloudcreate.essentials.components.eventsourced.aggregates.stateful.*;
import dk.cloudcreate.essentials.components.eventsourced.aggregates.stateful.classic.state.AggregateRootWithState;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.EventStore;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.eventstream.AggregateEventStream;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.types.EventOrder;
import dk.cloudcreate.essentials.shared.reflection.invocation.*;
import dk.cloudcreate.essentials.shared.types.GenericType;
import java.util.*;
import java.util.stream.Stream;
import static dk.cloudcreate.essentials.shared.FailFast.*;
import static dk.cloudcreate.essentials.shared.MessageFormatter.msg;
/**
* A specialized and opinionated mutable {@link Aggregate} design
* This {@link AggregateRoot} is designed to work with Class based Event's that inherit from {@link Event}.
* This design is deliberate and will manage a lot of things for you as a developer at the cost of some flexibility.
* Specifically you only have to supply the Aggregate ID, through {@link Event#aggregateId()}, on the FIRST/initial {@link Event}
* that's being applied to the {@link AggregateRoot} using the {@link #apply(Event)} method.
* Every consecutive {@link Event} applied will automatically have its {@link Event#aggregateId(Object)} method called IF it doesn't already have a value.
* I.e. you can be lazy and skip setting the aggregate id on the Event if you don't want to.
* The {@link AggregateRoot} also automatically keeps track of the {@link Event#eventOrder()} value and will set it for you and ensure that it's consecutively growing.
*
* Note: The {@link AggregateRoot} works best in combination with the {@link StatefulAggregateRepository}
*
* You don't have to use the {@link AggregateRoot} IF it doesn't fit your purpose. It's always possible to build your own {@link Aggregate} concept.
*
* @param the aggregate id type
* @param the type of event
* @param the aggregate self type (i.e. your concrete aggregate type)
* @see AggregateRootWithState
*/
public abstract class AggregateRoot, AGGREGATE_TYPE extends AggregateRoot> implements StatefulAggregate {
private transient PatternMatchingMethodInvoker> invoker;
private ID aggregateId;
private List uncommittedChanges;
/**
* Zero based event order
*/
private EventOrder eventOrderOfLastAppliedEvent = EventOrder.NO_EVENTS_PREVIOUSLY_PERSISTED;
private boolean hasBeenRehydrated;
private boolean isRehydrating;
private EventOrder eventOrderOfLastRehydratedEvent = EventOrder.NO_EVENTS_PREVIOUSLY_PERSISTED;
public AggregateRoot() {
initialize();
}
/**
* Initialize the aggregate, e.g. setting up state objects, {@link PatternMatchingMethodInvoker}, etc.
*/
protected void initialize() {
invoker = new PatternMatchingMethodInvoker<>(this,
new SingleArgumentAnnotatedMethodPatternMatcher<>(EventHandler.class,
new GenericType<>() {
}),
InvocationStrategy.InvokeMostSpecificTypeMatched);
}
/**
* Effectively performs a leftFold over all the previously persisted events related to this aggregate instance
*
* @param persistedEvents the previous persisted events related to this aggregate instance, aka. the aggregates history
* @return the same aggregate instance (self)
*/
@SuppressWarnings("unchecked")
@Override
public AGGREGATE_TYPE rehydrate(AggregateEventStream persistedEvents) {
requireNonNull(persistedEvents, "You must provide a persistedEvents stream");
return rehydrate(persistedEvents.map(persistedEvent -> persistedEvent.event().deserialize()));
}
/**
* Effectively performs a leftFold over all the previous events related to this aggregate instance
*
* @param previousEvents the previous events related to this aggregate instance, aka. the aggregates history
* @return the same aggregate instance (self)
*/
@SuppressWarnings("unchecked")
public AGGREGATE_TYPE rehydrate(Stream previousEvents) {
requireNonNull(previousEvents, "You must provide a previousEvents stream");
isRehydrating = true;
previousEvents.forEach(event -> {
if (aggregateId == null) {
// The aggregate doesn't know its aggregate id, hence the FIRST historic event being applied MUST know it
aggregateId = event.aggregateId();
requireNonNull(aggregateId, msg("The first previous/historic Event '{}' applied to Aggregate '{}' didn't contain an aggregateId",
event.getClass().getName(),
this.getClass().getName()));
}
applyEventToTheAggregateState(event);
eventOrderOfLastAppliedEvent = event.eventOrder();
});
eventOrderOfLastRehydratedEvent = eventOrderOfLastAppliedEvent;
isRehydrating = false;
hasBeenRehydrated = true;
return (AGGREGATE_TYPE) this;
}
@Override
public EventOrder eventOrderOfLastRehydratedEvent() {
return eventOrderOfLastRehydratedEvent;
}
/**
* Apply a new non persisted/uncommitted Event to this aggregate instance.
* If it is the very FIRST {@link Event} that is being applied then {@link Event#aggregateId()} MUST return the ID of the aggregate the event relates to
* Every consecutive {@link Event} applied will have its {@link Event#aggregateId(Object)} method called IF it doesn't already have a value. I.e. you can be lazy and skip setting the aggregate id
* on the Event if you don't want to.
* The {@link AggregateRoot} automatically keeps track of the {@link Event#eventOrder()} value and will set it for you and ensure that it's growing consecutively.
*
* @param event the event to apply
*/
protected void apply(EVENT_TYPE event) {
requireNonNull(event, "You must supply an event");
ID eventAggregateId = event.aggregateId();
if (this.aggregateId == null) {
// The aggregate doesn't know its aggregate id, hence the FIRST event being applied MUST know it
this.aggregateId = eventAggregateId;
if (this.aggregateId == null) {
throw new InitialEventIsMissingAggregateIdException(msg("The first Event '{}' applied to Aggregate '{}' didn't contain an aggregateId",
event.getClass().getName(),
this.getClass().getName()));
}
}
if (eventAggregateId == null) {
event.aggregateId(aggregateId());
} else {
requireTrue(Objects.equals(eventAggregateId, this.aggregateId), msg("Aggregate Id's do not match! Cannot apply Event '{}' with aggregateId '{}' to Aggregate '{}' with aggregateId '{}'",
event.getClass().getName(),
eventAggregateId,
this.getClass().getName(),
this.aggregateId));
}
var nextEventOrderToBeApplied = eventOrderOfLastAppliedEvent().increment();
event.eventOrder(nextEventOrderToBeApplied);
applyEventToTheAggregateState(event);
eventOrderOfLastAppliedEvent = nextEventOrderToBeApplied;
_uncommittedChanges().add(event);
}
@Override
public ID aggregateId() {
requireNonNull(aggregateId, "The aggregate id has not been set on the AggregateRoot and not supplied using one of the Event applied to it. At least the first event MUST supply it");
return aggregateId;
}
/**
* Has {@link #rehydrate(Stream)} or {@link #rehydrate(AggregateEventStream)} been used
*/
public boolean hasBeenRehydrated() {
return hasBeenRehydrated;
}
/**
* Is the event being supplied to {@link #applyEventToTheAggregateState(Event)} a previously persisted (i.e. a historic event)
*/
protected final boolean isRehydrating() {
return isRehydrating;
}
/**
* 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
* {@link EventHandler}
*
* @param event the event to apply to the aggregate
* @see #isRehydrating()
*/
protected void applyEventToTheAggregateState(Event event) {
if (invoker == null) {
// Instance was created by Objenesis
initialize();
}
invoker.invoke(event, unmatchedEvent -> {
// Ignore unmatched events as Aggregates don't necessarily need handle every event
});
}
/**
* Get the {@link Event#eventOrder() of the last {}@link Event} that was applied to the {@link AggregateRoot}
* (either using {@link #rehydrate(AggregateEventStream)}/{@link #rehydrate(Stream)} or using {@link #apply(Event)}
*
* @return the event order of the last applied {@link Event} or {@link EventOrder#NO_EVENTS_PREVIOUSLY_PERSISTED} in case no
* events has ever been applied to the aggregate
*/
public EventOrder eventOrderOfLastAppliedEvent() {
if (eventOrderOfLastAppliedEvent == null) {
// Since the aggregate instance MAY have been created using Objenesis (which doesn't
// initialize fields nor call a constructor) we have to be defensive and lazy way initialize
// the eventOrderOfLastAppliedEvent
eventOrderOfLastAppliedEvent = EventOrder.NO_EVENTS_PREVIOUSLY_PERSISTED;
}
return eventOrderOfLastAppliedEvent;
}
/**
* The the events that have been applied to this aggregate instance but not yet persisted to
* the underlying {@link EventStore}
*/
@Override
public EventsToPersist getUncommittedChanges() {
return new EventsToPersist<>(aggregateId,
eventOrderOfLastRehydratedEvent,
_uncommittedChanges());
}
/**
* Resets the {@link #getUncommittedChanges()} - effectively marking them as having been persisted
* and committed to the underlying {@link EventStore}
*/
@Override
public void markChangesAsCommitted() {
uncommittedChanges = new ArrayList<>();
eventOrderOfLastRehydratedEvent = null;
}
/**
* Since the aggregate instance MAY have been created using Objenesis (which doesn't
* initialize fields nor call a constructor) we have to be defensive and lazy way to initialize the list
*
* @return the initialized uncommitted changes
*/
private List _uncommittedChanges() {
if (uncommittedChanges == null) {
uncommittedChanges = new ArrayList<>();
}
return uncommittedChanges;
}
}