Skip to main content
  1. Articles/

Practical DDD in Golang: Domain Service

·1802 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 3: This Article

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.

A Wrong Approach

type TransactionService struct {
	bonusRepository BonusRepository
	result          Money // field that contains state
}

func (s *TransactionService) Deposit(account Account, money Money) error {
	bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money)
	if err != nil {
		return err
	}
	//
	// some code
	//
	s.result = s.result.Add(money) // changing state of service
	return nil
}

In the example above, the TransactionService maintains a stateful field in the form of the Money Value Object. Whenever we intend to make a new deposit, we execute the logic for applying Bonuses and then add it to the final result, which is a field inside the Service. This approach is incorrect because it results in the modification of the total whenever anyone makes a deposit. This is not the desired behavior; instead, we should keep the summarization per Account. To achieve this, we should return the calculation as the result of a method, as shown in the example below.

The Right Approach

type TransactionService struct {
	bonusRepository BonusRepository
}

func (s *TransactionService) Deposit(current Money, account Account, money Money) (Money, error) {
	bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money)
	if err != nil {
		return Money{}, err
	}
	//
	// some code
	//
	return current.Add(money), nil // returning new value that represents new state
}

The new TransactionService always generates the latest calculations instead of storing them internally. Different users cannot share the same object in memory, and the Domain Service can once again act as a single instance. In this approach, the client of this Service is now responsible for maintaining the new result and updating it whenever a deposit occurs.

It represents behaviors
#

A Domain Service represents behaviors specific to the Problem Domain. It offers solutions for complex business invariants that cannot be neatly encapsulated within a single Entity or Value Object. Occasionally, a particular behavior may involve interactions with multiple Entities or Value Objects, making it challenging to determine which Entity should own that behavior. In such cases, a Domain Service comes to the rescue.

It’s essential to clarify that a Domain Service is not responsible for handling sessions or requests, has no knowledge of UI components, doesn’t execute database migrations, and doesn’t validate user input. Its sole role is to manage business logic within the domain.

An Example of a Domain Service

type ExchangeRateService interface {
	IsConversionPossible(from Currency, to Currency) bool
	Convert(to Currency, from Money) (Money, error)
}

type DefaultExchangeRateService struct {
	repository *ExchangeRateRepository
}

func NewExchangeRateService(repository *ExchangeRateRepository) ExchangeRateService {
	return &DefaultExchangeRateService{
		repository: repository,
	}
}

func (s *DefaultExchangeRateService) IsConversionPossible(from Currency, to Currency) bool {
	var result bool
	//
	// some code
	// 
	return result
}

func (s *DefaultExchangeRateService) Convert(to Currency, from Money) (Money, error) {
	var result Money
	//
	// some code
	// 
	return result, nil
}

In the example above, we have the ExchangeRateService as an instance. Whenever I need to provide a stateless structure that I should inject into another object, I define an interface. This practice aids in unit testing. The ExchangeRateService is responsible for managing the entire business logic related to currency exchange. It includes the ExchangeRateRepository to retrieve all exchange rates, allowing it to perform conversions for any amount of money.

Another Example of a Domain Service

type TransactionService struct {
	bonusRepository BonusRepository
	accountService  AccountService
	//
	// some other fields
	//
}

func (s *TransactionService) Deposit(account Account, money Money) error {
	bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money)
	if err != nil {
		return err
	}
	//
	// some code
	//
	for _, bonus := range bonuses {
		err = bonus.Apply(&account)
		if err != nil {
			return err
		}
	}
	//
	// some code
	//
	err = s.accountService.Update(account)
	if err != nil {
		return err
	}

	return nil
}

As mentioned, a Domain Service encapsulates business invariants that are too intricate to be confined to a single Entity or Value Object. In the example above, the TransactionService manages the complex logic of applying Bonuses whenever a new deposit is made by an Account. Instead of compelling the Account or Bonus Entities to rely on each other, or worse yet, furnishing expected repositories or services to Entity methods, the more suitable approach is to create a Domain Service. This Service can encapsulate the entire business logic for applying Bonuses to any Account as needed.

