I have workflow where I have complex command handlers encapsulated inside aggregate. These handlers emit some events, and then further logic based on result of these events can emit more events. Sample pseudocode of architecture:
public class ExampleAggregate : Aggregate
{
public void Execute(CommandA c)
{
ChangeState(c.params); // applies events
// this relies on synchronous application of events inside ChangeState,
// real code checks more state, in multiple entities inside aggregate
if (this.state == Y)
{
ChangeOtherState(c.otherParams); // applies events
}
}
private State state;
private void ChangeState(Params params) // can be called from multiple command handlers
{
if (params == ... && this.state == X)
{
Apply(new StateChanged { state = Y });
}
if (params == ... && this.state == Y)
{
Apply(new StateChanged { state = Z });
}
(...)
}
private void ChangeOtherState(...) => /* do some logic and apply some events*/;
private void On(StateChanged e) => this.state = e.state;
}
(...)
// later, after command is processed, events are pulled and passed further
var aggregate = database.LoadAggregate(command.aggregateId);
aggregate.Execute(command); // synchronous
var events = aggregate.PullEvents();
// in-memory aggregate can be discarded now
database.Publish(events);
This example is simple, but I have more complex examples when applying events and logic is intertwined in loops, for example changing multiple entities inside aggregate and then logic (based on these multiple entities changed and multiple events applied) can emit more events.
I wonder if this is valid and correct approach? Also if there is any alternative or smart pattern which can be used?
EDIT:
"state" variable is only example - there are multiple fields which can be checked and drive execution of other methods.
Command handler is method, which can apply events or call "command" methods, which can also apply events.
Event handlers cannot call "command" methods, create commands, make any logic, etc. - these are only for changing state based on events. No state change can happen outside event handlers.
Single command handler execution is synchronous and rather fast, it's important that aggregate is most of the time cached in memory so command handlers execute very fast. But at the same time, if aggregate is unloaded, it can be easily restored from events (only events are persisted in database).
Maybe it's also important that aggregate contains multiple nested entities - command handlers can execute "command" methods on these entities, which can also apply events on aggregate.
EDIT 2: more real-world (but still made up) example, it shows that after every change to order, it has to update discounts on entities. In my code, there are more dependencies like this, where applying events influences further state changes.
public class Order : Aggregate
{
public void Execute(ApplyOrderDiscountCount c)
{
/* validate c.DiscountCode */
Apply(new OrderDiscountCodeApplied { NewDiscountCode = c.DiscountCode });
UpdateDiscounts();
}
public void Execute(IncreaseOrderProductCount c)
{
var product = FindProduct(c.ProductId);
if (product == null)
{
/* validate c.ProductId */
Apply(new OrderProductAdded
{
ProductId = c.ProductId,
Count = 1,
ItemPrice = /* can query some service */,
...
});
}
else
{
product.IncreaseCount();
}
UpdateDiscounts();
}
public void Execute(DecreaseOrderProductCount c)
{
var product = FindProduct(c.ProductId);
if (product == null)
{
return; // product not found
}
product.DecreaseCount();
UpdateDiscounts();
}
public void Execute(DecreaseAllOrderProductsCount c)
{
for (product in _products)
{
product.DecreaseCount();
}
// discount is updated only once after all products are updated
UpdateDiscounts();
}
public void On(OrderDiscountCodeApplied e) => _discountCode = e.NewDiscountCode;
public void On(OrderProductAdded e) => _products.Add(new OrderProduct(this, e.Id, e.Count, e.ItemPrice));
public void On(IncreaseOrderProductCount e) => FindProduct(e.ProductId).On(e);
public void On(DecreaseOrderProductCount e) => FindProduct(e.ProductId).On(e);
private List<OrderProduct> _products;
private string _discountCode;
private OrderProduct FindProduct(string productId)
=> _products.FindIndex(_ => _.Id == productId);
private void UpdateDiscounts()
{
var discountByProductId = /* some logic calculating discount,
e.g buy two items and get third for 50%, or second product from same category
gets 10% discount, discount code can be applied to whole order etc.
This logic can be embedded in Order class, or it can be queried from external service.
*/;
foreach (product in _products)
{
product.SetDiscount(discountByProductId[product.Id]);
}
}
}
public class OrderProduct : Entity<Order>
{
public string Id { get; }
public int Count { get; private set; }
public decimal ItemPrice { get; }
public decimal Discount { get; private set; }
public OrderProduct(Order order, string id, int count, decimal itemPrice): base(order) { ... }
public void IncreaseCount()
{
Apply(new OrderProductCountIncreased { ProductId = Id, NewCount = Count + 1 });
}
public void DecreaseCount()
{
if (Count <= 0)
{
return; // already empty
}
Apply(new OrderProductCountDecreased { ProductId = Id, NewCount = Count - 1 });
}
public void SetDiscount(decimal discount)
{
if (Discount == discount)
{
return;
}
Apply(new OrderProductDiscountChanged { ProductId = Id, NewDiscount = discount );
}
public void On(OrderProductCountIncreased e) => Count = e.NewCount;
public void On(OrderProductCountDecreased e) => Count = e.NewCount;
public void On(OrderProductDiscountChanged e) => Discount = e.NewDiscount;
}
Apply(new SomeEvent { fieldX = Y }); if (this.fieldX == Z) { Apply(new OtherEvent { ... }); }
- ofc this directly does not make sense, but application of SomeEvent can be nested and Y value can be calculated based on state of aggregate/entities