Skip to main content
  1. Articles/

Practical DDD in Golang: Aggregate

·2269 words·11 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 6: This Article

I have spent years understanding and practicing the DDD approach. Most of the principles were easy to understand and implement in the code. However, there was one that particularly caught my attention. I must say that the Aggregate pattern is the most critical one in DDD, and perhaps the entire Tactical Domain-Driven Design doesn’t make sense without it. It serves to bind business logic together. While reading, you might think that the Aggregate resembles a cluster of patterns, but that is a misconception. The Aggregate is the central point of the domain layer. Without it, there is no reason to use DDD.

Business Invariants
#

In the real business world, some rules are flexible. For example, when you take a loan from a bank, you need to pay some interest over time. The overall amount of interest is adjustable and depends on your invested capital and the period you will spend to pay the debt. In some cases, the bank may grant you a grace period, offer you a better overall credit deal due to your loyalty in the past, provide you with a once-in-a-lifetime offer, or require you to place a mortgage on a house.

All of these flexible rules from the business world are implemented in DDD using the Policy pattern. They depend on many specific cases and, as a result, require more complex code structures. In the real business world, there are also some immutable rules. Regardless of what we try, we cannot change these rules or their application in our business. These rules are known as Business Invariants. For example, nobody should be allowed to delete a customer account in a bank if any of the bank accounts associated with the customer has money or is in debt. In many banks, one customer may have multiple bank accounts with the same currency. However, in some of them, the customer is not allowed to have any foreign currency accounts or multiple accounts with the same currency. When such business rules exist, they become Business Invariants. They are present from the moment we create the object until the moment we delete it. Breaking them means breaking the whole purpose of the application.

Currency Entity

type Currency struct {
	id uuid.UUID
	//
	// some fields
	//
}

func (c Currency) Equal(other Currency) bool {
	return c.id == other.id
}

BankAccount Entity

type BankAccount struct {
	id       uuid.UUID
	iban     string
	amount   int
	currency Currency
}

func NewBankAccount(currency Currency) BankAccount {
	return BankAccount{
		//
		// define fields
		//
	}
}

func (ba BankAccount) HasMoney() bool {
	return ba.amount > 0
}

func (ba BankAccount) InDebt() bool {
	return ba.amount > 0
}

func (ba BankAccount) IsForCurrency(currency Currency) bool {
	return ba.currency.Equal(currency)
}

BankAccounts Value Object

type BankAccounts []BankAccount

func (bas BankAccounts) HasMoney() bool {
	for _, ba := range bas {
		if ba.HasMoney() {
			return true
		}
	}

	return false
}

func (bas BankAccounts) InDebt() bool {
	for _, ba := range bas {
		if ba.InDebt() {
			return true
		}
	}

	return false
}

func (bas BankAccounts) HasCurrency(currency Currency) bool {
	for _, ba := range bas {
		if ba.IsForCurrency(currency) {
			return true
		}
	}

	return false
}

CustomerAccount Entity and Aggregate

type CustomerAccount struct {
	id        uuid.UUID
	isDeleted bool
	accounts  BankAccounts
	//
	// some fields
	//
}

func (ca *CustomerAccount) MarkAsDeleted() error {
	if ca.accounts.HasMoney() {
		return errors.New("there are still money on bank account")
	}
	if ca.accounts.InDebt() {
		return errors.New("bank account is in debt")
	}

	ca.isDeleted = true

	return nil
}

func (ca *CustomerAccount) CreateAccountForCurrency(currency Currency) error {
	if ca.accounts.HasCurrency(currency) {
		return errors.New("there is already bank account for that currency")
	}
	ca.accounts = append(o.accounts, NewBankAccount(currency))

	return nil
}

In the example above, we can see a Go code construct with CustomerAccount as an Entity and Aggregate. Additionally, there are BankAccount and Currency as Entities. Individually, all three entities have their own business rules. Some rules are flexible, while others are invariants. However, when they interact with each other, certain invariants affect all of them. This is the area where we place our Aggregate.