It represents contracts
#

In some scenarios, our Bounded Context relies on others. A common example is a cluster of Microservices, where one Microservice accesses another via a REST API. Frequently, data obtained from an external API is vital for the primary Bounded Context to function. Therefore, within our domain layer, we should have access to that data. It’s imperative to maintain separation between our domain layer and technical intricacies. This means that incorporating integration with an external API or database directly into our business logic is considered a code smell.

This is where the Domain Service comes into play. In the domain layer, I always provide an Interface for the Service as a Contract for external integrations. We can then inject that interface throughout our business logic, while the actual implementation resides in the infrastructural layer.

A Contract on the Domain Layer

type AccountService interface {
	Update(account Account) error
}

The Implementation on the Infrastructure Layer

type AccountAPIService struct {
	client *http.Client
}

func NewAccountService(client *http.Client) domain.AccountService {
	return &AccountAPIService{
		client: client,
	}
}

func (s AccountAPIService) Update(account domain.Account) error {
	var request *http.Request
	//
	// some code
	//
	response, err := s.client.Do(request)
	if err != nil {
		return err
	}
	//
	// some code
	//
	return nil
}

In the example above, I have defined the AccountService Interface in the domain layer. It serves as a Contract that other Domain Services can utilize. However, the actual implementation is provided through AccountAPIService. AccountAPIService is responsible for sending HTTP requests to an external CRM system or to our internal Microservice, specifically designed for handling Accounts. This approach allows for flexibility, as we can create an alternative implementation of AccountService. For instance, we could develop an implementation that works with test Accounts from a file, suitable for an isolated testing environment.

Domain Service Vs. other types of Services
#

Up to this point, it’s clear when and why we should provide a Domain Service. However, in some cases, it’s not immediately evident if a Service should also be considered a Domain Service or belong to a different layer. Infrastructural Services are typically the easiest to identify. They invariably encompass technical details, database integration, or interaction with external APIs. Often, they serve as concrete implementations of Interfaces from other layers.

Presentational Services are also straightforward to recognize. They consistently involve logic related to UI components or the validation of user inputs, with Form Service being a typical example.

The challenge arises when distinguishing between Application and Domain Services. I have personally found it most challenging to differentiate between these two types. In my experience, I have primarily used Application Services for providing general logic for managing sessions or handling requests. They are also suitable for managing Authorization and Access Rights.

An Application Service

type AccountSessionService struct {
	accountService AccountService
}

func (s *AccountSessionService) GetAccount(session *sessions.Session) (*Account, error) {
	value, ok := session.Values["accountID"]
	if !ok {
		return nil, errors.New("there is no account in session")
	}
	
	id, ok := value.(string)
	if !ok {
		return nil, errors.New("invalid value for account ID in session")
	}
	
	account, err := s.accountService.ByID(id)
	if err != nil {
		return nil, err
	}
	
	return account, nil
}

In numerous instances, I have employed an Application Service as a wrapping structure for a Domain Service. I adopted this approach whenever I needed to cache something within the session and utilize the Domain Service as a fallback for data retrieval. You can observe this approach in the example above. In this example, AccountSessionService serves as an Application Service, encompassing the functionality of the AccountService from the Domain Layer. Its responsibility is to retrieve a value from the session store and subsequently utilize it to retrieve Account details from the underlying Service.

Conclusion
#

A Domain Service is a stateless structure that encapsulates behaviors from the actual business domain. It interacts with various objects, such as Entities and Value Objects, to handle complex behaviors, especially those that don’t have a clear home within other objects. It’s important to note that a Domain Service shares only its name with Services from other layers, as its purpose and responsibilities are entirely distinct.

A Domain Service is exclusively relevant to business logic and should remain detached from technical details, session management, handling requests, or any other application-specific concerns.

Useful Resources
#

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

Related

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.