Stack overflow issuing command to nested AggregateMember

Hello All,

I have run into a snag handling commands in nested AggregateMember instances. I am modeling a warehouse management system for an organization with many disparate campuses, each of which is comprised of one or more facilities and each facility can be sub-divided into multiple areas.

The code for the aggregate root and two entities follows.

Campus:

`

package com.myco.warehousing.command;

import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.commandhandling.model.AggregateMember;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.spring.stereotype.Aggregate;
import org.springframework.util.Assert;

import com.myco.warehousing.api.Address;
import com.myco.warehousing.api.CampusCreatedEvent;
import com.myco.warehousing.api.CreateCampusCommand;
import com.myco.warehousing.api.DescribeFacilityCommand;
import com.myco.warehousing.api.FacilityDescribedEvent;

@Aggregate(repository = “campusRepository”)
public class Campus implements Serializable {

private static final long serialVersionUID = -64868971771565457L;

@AggregateIdentifier(routingKey = “campusId”)
private String id;
private String name;
private Address address;

@AggregateMember
private Map<String, Facility> facilitiesById;
private Map<String, Facility> facilitiesByCode;

Campus() {
}

String id() {
return id;
}

String name() {
return name;
}

Address address() {
return address;
}

Map<String, Facility> facilitiesById() {
return facilitiesById == null ? Collections.emptyMap() : facilitiesById;
}

Map<String, Facility> facilitiesByCode() {
return facilitiesByCode == null ? Collections.emptyMap() : facilitiesByCode;
}

@CommandHandler
Campus(CreateCampusCommand command) {
Assert.notNull(command, “null command”);
Assert.hasText(command.getCampusId(), “null/blank campus identifier”);
Assert.hasText(command.getCampusName(), “null/blank campus name”);
Assert.notNull(command.getCampusAddress(), “null campus address”);

apply(new CampusCreatedEvent(command.getCampusId(), command.getCampusName(), command.getCampusAddress()));
}

@CommandHandler
void handle(DescribeFacilityCommand command) {
Assert.notNull(command, “null command”);
Assert.hasText(command.getCampusId(), “null/blank campus identifier”);
Assert.hasText(command.getFacilityId(), “null/blank facility identifier”);
Assert.hasText(command.getFacilityName(), “null/blank facility name”);
Assert.hasText(command.getFacilityCode(), “null/blank facility code”);
Assert.notNull(command.getFacilityAddress(), “null facility address”);

Assert.state(!facilitiesById().containsKey(command.getFacilityId()),
String.format("Facility with matching id already defined: %s -> %s ", command.getFacilityId(),
facilitiesById().get(command.getFacilityId())));

String facilityCode = command.getFacilityCode().trim().toUpperCase();
Assert.state(!facilitiesByCode().containsKey(facilityCode), String.format(
"Facility with matching code already defined: %s -> %s ", facilityCode, facilitiesByCode().get(facilityCode)));

apply(new FacilityDescribedEvent(command.getCampusId(), command.getFacilityId(), command.getFacilityName(),
facilityCode, command.getFacilityAddress()));
}

@EventSourcingHandler
void on(CampusCreatedEvent event) {
this.id = event.getCampusId();
this.name = event.getCampusName();
this.address = event.getCampusAddress();
}

@EventSourcingHandler
void on(FacilityDescribedEvent event) {
if (facilitiesById == null) {
facilitiesById = new HashMap<>();
facilitiesByCode = new HashMap<>();
}

Facility facility = new Facility(event.getFacilityId(), event.getFacilityName(), event.getFacilityCode(),
event.getFacilityAddress());
facilitiesById.put(event.getFacilityId(), facility);
facilitiesByCode.put(event.getFacilityCode(), facility);
}
}

`

Facility:

