Thanks, Allard
Hi Dmitry,
the goal in Axon 3 was to reduce coupling between the domain model and the framework even further. This is important feedback for us. However, the approach Axon 2 had was also very limiting in certain areas.
Just curious, haven’t you considered proxy generation approach, like used by JPA to wrap entities ?
Regarding the andThenApply(), we have been debating on whether we should have an “andThen” (either instead of, or next to the andThenApply()). In your case, it sounds like an andThen() would help you better, since you can decide for yourself, based on input howmany apply() calls you want to perform.
I’d probably vote for andThen(Runnable r), as more universal, especially that delayedTasks is already a queue of Runnables
The fact that apply() aren’t executed immediately inside an aggregate is an unfortunate result of using Constructors. We can’t get a reference to the instance until we have completed the constructor. Therefore, any messages passed to apply will only be invoked after completion of the constructor. The “andThen” and “andThenApply” code is executed after the constructor completes.
I think it is serious enough special case to at least mention in javadoc and reference guide.
It’s true that the static apply method puts some extra requirements on how the aggregate is being used outside of the scope of Axon. We didn’t see any possibility to remove this entirely, yet. If you have ideas, please don’t hesitate to share them.
You can either pass aggregate to apply() as a parameter or have some possiblity to access current lifecycle and set aggregateRoot - currently it’s not possible, static method getCurrent() is protected, and aggregateRoot field is private.
For me better alternative would be to let aggregate root to implement interface with default methods, something like an example below:
public interface Aggregate {
Serializable id();
default void apply(EventMessage<?> event) {
handle(event);
}
default void handle(EventMessage<?> event) {};
}
public interface AnnotatedAggregate extends Aggregate {
static Map<Class<? extends AnnotatedAggregate>, AggregateModel<? extends AnnotatedAggregate>> models = new HashMap<>();
@Override
@SuppressWarnings(“unchecked”)
default void apply(EventMessage<?> event) {
final AggregateModel metadata = (AggregateModel) getMetadata(getClass());
metadata.publish(event, this);
getEventBus().publish(event);
}
static void registerMetadata(Class aggregateClass, AggregateModel model) {
models.put(aggregateClass, model);
}
@SuppressWarnings(“unchecked”)
static AggregateModel getMetadata(Class aggregateClass) {
return (AggregateModel) models.get(aggregateClass);
}
default EventBus getEventBus() {
return CurrentEventBus.getInstance();
}
}
If I’m right, metadata should be static, that is per aggregate class, rather than per instance. Not sure about event bus, but getter above can support all cases: per instance, per context and singleton event bus, if overridden.
Could you also elaborate on how you feel this design “puts serious limitations on how domain logic is written”?
I meant callbacks you now need to create aggregate and access its methods. Any asynchronous callbacks make debugging and stack traces more complicated to read, can promote memory leaks if objects are captured in closures, only allow to use effectively final variables, and in general promote less OOP and less readable code, something DDD is fighting against.