Practical DDD in Golang: Entity
- 10 minutes read - 2068 wordsThe story about DDD in Go continues by introducing one of the most common building blocks — the Entity.

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.
We always maintain the Repository interface in the domain layer. Within this layer (which is typically the lowest one in the layered architecture I use), certain Domain Services may depend on Repositories, so they should be aware of their existence. Repositories provide a contract that ensures we work with Entity objects from our domain layer, at least when dealing with them externally. Inside the Repository, we can handle things as needed, as long as we deliver accurate results.
With this structure, I’ve consistently managed to separate my business logic from the underlying storage. When it comes to making changes to the database, only the mapping methods, which transform DTOs to Entities and vice versa, need to be modified.
Additional examples of Entities
type Currency struct {
ID uint
Code string
Name string
HtmlCode string
}
type Person struct {
ID uint
FirstName string
LastName string
DateOfBirth time.Time
}
type BankAccount struct {
ID uint
IsLocked bool
Wallet Wallet
Person Person
}
In some instances, Entities might encompass intricate business logic, drawing data from various sources such as relational databases, NoSQL databases, and external APIs. Particularly in these scenarios, the concept of segregating the business layer from technical details proves to be extremely beneficial.
Identity
The primary distinction from Value Objects is the concept of Identity. Entities possess Identities, which is their sole property that can establish their uniqueness. Even if two Entities differ slightly in one or more of their fields, they are considered the same Entity if they share the same Identity. Therefore, when we assess their equality, we solely examine their Identities.
Checking Equality in Entity
type Currency struct {
ID uint
Code string
Name string
HtmlCode string
}
func (c Currency) IsEqual(other Currency) bool {
return other.ID == c.ID
}
There are three types of Identities:
-
Application-generated Identities: In this case, we create new Identities for entities before they are stored in the database. UUIDs are commonly used for this purpose.
-
Natural Identities: These involve using existing biological or unique identifiers when working with real-world entities, such as Social Security Numbers.
-
Database-generated Identities: This is the most common approach, even when the option to implement the previous two solutions is available. In this approach, Identities are generated by the database.
Application-generated Identities
type Currency struct {
ID uuid.UUID
Code string
Name string
HtmlCode string
}
func NewCurrency() Currency {
return Currency{
ID: uuid.New(), // generate new UUID
}
}
Natural Identities
type Person struct {
SSN string // social security number
FirstName string
LastName string
DateOfBirth time.Time
}
Database-generated Identities
type BankAccount struct {
ID uint
IsLocked bool
Wallet Wallet
Person Person
}
type BankAccountGorm struct {
ID uint `gorm:"primaryKey;autoIncrement:true"`
IsLocked bool
Amount int
CurrencyID uint
PersonID uint
}
I prefer to use only numerical values for indexing and querying. In many cases, when working with application-generated keys or natural keys, we encounter text-based data and need to find a way to accurately map these texts to numerical values in a database.
Since Identity is the primary distinction between Entity and Value Object, you might guess that this line of separation can be easily blurred. Indeed, depending on the Bounded Context, an object can easily transition from being an Entity to a Value Object.
Transaction Service
type Currency struct {
ID uint
Code string
Name string
HtmlCode string
}
Web Service
type Currency struct {
Name string
HtmlCode string
}
Just as seen in the example above, Currency
can function as a central Entity within a specific Bounded Context,
such as a Transaction Service
or Exchange Service
. However, in situations where we require it for UI formatting,
Currency
can be employed as a straightforward Value Object.
Validation
In contrast to a Value Object, an Entity can alter its state over time. This implies that we need to perform ongoing validation checks whenever we intend to modify an Entity.
Validation with each change
type BankAccount struct {
ID uint
IsLocked bool
Wallet Wallet
//
// some fields
//
}
func (ba *BankAccount) Add(other Wallet) error {
if ba.IsLocked {
return errors.New("account is locked")
}
//
// do something
//
}
Yes, I understand. In the example above, we can directly access Wallet and modify it without using the Add method. Personally, I’m not a big fan of Getters and Setters in Go. I find it hard to maintain when there are many functions that either return or set values. In such cases, I trust the engineers’ judgment to understand how they should change the state of the Entity if methods are already available. However, I leave this decision to each developer to make on their own. Using getters and setters with private fields is also a viable solution.
Pushing behaviors
The primary goal of DDD is to closely mirror the business processes. Therefore, it shouldn’t come as a surprise when we encounter numerous methods within our domain layer. These methods can belong to various objects. Since Entities hold the most intricate state compared to all other code components, they may also feature the most functions to represent their extensive behaviors.
In some instances, we might observe that a couple of fields within an Entity consistently interact with each other. If we use one of them to enforce a particular business rule, it’s likely that we’ll also need the other one. In such cases, we can always group these fields into a single unit, a Value Object, and delegate its management to the Entity. However, we must approach this carefully to ensure a clear separation of concerns between the Entity and Value Objects.
One Wrong Approach
type Wallet struct {
Amount int
Currency Currency
}
type BankAccount struct {
ID uint
IsLocked bool
Wallet Wallet
//
// some fields
//
}
func (ba *BankAccount) Deduct(other Wallet) error {
if ba.IsLocked {
return errors.New("account is locked")
}
if !other.Currency.IsEqual(ba.Wallet.Currency) {
return errors.New("currencies must be the same")
}
if other.Amount > ba.Wallet.Amount {
return errors.New("insufficient funds")
}
ba.Wallet = Wallet{
Amount: ba.Wallet.Amount - other.Amount,
Currency: ba.Wallet.Currency,
}
return nil
}
In the example above, we can see that the BankAccount
Entity takes on more responsibility from the Wallet
Value
Object. It’s clear when we check if the BankAccount
is locked or not. However, verifying the equality of Currency
and ensuring there’s enough amount in the Wallet raises a code smell. In such situations, I relocate the entire
deduction logic to the Value Object, except for the crucial task of verifying if the BankAccount
is locked.
This way, the Wallet
gets its share of code to validate and deduct the amount.
The Right Approach
type Wallet struct {
Amount int
Currency Currency
}
func (w Wallet) Deduct(other Wallet) (*Wallet, error) {
if !other.Currency.IsEqual(w.Currency) {
return nil, errors.New("currencies must be the same")
}
if other.Amount > w.Amount {
return nil, errors.New("insufficient funds")
}
return &Wallet{
Amount: w.Amount - other.Amount,
Currency: w.Currency,
}, nil
}
type BankAccount struct {
ID uint
IsLocked bool
Wallet Wallet
//
// some fields
//
}
func (ba *BankAccount) Deduct(other Wallet) error {
if ba.IsLocked {
return errors.New("account is locked")
}
result, err := ba.Wallet.Deduct(other)
if err != nil {
return err
}
ba.Wallet = *result
return nil
}
This way, the Wallet Value Object can be associated with any other Entity or Value Object and still facilitate deductions based on its internal state. Conversely, the BankAccount can offer an additional method for deducting amounts from locked accounts without duplicating the same logic. An Entity has the flexibility to delegate its behaviors to other building blocks, such as Domain Services.
I transfer these methods to Services in two scenarios. The first situation arises when the behavior is too intricate, possibly involving interactions with Specifications, Policies, other Entities, or Value Objects. It might also rely on results obtained from Repositories or other Services. The second case involves behaviors that aren’t overly complex but lack a clear place to reside. They could potentially belong to one Entity, another Entity, or even a Value Object.
Another Wrong Approach
type Currency struct {
ID uint
//
// some fields
//
}
type ExchangeRatesService struct {
repository ExchangeRatesRepository
}
func (s *ExchangeRatesService) Exchange(to Currency, other Wallet) (Wallet, error) {
//
// do something
//
}
When the business logic becomes too complex, my practice is to transfer it to a distinct Domain Service, as shown with the ExchangeRatesService in the example above. This approach has consistently allowed me to enhance my domain layer by introducing new Domain Policies.
At times, it seems like the right course of action to delegate behavior to other building blocks. However, it’s crucial to exercise caution when doing so. Transferring too many behaviors from Entities to Domain Services can give rise to another code smell known as the Anemic Domain Model.
Another Right Approach
type TransactionService struct {
//
// some fields
//
}
func (s *TransactionService) Add(account *BankAccount, second Wallet) error {
//
// do something
//
}
The example above illustrates the TransactionService
Domain Service, which assumes responsibility from the
BankAccount
Entity. When there’s no need to validate complex business invariants, this behavior doesn’t necessarily
belong in a Domain Service. Determining the appropriate location for a specific behavior is akin to an exercise
that may appear challenging at first but becomes more intuitive with practice. Even today, I occasionally face
difficulties in pinpointing the ideal location, but more often than not, I can structure the code as it should be.
Conclusion
While we commonly utilize them in various frameworks, it’s not always the best practice. Their role should be to represent our states and behaviors rather than merely mirroring the database schema. Entities provide us with valuable means to describe stateful real-world objects. In many instances, they serve as the core components of our applications, if not essential for our business logic to function properly.