Skip to main content
  1. Articles/

Practical DDD in Golang: Domain Event

·1907 words·9 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
DDD in Golang - This article is part of a series.
Part 4: This Article

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.

Useful Resources
#

DDD in Golang - This article is part of a series.
Part 4: This Article

Related

Practical DDD in Golang: Domain Service

·1802 words·9 mins· loading · loading
After discussing Entity and Value Objects, I will now introduce the third member of the group of Domain-Modeling patterns in this article: Domain Service. Domain Service is perhaps the most misunderstood DDD pattern, with confusion stemming from various web frameworks. In many frameworks, a Service takes on a multitude of roles. It’s responsible for managing business logic, creating UI components such as form fields, handling sessions and HTTP requests, and sometimes even serving as a catch-all “utils” class or housing code that could belong to the simplest Value Object. However, almost none of the aforementioned examples should be a part of a Domain Service. In this article, I will strive to provide a clearer understanding of its purpose and proper usage. Stateless # A critical rule for Domain Services is that they must NOT maintain any state. Additionally, a Domain Service must NOT possess any fields that have a state. While this rule may seem obvious, it’s worth emphasizing because it’s not always followed. Depending on a developer’s background, they may have experience in web development with languages that run isolated processes for each request. In such cases, it may not have been a concern if a Service contained state. However, when working with Go, it’s common to use a single instance of a Domain Service for the entire application. Therefore, it’s essential to consider the consequences when multiple clients access the same value in memory. Use State in Entity type Account struct { ID uint Person Person Wallets []Wallet } Use State in Value Object type Money struct { Amount int Currency Currency } DON’T use State in Domain Service type DefaultExchangeRateService struct { repository *ExchangeRateRepository useForceRefresh bool } type CasinoService struct { bonusRepository BonusRepository bonusFactory BonusFactory accountService AccountService } As evident in the example above, both Entity and Value Object retain states. An Entity can modify its state during runtime, while Value Objects always maintain the same state. When we require a new instance of a Value Object, we create a fresh one. In contrast, a Domain Service does not house any stateful objects. It solely contains other stateless structures, such as Repositories, other Services, Factories, and configuration values. While it can initiate the creation or persistence of a state, it does not retain that state itself.

Practical DDD in Golang: Entity

·2074 words·10 mins· loading · loading
In the previous article, I attempted to provide insights into the Value Object design pattern and how we should apply it in Go. In this article, the narrative continues with the introduction of a design pattern called Entity. Many developers have heard about Entity countless times, even if they’ve never used the DDD approach. Examples can be found in PHP frameworks and Java. However, its role in DDD differs from its use elsewhere. Discovering its purpose in DDD marked a significant turning point for me. It seemed a bit unconventional, especially for someone with a background in PHP MVC frameworks, but today, the DDD approach appears more logical. It is not part of ORM # As demonstrated in the examples for PHP and Java frameworks, the Entity often assumes the roles of various building blocks, ranging from Row Data Gateway to Active Record. Due to this, the Entity pattern is frequently misused. Its intended purpose is not to mirror the database schema but to encapsulate essential business logic. When I work on an application, my Entities do not necessarily replicate the database structure. In terms of implementation, my first step is always to establish the domain layer. Here, I aim to consolidate the entire business logic, organized within Entities, Value Objects, and Services. Once I’ve completed and unit-tested the business logic, I proceed to create an infrastructural layer, incorporating technical details like database connections. As illustrated in the example below, we separate the Entity from its representation in the database. Objects that mirror database schemas are distinct, often resembling Data Transfer Objects or Data Access Objects. Entity inside the Domain Layer type BankAccount struct { ID uint IsLocked bool Wallet Wallet Person Person } Repository interface inside the Domain Layer // Repository interface inside domain layer type BankAccountRepository interface { Get(ctx context.Context, ID uint) (*BankAccount, error) } Data Access Object inside the Infrastructure Layer type BankAccountGorm struct { ID uint `gorm:"primaryKey;column:id"` IsLocked bool `gorm:"column:is_locked"` Amount int `gorm:"column:amount"` CurrencyID uint `gorm:"column:currency_id"` Currency CurrencyGorm `gorm:"foreignKey:CurrencyID"` PersonID uint `gorm:"column:person_id"` Person PersonGorm `gorm:"foreignKey:PersonID"` } Concrete Repository inside the Infrastructure Layer type BankAccountRepository struct { // // some fields // } func (r *BankAccountRepository) Get(ctx context.Context, ID uint) (*domain.BankAccount, error) { var dto BankAccountGorm // // some code // return &BankAccount{ ID: dto.ID, IsLocked: dto.IsLocked, Wallet: domain.Wallet{ Amount: dto.Amount, Currency: dto.Currency.ToEntity(), }, Person: dto.Person.ToEntity(), }, nil } The example shown above is just one of the many variations we can implement. While the structure of both the Entity and DTO can vary depending on the specific business case (such as having multiple Wallets per BankAccount), the core concept remains consistent.

Practical DDD in Golang: Value Object

·1892 words·9 mins· loading · loading
Saying that a particular pattern is the most important might seem like an exaggeration, but I wouldn’t even argue against it. The first time I encountered the concept of a Value Object was in Martin Fowler’s book. At that time, it seemed quite simple and not very interesting. The next time I read about it was in Eric Evans’ “The Big Blue Book.” At that point, the pattern started to make more and more sense, and soon enough, I couldn’t imagine writing my code without incorporating Value Objects extensively. Simple but beautiful # At first glance, a Value Object seems like a simple pattern. It gathers a few attributes into one unit, and this unit performs certain tasks. This unit represents a particular quality or quantity that exists in the real world and associates it with a more complex object. It provides distinct values or characteristics. It could be something like a color or money (which is a type of Value Object), a phone number, or any other small object that offers value, as shown in the code block below. Quantity type Money struct { Value float64 Currency Currency } func (m Money) ToHTML() string { returs fmt.Sprintf(`%.2f%s`, m.Value, m.Currency.HTML) } Quality type Color struct { Red byte Green byte Blue byte } func (c Color) ToCSS() string { return fmt.Sprintf(`rgb(%d, %d, %d)`, c.Red, c.Green, c.Blue) } Type extension type Salutation string func (s Salutation) IsPerson() bool { returs s != "company" } Logical Group type Phone struct { CountryPrefix string AreaCode string Number string } func (p Phone) FullNumber() string { returs fmt.Sprintf("%s %s %s", p.CountryPrefix, p.AreaCode, p.Number) } In Golang, you can depict Value Objects by creating new structs or by enhancing certain basic types. In either scenario, the goal is to introduce specialized functionalities for that individual value or a set of values. Frequently, Value Objects can supply particular methods for formatting strings to determine how values should operate during JSON encoding or decoding. However, the primary purpose of these methods should be to maintain the business rules linked to that particular characteristic or quality in real life. Identity and Equality # A Value Object lacks identity, and that’s its key distinction from the Entity pattern. The Entity pattern possesses an identity that distinguishes its uniqueness. If two Entities share the same identity, it implies they refer to the same objects. On the other hand, a Value Object lacks such identity. It only consists of fields that provide a more precise description of its value. To determine equality between two Value Objects, we must compare the equality of all their fields, as demonstrated in the code block below.