Only the first aggregate member retrieve command

So I’m learning from this example Axon-trader/OrderBook.java at master · AxonFramework/Axon-trader · GitHub and I want to build something similar. Here’s what I did.

data class SomethingCommand(
  @TargetAggregateIdentifier
  val factoryId: String,
  val personId: String
)

data class SomethingEvent(
  val factoryId: String,
  val personId: String
)

data class Person(
  @EntityId
  val personId: String
) {
  @CommandHandler
  fun handle(cmd: SomethingCommand){
    // only the first or the top aggregate member retrieve this command
    AggregateLifecycle.apply(SomethingEvent(
      factoryId = cmd.factoryId,
      personId = cmd.personId
    ))
  }
}

data class CreateFactoryCommand(
  @TargetAggregateIdentifier
  val factoryId: String
)

data class FactoryCreatedEvent(
  val factoryId: String
)

@Aggregate
class FactoryAggregate() {

  @AggregateIdentifier
  lateinit var factoryId: String

  @CommandHandler
  constructor(cmd: CreateFactoryCommand): this () {
    AggregateLifecycle.apply(FactoryCreatedEvent(
      factoryId = cmd.factoryId
    ))
  }

  @EventSourcingHandler
  fun on(event: FactoryCreatedEvent){
    factoryId = event.factoryId
    // for testing purpose
    workers.add(Person(personId = "person1"))
    managers.add(Person(personId = "person2"))
  }

  // magic trick here if I move managers aggregate to the top of workers
  // now managers will be able to retrieve command while workers can't
  //  @AggregateMember
  //  val managers = mutableListOf<Person>()
  //  @AggregateMember
  //  val workers = mutableListOf<Person>()
  
  @AggregateMember
  val workers = mutableListOf<Person>()

  @AggregateMember
  val managers = mutableListOf<Person>()
  
}


class FactoryAggregateTest {
  lateinit var fixture: AggregateTestFixture<FactoryAggregate>

  @BeforeEach
  fun setUp(){
    val initCmd = CreateFactoryCommand(factoryId = "123")
    fixture = AggregateTestFixture(FactoryAggregate::class.java)
    fixture.givenCommands(initCmd)
  }

  @Test
  fun testSomethingCommand_ShouldHandleCommandNormally(){
    // only the person1 passed the test
    // with id person2 it throws exception
    // Aggregate cannot handle command [SomethingCommand], as there is no entity instance within the aggregate to forward it to.
    fixture
      .`when`(SomethingCommand(factoryId = "123", personId = "person2"))
      .expectEvents(SomethingEvent(
        factoryId = "123",
        personId = "person2"
      ))
  }
}

I’m not native speaker, but I will try to explain what I found. As you can see in the code, I tried to test command handling inside of aggregate member, but only the first or the top aggregate member will be able to retrieve and handle command. Let’s say workers is above the managers, now workers aggregate will be able to retrieve command while managers aggregate can’t and vice versa (you can copy my code and try it.). Did I miss something here since I see something similar in the AxonTrader there’re sellOrders and buyOrders with the same type of aggregate member.

I use axonframework version 4.4.8

Hi @xXExpertAxonUserXx, and welcome to our forum.

The answer to your predicament is relatively straightforward, actually.
The solution you’re looking for is currently not supported by Axon Framework at all.

Sure, it works for Event Sourcing (as you can see in the referenced Axon Traded application), but not for Command Handlers for the time being.
In all honestly, the majority of use cases I’ve encountered that resemble yours didn’t reuse the same class type for the differing fields.

So, instead of going for the Person type, we went for a Worker and Manager implementation of Person that would become the Aggregate Member.
The use of concrete entity implementations becomes reasonable due to the fact “workers” and “managers” will, in the end, not be able to execute the same operations (read: commands).

Nonetheless, I think there’s merit in supporting the approach you’re sharing.
As such, I have constructed an issue on GitHub for it.
If you’re interested, you can find it here.

1 Like

Thanks for your advice, and I’m so happy that the next version I will be able to use it. I’m really appreciated for your help. Btw, I need to make it worked first due to the requirement that I got so I implemented the solution that you suggest and I will have to use it until the newer version came out. Here’s what I did My forked branch. It would be nice if you could review my code, but it’s fine anyway if you’re not available. Really, Thanks a lot.

1 Like