Practical DDD in Golang: Domain Event
- 9 minutes read - 1901 wordsThe tale of DDD in Go advances by introducing a fundamental building block that mirrors real-world events — the Domain Event.

In many cases, Entities are the most effective means of representing elements in Domain-Driven Design. Together with Value Objects, they can provide a precise reflection of our Problem Domain. However, sometimes, the most apt way to depict a Problem Domain is by employing events that transpire within it. In my experience, I increasingly attempt to identify events and then discern the Entities associated with them. Although Eric Evans didn’t cover the Domain Event pattern in the first edition of his book, today, it’s challenging to fully develop the domain layer without incorporating events.
The Domain Event pattern serves as a representation of such occurrences within our code. We employ it to elucidate any real-world event that holds relevance for our business logic. In the contemporary business landscape, virtually everything is connected to some form of event.
It can be anything
Domain Events can encompass a wide range of occurrences, but they must adhere to certain rules. The first rule
is that they are immutable. To support this characteristic, I consistently utilize private fields within
Event structs, even though I’m not particularly fond of private fields and getters in Go. However, Events typically
don’t require many getters. Additionally, a specific Event can only occur once. This implies that we can create
an Order
Entity with a particular Identity only once, and consequently, our code can only trigger the Event
that describes the creation of that Order
once. Any other Event related to that Order would be a different
type of Event, pertaining to a distinct Order
. Each Event essentially narrates something that has already
taken place, representing the past. This means we trigger the OrderCreated
Event after we have already created
the Order
, not before.
Global Events
type Event interface {
Name() string
}
type GeneralError string
func (e GeneralError) Name() string {
return "event.general.error"
}
Order Event
type OrderEvent interface {
Event
OrderID() uuid.UUID
}
type OrderDispatched struct {
orderID uuid.UUID
}
func (e OrderDispatched) Name() string {
return "event.order.dispatched"
}
func (e OrderDispatched) OrderID() uuid.UUID {
return e.orderID
}
type OrderDelivered struct {
orderID uuid.UUID
}
func (e OrderDelivered) Name() string {
return "event.order.delivery.success"
}
func (e OrderDelivered) OrderID() uuid.UUID {
return e.orderID
}
type OrderDeliveryFailed struct {
orderID uuid.UUID
}
func (e OrderDeliveryFailed) Name() string {
return "event.order.delivery.failed"
}
func (e OrderDeliveryFailed) OrderID() uuid.UUID {
return e.orderID
}
The code example provided above demonstrates simple Domain Events. This code represents just one of countless ways
to implement them in Go. In certain situations, such as with GeneralError
, I have employed straightforward
strings as Event representations. However, there are instances when I’ve utilized more complex objects or extended
the base Event
interface with a more specific one to introduce additional methods, as seen with OrderEvent
.
It’s worth noting that the Domain Event, as an interface, doesn’t require the implementation of any specific methods.
It can take on any form you find suitable. As mentioned earlier, I sometimes use strings, but essentially,
anything can serve as an adequate representation. Occasionally, for the sake of generalization, I still declare the
Event
interface.
The Old Friend
The Domain Event pattern, fundamentally, is another manifestation of the
Observer pattern. The Observer pattern
identifies key roles, including Publisher
, Subscriber
(or Observer
), and, naturally, Event
. The Domain Event
pattern operates on the same principles. The Subscriber
, often referred to as the EventHandler
, is a structure
that should react to a specific Domain Event to which it has subscribed. The Publisher
, in this context, is a
structure responsible for notifying all EventHandlers
when a particular Event
occurs. The Publisher
serves as
the entry point for triggering any Event
and contains all the EventHandlers
. It offers a straightforward
interface for any Domain Service,
Factory, or other objects that wish to publish a particular Event.
Observer pattern in practice
type EventHandler interface {
Notify(event Event)
}
type EventPublisher struct {
handlers map[string][]EventHandler
}
func (e *EventPublisher) Subscribe(handler EventHandler, events ...Event) {
for _, event := range events {
handlers := e.handlers[event.Name()]
handlers = append(handlers, handler)
e.handlers[event.Name()] = handlers
}
}
func (e *EventPublisher) Notify(event Event) {
for _, handler := range e.handlers[event.Name()] {
handler.Notify(event)
}
}
The code snippet presented above encompasses the remainder of the Domain Event pattern. The EventHandler
interface defines any structure that should respond to a particular Event. It contains only one Notify
method,
which expects the Event as an argument.
The EventPublisher
struct is more intricate. It offers the general Notify
method, which is responsible
for informing all EventHandlers
subscribed to a specific Event. Another function, Subscribe
, enables any
EventHandler
to subscribe to any Event. The EventPublisher
struct could be less complex; instead of allowing
EventHandler
to subscribe to a particular Event using a map, it could manage a simple array of EventHandlers
and notify all of them for any Event.
In general, we should publish Domain Events synchronously in our domain layer. However, there are occasions when I want to trigger them asynchronously, for which I employ Goroutines.
Observer pattern with goroutines
type Event interface {
Name() string
IsAsynchronous() bool
}
type EventPublisher struct {
handlers map[string][]EventHandler
}
func (e *EventPublisher) Notify(event Event) {
if event.IsAsynchronous() {
go e.notify(event) // runs code in separate Go routine
}
e.notify(event) // synchronous call
}
func (e *EventPublisher) notify(event Event) {
for _, handler := range e.handlers[event.Name()] {
handler.Notify(event)
}
}
The example above illustrates one variation for asynchronously publishing Events. To accommodate both approaches,
I frequently define a method within the Event
interface that later informs me whether the Event
should be fired
synchronously or not.
Creation
My biggest dilemma was determining the right place to create an Event. To be honest, I created them everywhere.
The only rule I followed was that stateful objects could not notify the EventPublisher
.
Entities,
Value Objects, and
Aggregates are stateful objects. From that perspective,
they should not contain the EventPublisher
inside them, and providing it as an argument to their methods always
seemed like messy code to me. I also do not use stateful objects as EventHandlers
. If I need to perform an action
with some Entity when a specific Event occurs, I create an EventHandler
that contains a
Repository. Then, the Repository can provide an Entity
that needs to be adapted. Still, creating Event objects inside a method of an Aggregate is acceptable.
Sometimes, I create them within an Entity’s method and return them as a result. Then, I use stateless structures
like Domain Service or
Factory to notify the EventPublisher
.
Order Aggregate
type Order struct {
id uuid.UUID
//
// some fields
//
isDispatched bool
deliveryAddress Address
}
func (o Order) ID() uuid.UUID {
return o.id
}
func (o *Order) ChangeAddress(address Address) Event {
if o.isDispatched {
return DeliveryAddressChangeFailed{
orderID: o.ID(),
}
}
//
// some code
//
return DeliveryAddressChanged{
orderID: o.ID(),
}
}
Order Service
type OrderService struct {
repository OrderRepository
publisher EventPublisher
}
func (s *OrderService) Create(order Order) (*Order, error) {
result, err := s.repository.Create(order)
if err != nil {
return nil, err
}
//
// update Adrress in DB
//
s.publisher.Notify(OrderCreated{
orderID: result.ID(),
})
return result, err
}
func (s *OrderService) ChangeAddress(order Order, address Address) {
evt := order.ChangeAddress(address)
s.publisher.Notify(evt) // publishing of events only inside stateless objects
}
In the example above, the Order
Aggregate provides a method for updating delivery addresses. The result of that
method may be an Event. This means that Order
can create some Events, but that’s its limit. On the other hand,
OrderService
can both create Events and publish them. It can also fire Events that it receives from Order
while
updating the delivery address. This is possible because it contains EventPublisher
.
Events on other layers
We can listen to Domain Events in other layers, like the application, presentation, or infrastructure layers. We can
also define separate Events that are dedicated only to those layers. In those cases, we are not dealing with Domain
Events. A simple example is Events in the Application Layer. After creating an Order
, in most cases, we should
send an Email
to the customer. Although it may seem like a business rule, sending emails is always application-specific.
In the example below, there is a simple code with EmailEvent
. As you may guess, an Email
can be in many different
states, and transitioning from one state to another is always performed during some Events.
The Domain Layer
type Email struct {
id uuid.UUID
//
// some fields
//
}
type EmailEvent interface {
Event
EmailID() uuid.UUID
}
type EmailSent struct {
emailID uuid.UUID
}
func (e EmailSent) Name() string {
return "event.email.sent"
}
func (e EmailSent) EmailID() uuid.UUID {
return e.emailID
}
The Application Layer
type EmailHandler struct{
//
// some fields
//
}
func (e *EmailHandler) Notify(event Event) {
switch actualEvent := event.(type) {
case EmailSent:
//
// do something
//
default:
return
}
}
Sometimes we want to trigger a Domain Event outside of our Bounded Context. These Domain Events are internal to our Bounded Context but are external to other contexts. Although this topic is more related to Strategic Domain-Driven Design, I will briefly mention it here. To publish an Event outside of our Microservice, we may use a messaging service like SQS.
Send Events to SQS
import (
//
// some imports
//
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/sqs"
)
type EventSQSHandler struct {
svc *sqs.SQS
}
func (e *EventSQSHandler) Notify(event Event) {
data := map[string]string{
"event": event.Name(),
}
body, err := json.Marshal(data)
if err != nil {
log.Fatal(err)
}
_, err = e.svc.SendMessage(&sqs.SendMessageInput{
MessageBody: aws.String(string(body)),
QueueUrl: &e.svc.Endpoint,
})
if err != nil {
log.Fatal(err)
}
}
In the code snippet above, you can see the EventSQSHandler
, a simple struct in the infrastructure layer. It sends a
message to the SQS queue whenever a specific Event occurs, publishing only the Event names without specific details.
When it comes to publishing internal Events to the outside world, we may also listen to external Events and map them
to internal ones. To achieve this, I always provide a Service on the infrastructure layer that listens to messages
from the outside.
Catch Events from SQS
type SQSService struct {
svc *sqs.SQS
publisher *EventPublisher
stopChannel chan bool
}
func (s *SQSService) Run(event Event) {
eventChan := make(chan Event)
MessageLoop:
for {
s.listen(eventChan)
select {
case event := <-eventChan:
s.publisher.Notify(event)
case <-s.stopChannel:
break MessageLoop
}
}
close(eventChan)
close(s.stopChannel)
}
func (s *SQSService) Stop() {
s.stopChannel <- true
}
func (s *SQSService) listen(eventChan chan Event) {
go func() {
message, err := s.svc.ReceiveMessage(&sqs.ReceiveMessageInput{
//
// some code
//
})
var event Event
if err != nil {
log.Print(err)
event = NewGeneralError(err)
return
} else {
//
// extract message
//
}
eventChan <- event
}()
}
The example above illustrates the SQSService
within the infrastructure layer. This service listens to SQS messages
and maps them to internal Events when possible. While I haven’t used this approach extensively, it has proven valuable
in scenarios where multiple Microservices need to respond to events like Order
creation or Customer
registration.
Conclusion
Domain Events are essential constructs in our domain logic. In today’s business world, everything is closely tied to specific events, making it a good practice to describe our Domain Model using events. The Domain Event pattern is essentially an implementation of the Observer pattern. While it can be created within various objects, it is best fired from stateless ones. Additionally, other layers can also make use of Domain Events or implement their own event mechanisms.