[Golang] The possibility of handling Domain Events Concurrently

Mazen Kenjrawi
5 min readOct 23, 2022

--

What are Domain Events?

Well, the sophisticated answer would be something like this:
Domain events are the way to express the domain rules. These rules are the side effects, which are defined by the domain experts through the ubiquitous language.

However, we can also consider them as a very important part of the domain model, in which aggregates are the source of dispatching those events, which eventually will apply their side effect on other aggregates within the same domain through domain-event handlers.

Unlike integrational events, domain events never leave their bounded context. They are in-memory messaging between aggregates of the same bounded context.

That being said, we can now understand their full life cycle:

  • They are initiated within the aggregate of a bounded context in the domain model layer.
  • They hold enough data to perform a side effect on other aggregates within the same bounded context.
  • They are transmitted in-memory from the domain model layer to the application layer.
  • They are handled within the application layer via domain-event handlers, and this happens only once the initiator aggregate is fully and successfully persisted.

It’s important to know that all of these steps will happen during the same process.

From all of the above, we can also look at domain events as enablers for better separation of concerns in the same domain.

Ecommerce shop as an example

As practical people, we always strive to understand things by their applications, so here is an example:

Let’s say we have an ecommerce shop in which we have the Order aggregate like this:

Order aggregate

Pro tip: don’t export any field in your aggregate! If you do so, you are breaking encapsulation! In case you do so, then any package of any layer can mutate and manipulate the state of your aggregate, which eventually may break its consistency or its valid state.
Use
getters instead.

Now, let’s say business asked us, whenever we place an order, we have to do the following:

  • Register a “new” order in our system to show it on our statistics page.
  • Send a confirmation email to the customer.
  • Validate the payment, and once the payment is valid:
    - move the order to “shipped” status.
    - check if any used coupons have now expired.
    - add 1 bonus point for the customer for each 100 price unit he spent.

If we aren’t careful in our design here we would end up doing lots of things while (or after) persisting a new order in our data storage, which isn’t SRP friendly, obviously!
Also, another drawback would be what I call “future fragile design”, meaning whenever we are asked to support another business requirement, we will be forced to add more complexity, and eventually following such an approach will make us face-to-face with unmaintainable hard-to-test and buggy code base, which nobody would like to deal with, and let’s face it… We’ve all been there ;)

Another way to approach this is via domain events! And what Golang can offer us is an out-of-the-box concurrent solution, so our medium to emit and transport those events can be basically a channel or chan data type.

Keeping that in mind, now we will emit an event OrderPlacedEvent whenever a new order is placed, and we can provide a configurable map for how many subscribers (or listeners) to this event, and this list of subscribers can grow or shrink independently as business needs.

Emitting OrderPlaced domain event while creating Order aggregate

Now, if we are using what I call the “concurrent command bus”, then while handling PlaceOrderCommand we will eventually expect to see orderPlacedEvent emitted into the channel we passed from the concurrent bus to be used as our domain events queue.

Concurrent Command Bus

Now, whenever we pass any command to this bus, it will invoke its corresponding handler based on the command’s type.
We also have a domain events channel, which will be the medium of transmitting events from aggregate (domain layer) to the bus (infrastructure layer) of the same bounded context, to be handled later by domain event handlers (application layer).

Later, once handling the command ends with no errors, the bus will consume the domain events by invoking all corresponding application handlers for each one of these events.

The handling functionality of the concurrent bus

Now, getting back to business requirements, whenever we place an order, we have to:

Send a confirmation email to the customer.

Validate the payment…

So our OrderPlacedEvent should be interpreted as “execute 2 additional commands”, and this is what should be actually happening in b.registerEventAsCommand(domainEvent) , the bus transformer will map each domain event to a list of commands (its side effects) and store them in bus.eventCmds to be handled later.

Later, when the place order handler finishes with no errors, we will start executing these registered and ready to be handled commands.

Some of these commands, like validating the payment, may have other domain events as well, which will be also handled in a concurrent way eventually as well in different goroutines.

This is a reliable and powerful tool or mindset to keep in your toolbox whenever you have to deal with such complexity.
It somehow shifts that complexity away from your core domain and its use cases (domain and application layers), making it nothing but a static map inside a factory in your infrastructure layer, connected to a set of reusable components in your application layer.

Later on, whenever it sounds possible, and your bounded context may grow to a certain level in which splitting it seems reasonable, some of your domain events are ready and prepared to become an integrational ones, all what you need then is to map them to handlers, in which they will be serialized into amqp messages or streamed as kafka events…

You can also check this prototype repository that I have created as a proof of concept to what we just discussed.

I hope I have delivered a good portion of new knowledge to you. What’s left to cover in this topic may be implementing a retryable mechanism and failover in case of an incident to even raise the reliability of our system to a higher degree, which I will come to in a later article.

--

--

Mazen Kenjrawi
Mazen Kenjrawi

Written by Mazen Kenjrawi

I’m a software engineer living in Berlin. Large portion of my free time is dedicated to my favourite hobby, reading! And now I’m here writing ;)

No responses yet