TL;DR Domain events aren't just a fad, aimed to force asynchronous communication for the sake of being more "reactive" - this is an underappreciated modelling pattern that helps in putting a clear separation between what is the precisely defined business logic & what is the side effect, out of boundaries of current context. Lack of understanding herein may cause excessive complexity in functional areas of the greatest interest - most likely, your Core Domain ...
One the goals of my recent WG.NET presentation about coupling was to emphasize that coupling is not only about loosening dependencies between "layers" or just hiding implementation details behind the abstract interfaces. Coupling does happen even in reasonably shaped Architectures, if we don't give too much thought to how we'd like to scale out our business logic.
Let's start with the example:
Imagine that you work on a system that provides a subscription-based access to some digital services (MOOC, media streaming, whatever). There's a context of subscription (no-brainer ...) which should most likely expose business-specific operations on the subscription(s), one of them quite likely - UpdateSubscription (examples are in C#):
public class Subscription : Entity, IAggregateRoot
{
...
public Subscription UpdateSubscription(int subId, ...)
{
// validate the entry state (whether operation can be performed)
Contract.Requires(...);
// update the internal state (within aggregate!)
this.someProperty1 = ...
this.someProperty2 = ...
SomeInternalLogic(...)
return this; // for the sake of chaining
}
}
Clearly, the main concern of this operation is to adjust the subscription so it is up-to-date with what the user has requested. Based on this information we'll authorize her/him to certain operation(s) / data. But that's not all. What about the financials? A change within the subscription could have an impact on how we'd like to charge the user - in other words: what should be put on the invoice. Easy peasy, let's just add the necessary (other) service calls in our method. What may be a bit puzzling is the fact, that invoicing looks like a different context ...
public class Subscription : Entity, IAggregateRoot
{
private IInvoicingService _invoicing;
public Subscription(IInvoicingService invoicing)
{
_invoicing = invoicing;
}
...
public Subscription UpdateSubscription(int subId, ...)
{
// validate the entry state (whether operation can be performed)
Contract.Requires(...);
// update the internal state (within aggregate!)
this.someProperty1 = ...
this.someProperty2 = ...
SomeInternalLogic(...)
// adjust invoicing details
_invoicing.DoSomethingThereAsWell(...);
return this; // for the sake of chaining
}
}
Looks good? We'll see about that ... But that's not all (yet). Due to performance reasons, we should also adjust the pre-fetched media items for our user. Why so? If her/his updated subscription supports 4k (& the prior one was FullHD only), (s)he should be getting better quality content since the update happened, right? OK, no big deal, let's add another service call (again - this is another context - in this case: Media).
public class Subscription : Entity, IAggregateRoot
{
private IInvoicingService _invoicing;
private IPrefetchedMediaService _prefetched;
public Subscription(IInvoicingService invoicing, IPrefetchedMediaService prefetched)
{
_invoicing = invoicing;
_prefetched = prefetched;
}
...
public Subscription UpdateSubscription(int subId, ...)
{
// validate the entry state (whether operation can be performed)
Contract.Requires(...);
// update the internal state (within aggregate!)
this.someProperty1 = ...
this.someProperty2 = ...
SomeInternalLogic(...)
// adjust invoicing details
_invoicing.DoSomethingThereAsWell(...);
// prefetch media according to new subscription details
_prefetched.DoSomethingThereAsWell(...);
return this; // for the sake of chaining
}
}
We're getting closer & closer to the full business handling of subscription update, our users (& accountants, & infrastructure engineers, ...) will be so happy. But, but, but - I've forgotten to tell you that subscription information is redundantly duplicated in the full-text search engine used by our internal employees (Back Office workers), so this information should get populated there as soon as subscription is updated. Awesome, you know what to do:
public class Subscription : Entity, IAggregateRoot
{
private IInvoicingService _invoicing;
private IPrefetchedMediaService _prefetched;
private ISearchEngineService _searchEngine;
public Subscription(IInvoicingService invoicing, IPrefetchedMediaService prefetched, ISearchEngineService searchEngine)
{
_invoicing = invoicing;
_prefetched = prefetched;
_searchEngine = searchEngine;
}
...
public Subscription UpdateSubscription(int subId, ...)
{
// validate the entry state (whether operation can be performed)
Contract.Requires(...);
// update the internal state (within aggregate!)
this.someProperty1 = ...
this.someProperty2 = ...
SomeInternalLogic(...)
// adjust invoicing details
_invoicing.DoSomethingThereAsWell(...);
// prefetch media according to new subscription details
_prefetched.DoSomethingThereAsWell(...);
// update information in the search engine
_searchEngine.DoSomethingThereAsWell(...);
return this; // for the sake of chaining
}
}
Let's stop here & check our "favourite" function out. Its contains all the necessary actions that are supposed to happen when subscription is updated, it uses few injected services, but it depends on the contracts only. All seems correct & in place, but ... do the contents of UpdateSubscription really represent bounded, isolated operation of updating the subscription? I have some doubts ...
- this can't be really tested in separation anymore (due to dependencies, that may have their own dependencies ...)
- it requires not only subscription-specific knowledge, but also knowledge about other areas - either business ones (invoicing) or technical ones (search engine)
- it gets complicated - imagine how could "submit order" look alike in a huge retail on-line shop (like Amazon) -> it can trigger tens of different actions within very different contexts ...
OK, so what is actually wrong if we do stuff exactly according to the book? In shortest possible words - we've mixed two important terms: business operation & side effect.
- What happens within the given context (Subscription) is a business operation -> it's related to subscription & subscription only, it's transactional inside the context & leaves the persisted state coherent. It can be defined, verified, approved & tested by the Domain Expert who excels in the area of Subscriptions. Period.
- What happens outside of the given context (in Invoicing, Media, Search Engine) is a side effect of business (domain) event from Subscription context - it's triggered by occurrence of this event, it relies on the data included within the event, but it's NOT a part of UpdateSubscription business logic, so it should be moved out of Subscription context & put inside all these other contexts
How to implement the latter? Here you go:
namespace ABC.Subscriptions
{
public class Subscription : Entity, IAggregateRoot
{
...
public Subscription UpdateSubscription(int subId, ...)
{
// validate the entry state (whether operation can be performed)
Contract.Requires(...);
// update the internal state (within aggregate!)
this.someProperty1 = ...
this.someProperty2 = ...
SomeInternalLogic(...)
RaiseDomainEvent(new SubscriptionUpdatedEvent { ... });
return this; // for the sake of chaining
}
}
}
namespace ABC.Invoicing
{
public class UpdateInvoiceDueToSubscriptionChanged : IEventHandler<SubscriptionUpdatedEvent>
{
IInvoicingService _invoicing;
public UpdateInvoiceDueToSubscriptionChanged(IInvoicingService invoicing)
{
_invoicing = invoicing;
}
...
public void Handle(SubscriptionUpdatedEvent event)
{
_invoicing.DoSomethingThereAsWell(...);
}
}
}
namespace ABC.PrefetchedMedia
{
public class PrefetchMediaDueToSubscriptionChanged : IEventHandler<SubscriptionUpdatedEvent>
{
IPrefetchedMediaService _prefetched;
public PrefetchMediaDueToSubscriptionChanged(IPrefetchedMediaService prefetched)
{
_prefetched = prefetched;
}
...
public void Handle(SubscriptionUpdatedEvent event)
{
_prefetched.DoSomethingThereAsWell(...);
}
}
}
namespace ABC.SearchEngine
{
public class UpdateSearchIndexDueToSubscriptionChanged : IEventHandler<SubscriptionUpdatedEvent>
{
ISearchEngineService _searchEngine;
public PrefetchMediaDueToSubscriptionChanged(ISearchEngineService searchEngine)
{
_searchEngine = searchEngine;
}
...
public void Handle(SubscriptionUpdatedEvent event)
{
_searchEngine.DoSomethingThereAsWell(...);
}
}
}
This is nothing new, based technically on publish-subscribe pattern, it was covered several years ago in Vaughn Vernon's Red Book, it's a foundation of Alberto Brandolini's Event Storming approach (to model Business Domains), there are shitloads of implementation examples on the Internet, but ... somehow it seems to be the counter-intuitive & hard to grasp for many software crafts(wo)men.
In fact - it's understandable, to some degree. More explicit dependencies, e.g. imperative, direct calls are easier to trace & trouble-shoot. But IMHO the slightly reduced readability is a good price to pay for a proper, scalable business logic composition. People seem to have a conceptual problem with event handlers "dangling" freely out of clear execution paths. Personally I find it a grouping issue: handlers, just like services or any other domain objects should be explicitly qualified to a particular domain context.
Pic: © GoldIris