We have a logic for BankAccount creation that depends on all BankAccounts of a particular CustomerAccount. In this case, one Customer cannot have multiple BankAccounts with the same Currency. Furthermore, we cannot delete a CustomerAccount if all BankAccounts connected to it are not in a clean state, meaning they should not have any money in them.

Business Invariants
Business Invariants

The diagram above displays a group of three entities we’ve previously discussed. They are all interconnected by Business Invariants that guarantee the Aggregate is consistently in a dependable state. If any other Entity or Value Object is governed by the same Business Invariants, those new objects also become components of the same Aggregate. However, if within the same Aggregate, we lack a single Invariant that links one object to the rest, then that object does not belong to that Aggregate.

Boundary
#

Many times I have used DDD, there was a question about how to define the Aggregate boundary. By adding every new Entity or Value Object into the game, that question always rises. Till now, it is clear that Aggregate is not just some collection of objects. It is a domain concept. Its members define a logical cluster. Without grouping them, we can not guarantee that they are in a valid state.

Person Entity

type Person struct {
	id uuid.UUID
	//
	// some fields
	//
	birthday time.Time
}

func (p *Person) IsLegal() bool {
	return p.birthday.AddDate(18, 0, 0).Before(time.Now())
}

Company Entity

type Company struct {
	id uuid.UUID
	//
	// some fields
	//
	isLiquid bool
}

func (c *Company) IsLegal() bool {
	return c.isLiquid
}

Customer Entity and Aggregate

type Customer struct {
	id      uuid.UUID
	person  *Person
	company *Company
	//
	// some fields
	//
}

func (c *Customer) IsLegal() bool {
	if c.person != nil {
		return c.person.IsLegal()
	} else {
		return c.company.IsLegal()
	}
}

In the code snippet above, you can see the Customer Aggregate. In many applications, you will typically have an Entity called Customer, and often, that Entity will also serve as the Aggregate. Here, we have some Business Invariants that determine the validity of a specific Customer, depending on whether it is a Person or a Company. While there could be more Business Invariants, for now, one suffices. Since we are developing a banking application, the question arises: Do CustomerAccount and Customer belong to the same Aggregate? There is a connection between them, and certain business rules link them, but are these rules considered Invariants?

Aggregate Boundary
Aggregate Boundary

One Customer can have multiple CustomerAccounts (or none at all). We have observed that there are certain Business Invariants associated with objects related to Customer and other Invariants related to CustomerAccount. To adhere to the precise definition of Invariants, if we cannot identify any that connect Customer and CustomerAccount together, then it is advisable to separate them into distinct Aggregates. This same consideration applies to any other cluster of objects we introduce: Do they share any Invariants with the existing Aggregates?

ll Aggregates
All Aggregates

It’s always a good practice to keep Aggregates as small as possible. Aggregate members are typically persisted together in storage, such as a database, and adding too many tables within a single transaction can be problematic. In this context, it’s evident that we should define a Repository at the level of the Aggregate and persist all its members exclusively through that Repository, as demonstrated in the example below.

CustomerRepository

type CustomerRepository interface {
	Search(ctx context.Context, specification CustomerSpecification) ([]Customer, error)
	Create(ctx context.Context, customer Customer) (*Customer, error)
	UpdatePerson(ctx context.Context, customer Customer) (*Customer, error)
	UpdateCompany(ctx context.Context, customer Customer) (*Customer, error)
	//
	// and many other methods
	//
}

We can define Person and Company as Entities (or Value Objects). However, even if they have their own Identity, we should update them through the Customer by using the CustomerRepository. Working directly with Person or Company or persisting them without Customer and other related objects can break Business Invariants. It’s important to ensure that transactions apply to all of them together or, if necessary, can be rolled back as a whole. Deletion of an Aggregate must also occur as a cohesive unit. In other words, when we delete the Customer Entity, we should also delete the Person and Company Entities, as they don’t have a reason to exist separately.

