Replies: 3 comments 5 replies
-
Let's discuss an example in PHP from an existing project. We're building an e-shop that has the concept of "a cart". There's a use case describing how a cart can be "locked". This is a kind of special state, since a cart can be locked both before and after checkout. If the cart is locked after checkout and the payment were to fail, the cart should go back to its pending state, but still be locked. Currently all of this is modeled in one aggregate root: the If we could use multi-entity aggregates, we could model the "locked state" as a separate entity. It would look something like this: class CartLock extends AggregateEntity
{
private bool $locked = false;
public function isLocked(): bool
{
return $this->locked;
}
public function lock(): self
{
$this->recordThat(…);
return $this;
}
protected function applyLocked(CartLocked $event): void
{
$this->locked = true;
}
public function unlock(): self
{
$this->recordThat(…);
return $this;
}
protected function applyUnlocked(CartUnlocked $event): void
{
$this->locked = false;
}
} The cart aggregate root in turn would be able to use this entity like so: class CartAggregateRoot extends AggregateRoot
{
private CartLock $cartLock;
public function __construct()
{
$this->cartLock = new CartLock();
}
public function fail(?string $message): void
{
// …
$this->recordThat(new CartFailed(
$this->cartUuid(),
$message
));
}
public function applyFailed(CartFailed $event): void
{
if ($this->cartLock->isLocked()) {
// …
} else {
// …
}
}
} Right now, all of this logic happens in the |
Beta Was this translation helpful? Give feedback.
-
There's one piece missing to make all of this work. Axon models its AR methods as command handlers. Instead of directly calling $commandBus->dispatch(new FailCartCommand($cartUuid, …)); The command bus will map the command to an AR method. This is a major paradigm shift from how our package current works. The advantage of it though is that commands can also be dispatched automatically to aggregate entities without the user needing to know the mapping. Without commands, we'd have to add an extra method on the cart AR to be able to lock and unlock the internal entity: class CartAggregateRoot extends AggregateRoot
{
// …
public function lock(): self
{
$this->cartLock->lock();
return $this;
}
public function unlock(): self
{
$this->cartLock->unlock();
return $this;
}
} While this is not the biggest issue, it's a little more cumbersome. I'm not suggesting we should add a command bus right now, but I felt it necessary to mention nevertheless. |
Beta Was this translation helpful? Give feedback.
-
Here's an implementation that doesn't require a command bus: #196 |
Beta Was this translation helpful? Give feedback.
-
Axon has what seems like a great concept called "multi-entity aggregates" (https://docs.axoniq.io/reference-guide/axon-framework/axon-framework-commands/modeling/multi-entity-aggregates)
The tl;dr is that while there's still one aggregate root, it can be split into several internal entities. From the Axon docs:
Maybe it's worth discussing this paradigm and consider adding it in v5?
Beta Was this translation helpful? Give feedback.
All reactions