`

package com.myco.warehousing.command;

import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateMember;
import org.axonframework.commandhandling.model.EntityId;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.springframework.util.Assert;

import com.myco.warehousing.api.Address;
import com.myco.warehousing.api.AreaDescribedEvent;
import com.myco.warehousing.api.DescribeAreaCommand;

public class Facility {
public static enum Type {
BUILDING(“Building”), LOT(“Lot”), HANGAR(“Hangar”);

private String label;

private Type(String label) {
Assert.hasText(label, “null/blank label”);
this.label = label;
}

public String getLabel() {
return label;
}
}

@EntityId(routingKey = “facilityId”)
private String id;
private String name;
private String code;
private Address address;

@AggregateMember()
private Map<String, Area> areasById;
private Map<String, Area> areasByCode;

Facility(String id, String name, String code, Address address) {
this.id = id;
this.name = name;
this.code = code;
this.address = address;
}

String id() {
return id;
}

String name() {
return name;
}

String code() {
return code;
}

Address address() {
return address;
}

Map<String, Area> areasById() {
return areasById == null ? Collections.emptyMap() : areasById;
}

Map<String, Area> areasByCode() {
return areasByCode == null ? Collections.emptyMap() : areasByCode;
}

@CommandHandler
void handle(DescribeAreaCommand command) {

Assert.notNull(command, “null command”);
Assert.hasText(command.getCampusId(), “null/blank campus identifier”);
Assert.hasText(command.getFacilityId(), “null/blank facility identifier”);
Assert.hasText(command.getAreaId(), “null/blank area identifier”);
Assert.hasText(command.getAreaName(), “null/blank area name”);
Assert.hasText(command.getAreaCode(), “null/blank area code”);

Area area = areasById().get(command.getAreaId());
Assert.state(area == null,
String.format(“Area with matching id already defined: %s -> %s”, command.getAreaId(), area));

String areaCode = command.getAreaCode().trim().toUpperCase();
area = areasByCode().get(areaCode);
Assert.state(area == null, String.format(“Area with matching code already defined: %s -> %s”, areaCode, area));

apply(new AreaDescribedEvent(command.getCampusId(), command.getFacilityId(), command.getAreaId(),
command.getAreaName(), areaCode));
}

@EventSourcingHandler
void on(AreaDescribedEvent event) {
if (areasById == null) {
areasById = new HashMap<>();
areasByCode = new HashMap<>();
}
Area area = new Area(event.getAreaId(), event.getAreaName(), event.getAreaCode());
areasById.put(event.getAreaId(), area);
areasByCode.put(event.getAreaCode(), area);
}

@Override
public String toString() {
return “Facility [id=” + id() + “, name=” + name() + “, code=” + code() + “, address=” + address() + “, areas=”

  • areasById().values().stream().map(a -> a.toString()).collect(Collectors.joining(", ", “[”, “]”)) + “]”;
    }

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}

/**

  • NOTE: this method is ONLY supports comparisons relative to the parent aggregate root (the owning {@link Campus}).
  • The behavior of comparing two {@link Facility} instances from disparate {@link Campus} instances is undefined.

I found the issue. It had to do with the fact that axon publishes all events of a given type to all entities that handle the type, irrespective of the the routing key value.

So the problematic event handler is modified as shown in red:

`

@EventSourcingHandler
void on(SubAreaDescribedEvent event) {

// Short circuit when event is NOT intended for this specific Area instance.
if (!id().equals(event.getAreaId())) return;

if (subAreasById == null) {
subAreasById = new HashMap<>();
subAreasByCode = new HashMap<>();
}
Area area = new Area(event.getSubAreaId(), event.getSubAreaName(), event.getSubAreaCode());
subAreasById.put(event.getSubAreaId(), area);
subAreasByCode.put(event.getSubAreaCode(), area);
}

`

Hi Troy,

didn’t even get a proper chance to look at it :wink: Glad you found it.
Note that since 3.1, you can configure whether you want this behavior or not.
See https://github.com/AxonFramework/AxonFramework/pull/436

Basically, you can configure it on the @AggregateMember annotation. By default, events are forwarded to all entities. You can also choose to only forward to entities whose ID (annotated with @EntityId) matches a certain property on the incoming events.

Hope this helps.
Cheers,

Allard

Thanks! I updated the entity annotations to: @AggregateMember(eventForwardingMode=ForwardMatchingInstances.class) and verified that it works as expected!

Cheers,

Troy

So now I have an issue when I try to create a sub-area inside a sub-area (refer to the diagram on the first post in this thread, area’s can be nested to any depth). The test posted earlier now succeeds, but the following test fails:

`