As you can see, the size of an Aggregate should neither be too small nor too large. It should be precisely bounded by Business Invariants. Everything within that boundary must be used together, and anything outside that boundary belongs to other Aggregates.

Relationships
#

As you could see previously in the article, there are relationships between Aggregates. These relationships should always be represented in the code, but they should be kept as simple as possible. To avoid complex connections, it’s best to avoid referencing Aggregates directly and instead use Identities for relationships. You can see an example of this in the code snippet below.

A Wrong Approach with Referencing

type CustomerAccount struct {
	id        uuid.UUID
	//
	// some fields
	//
	customer Customer // the wrong way with referencing
	//
	// some fields
	//
}

The Right Approach with Identity

type CustomerAccount struct {
	id        uuid.UUID
	//
	// some fields
	//
	customerID uuid.UUID // the right way with identity
	//
	// some fields
	//
}

The other problem may be with the direction of relationships. The best scenario is when we have a unidirectional connection between them and we avoid any bidirectional relationships. Deciding on the direction of these relationships is not always easy, and it depends on the specific use cases within our Bounded Context.

For example, if we are developing software for an ATM where a user interacts with a CustomerAccount using a debit card, we might sometimes need to access the Customer by having its identity in the CustomerAccount. In another scenario, our Bounded Context might be an application that manages all CustomerAccounts for one Customer, where users can authorize and manipulate all BankAccounts. In this case, the Customer should contain a list of Identities associated with CustomerAccounts.

Aggregate Root
#

All the Aggregates discussed in this article share the same names as some of the Entities, such as the Customer Entity and the Customer Aggregate. These unique Entities are known as Aggregate Roots and are the primary objects within the Aggregates. An Aggregate Root serves as a gateway for accessing all other Entities, Value Objects, and Collections contained within the Aggregate. It is considered the main entry point for interacting with the Aggregate.

It is essential to follow the rule that members of an Aggregate should not be changed directly but through the Aggregate Root. The Aggregate Root should expose methods that represent its rich behaviors, define ways to access attributes or objects within it, and provide methods for manipulating that data. Even when an Aggregate Root returns an object, it should return only a copy of it to maintain encapsulation and control over the Aggregate’s internal state.

Rich Behaviors

func (ca *CustomerAccount) GetIBANForCurrency(currency Currency) (string, error) {
	for _, account := range ca.accounts {
		if account.IsForCurrency(currency) {
			return account.iban, nil
		}
	}
	return "", errors.New("this account does not support this currency")
}

func (ca *CustomerAccount) MarkAsDeleted() error {
	if ca.accounts.HasMoney() {
		return errors.New("there are still money on bank account")
	}
	if ca.accounts.InDebt() {
		return errors.New("bank account is in debt")
	}

	ca.isDeleted = true

	return nil
}

func (ca *CustomerAccount) CreateAccountForCurrency(currency Currency) error {
	if ca.accounts.HasCurrency(currency) {
		return errors.New("there is already bank account for that currency")
	}
	ca.accounts = append(ca.accounts, NewBankAccount(currency))

	return nil
}

func (ca *CustomerAccount) AddMoney(amount int, currency Currency) error {
	if ca.isDeleted {
		return errors.New("account is deleted")
	}
	if ca.isLocked {
		return errors.New("account is locked")
	}

	return ca.accounts.AddMoney(amount, currency)
}

Within an Aggregate, there are typically multiple Entities and Value Objects, each with its own Identity. These Identities can be classified into two types: Global Identity and Local Identity.

  1. Global Identity: The Aggregate Root within the Aggregate has a Global Identity. This Identity is unique globally, meaning that there is no other Entity in the entire application with the same Identity. It is permissible to reference the Global Identity of the Aggregate Root from outside the Aggregate, allowing external parts of the application to interact with the Aggregate.

  2. Local Identity: All other Entities and Value Objects within the Aggregate have local Identities. These Identities are unique only within the context of the Aggregate itself. They may be reused for Entities and Value Objects outside the Aggregate. Local Identities are known and managed solely by the Aggregate, and they should not be referenced or exposed outside the boundaries of the Aggregate.