@Test
public void testDescribeSubSubArea() {

String campusId = IdentifierFactory.getInstance().generateIdentifier();
String campusName = “Some Campus”;
String facilityId = IdentifierFactory.getInstance().generateIdentifier();
String facilityName = “Facility 1”;
String facilityCode = “FAC1”;
String street = “1234 XYZ Street”;
String city = “SLC”;
String state = “UT”;
String zip = “84105”;
String country = “US”;
String areaId = IdentifierFactory.getInstance().generateIdentifier();
String areaName = “Call Back Cage”;
String areaCode = “CBC”;
String area2Id = IdentifierFactory.getInstance().generateIdentifier();
String area2Name = “Vault”;
String area2Code = “VLT”;
String subAreaP1Id = IdentifierFactory.getInstance().generateIdentifier();
String subAreaP1Name = “Secured Area”;
String subAreaP1Code = “CBCS”;
String subAreaId = IdentifierFactory.getInstance().generateIdentifier();
String subAreaName = “Hot Area”;
String subAreaCode = “HOT”;

long aggregateVersion = 4;

// @formatter:off
fixture
.given(
new CampusCreatedEvent(
campusId,
campusName,
new Address(street, city, state, zip, country)),
new FacilityDescribedEvent(
campusId,
facilityId,
facilityName,
facilityCode,
new Address(street, city, state, zip, country)),
new AreaDescribedEvent(
campusId,
facilityId,
areaId,
areaName,
areaCode),
new AreaDescribedEvent(
campusId,
facilityId,
area2Id,
area2Name,
area2Code),
new SubAreaDescribedEvent(
campusId,
facilityId,
areaId,
subAreaP1Id,
subAreaP1Name,
subAreaP1Code))
.when(
new DescribeSubAreaCommand(
campusId,
aggregateVersion,
facilityId,
subAreaP1Id,
subAreaId,
subAreaName,
subAreaCode))
.expectEvents(
new SubAreaDescribedEvent(
campusId,
facilityId,
subAreaP1Id,
subAreaId,
subAreaName,
subAreaCode));
// @formatter:on
}

`

Invoking this test produces the following stack trace:
`

org.axonframework.test.AxonAssertionError: The published events do not match the expected events

Expected Actual
com.myco.warehousing.api.SubAreaDescribedEvent < >

A probable cause for the wrong chain of events is an exception that occurred while handling the command.
java.lang.IllegalStateException: Aggregate cannot handle this command, as there is no entity instance to forward it to.
at org.axonframework.commandhandling.model.inspection.ChildForwardingCommandMessageHandlingMember.handle(ChildForwardingCommandMessageHandlingMember.java:95)
at org.axonframework.commandhandling.model.inspection.ChildForwardingCommandMessageHandlingMember.handle(ChildForwardingCommandMessageHandlingMember.java:98)
at org.axonframework.commandhandling.model.inspection.AnnotatedAggregate.lambda$handle$3(AnnotatedAggregate.java:220)
at org.axonframework.commandhandling.model.AggregateLifecycle.executeWithResult(AggregateLifecycle.java:166)
at org.axonframework.commandhandling.model.inspection.AnnotatedAggregate.handle(AnnotatedAggregate.java:218)
at org.axonframework.commandhandling.model.LockAwareAggregate.handle(LockAwareAggregate.java:82)
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:195)
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:189)
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:151)
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:43)
at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:57)
at org.axonframework.test.aggregate.AggregateTestFixture$AggregateRegisteringInterceptor.handle(AggregateTestFixture.java:571)
at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:55)
at org.axonframework.messaging.unitofwork.DefaultUnitOfWork.executeWithResult(DefaultUnitOfWork.java:69)
at org.axonframework.commandhandling.SimpleCommandBus.handle(SimpleCommandBus.java:156)
at org.axonframework.commandhandling.SimpleCommandBus.doDispatch(SimpleCommandBus.java:127)
at org.axonframework.commandhandling.SimpleCommandBus.dispatch(SimpleCommandBus.java:91)
at org.axonframework.test.aggregate.AggregateTestFixture.when(AggregateTestFixture.java:260)
at org.axonframework.test.aggregate.AggregateTestFixture.when(AggregateTestFixture.java:249)
at com.myco.warehousing.command.CampusTest.testDescribeSubSubArea(CampusTest.java:279)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

at com.myco.warehousing.command.CampusTest.testDescribeSubSubArea(CampusTest.java:288)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

`

Any ideas?

Troy

Hi Troy,

I believe you have hit a limitation of the automatic forwarding of commands to specific entities in an aggregate. It doesn’t really support recurring, because it can’t find the route to a specific command handler. That mechanism is currently evaluated statically on startup.

Good news is that you don’t need that mechanism to get the stuff done. In your case, you probably need the entites that wraps the top-level area to handle the command and recursively look up the area to invoke a method on it.

Hope this helps.
Cheers,

Allard

Thanks Allard, I had figured it was something along those lines and I can see how to proceed.

Cheers,

Troy Hart