By distinguishing between Global and Local Identities, we can maintain consistency and avoid conflicts within the Aggregate while ensuring that the Aggregate Root remains uniquely identifiable throughout the application.

Global and Local Identity

type Person struct {
	id uuid.UUID // local identity
	//
	// some fields
	//
}

type Company struct {
	id uuid.UUID // local identity
	//
	// some fields
	//
}

type Customer struct {
	id      uuid.UUID // global identity
	person  *Person
	company *Company
	//
	// some fields
	//
}

Conclusion
#

An Aggregate is a concept in the domain that follows certain rules called Business Invariants. These rules must always be true, no matter the state of the application. They define the limits or boundaries of an Aggregate. When it comes to storing or removing data, all parts of an Aggregate must be handled together. Aggregate Roots act as entry points to the other elements within the Aggregate. To access these elements, you must go through the Aggregate Roots; you can’t reach them directly.

Useful Resources
#

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

Related

Practical DDD in Golang: Module

·2203 words·11 mins· loading · loading
At first glance, Modules may not seem like a typical software development pattern, especially when we often associate patterns with specific code structures or behaviors. This can be particularly confusing when considering Go Modules. These modules consist of closely related Go Packages, are versioned, and released together, serving as a form of dependency management in Go. Since both Go Modules and Packages impact the project’s structure, it raises the question of their relationship with the DDD pattern known as Module. Indeed, there is a connection between them. The Structure # In Go, we use Packages to organize and group our code. Packages are closely tied to the folder structure within our projects, although there can be variations in naming. These variations arise because we have the flexibility to name our package differently than the actual folder it resides in. Folder pkg/access/domain/model package access_model import ( "github.com/google/uuid" ) type User struct { ID uuid.UUID // // some fields // } Folder pkg/access/domain/service package access_service import ( "project/pkg/access/domain/model" ) type UserService interface { Create(user access_model.User) error // // some methods // } In the example above, you can observe slight differences between folder and package naming. In some cases, when dealing with multiple model packages, I add prefixes from my DDD Modules to facilitate referencing these packages within the same file. Now, we can start to gain a better understanding of what a DDD Module would be in the previous example. In this context, the Module encompasses the access package along with all its child packages. project ├── cmd │ ├── main.go ├── internal │ ├── module1 │ │ ├── infrastructure │ │ ├── presentation │ │ ├── application │ │ ├── domain │ │ │ ├── service │ │ │ ├── factory │ │ │ ├── repository │ │ │ └── model │ │ └── module1.go │ ├── module2 │ │ └── ... │ └── ... ├── pkg │ ├── module3 │ │ └── ... │ ├── module4 │ │ └── ... │ └── ... ├── go.mod └── ... The folder structure in the diagram above represents my preferred project structure for implementing Domain-Driven Design in Go. While I may create different variations of certain folders, I strive to maintain consistent DDD Modules. In my projects, each Module typically consists of no more than four base packages: infrastructure, presentation, application, and domain. As you can see, I adhere to the principles of Hexagonal Architecture. In this structure, I place the infrastructure package at the top. This is because, by following Uncle Bob’s Dependency Inversion Principle, my low-level Services from the infrastructure layer implement high-level interfaces from other layers. With this approach, I ensure that I define a Port, such as the UserRepository interface, in the domain layer, while the actual implementation resides in the infrastructure layer. There can be multiple Adapters for this implementation, like UserDBRepository or UserFakeRepository.

Practical DDD in Golang: Domain Event

·1907 words·9 mins· loading · loading
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.

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.