Introduction
In the past few weeks, I received a lot of private and public questions about the first article of the Golang and Domain-Driven Design series I wrote.
Most of the questions I received privately were about how to write an entire Golang project (or code in general) using DDD tactical patterns.
So I decided to address some of those questions, and I am going to explain more widely my vision about Golang and DDD describing some code patterns I use and trade-offs I always consider.
Tactical design
Before starting, I think it is essential to define what tactical design is.
The tactical design describes a group of patterns to use to shape as code the invariants and models defined in a domain analysis that is often driven by the strategic design. The end goal of applying those patterns is to model the code in a simple but expressive and safe way.
The tactical patterns are well known in communities of languages such as PHP, Java, and C# because the resources available today about DDD patterns are mostly in OOP, but this doesn’t mean that it is hard or wrong to adapt those concepts in languages that use different paradigms.
The patterns I am going to cover in this article are the most known and used:
- Value Object (or value type)
- Entity, Aggregate and AggregateRoot
- Repository
Before describing one by one all the patterns derived from the DDD literature, I want to take a different direction.
I would start from the concept of the always valid state because it is going to influence the way of writing code.
The always valid state
The idea around the always valid state is that a type shouldn’t be created when it is not compliant with the invariants of its context.
There are many ways to achieve this goal: adding a validation HTTP middleware, creating a function that validates the CLI input, or coupling the validation with the type that protects the invariants.
Each way has its bonus and malus; it is on us to evaluate those options and pick the best for our scenario, considering the programming language to define those rules too.
I am saying this because, in Golang, there’s no way to prevent the creation of a type in an invalid state.
The unachievable always valid state
Let’s quickly define a domain and let’s model its main components as code.
XYZ is a product that helps you to organize your browser tabs and bookmarks to facilitate the interaction with the tabs you open the most. It allows the customer to create collections of tabs and share those collections in workspaces creating an easy, cataloged, and specialized user experience for each workspace.
The domain is all about tabs and bookmarks.
After having defined the ubiquitous language of our domain during its analysis, one of the elements we need to code is the title of the tab.
The title MUST be a string between 1 and 50 chars and MUST NOT be nullable.
The tab and its title may be coded in this way:
// tab/tab.go
package tab
type Tab struct {
Title string
}
// cmd/app/main.go
package main
import "tab"
func main() {
t := Tab{Title:""}
// ...
}
In this first iteration, only a few lines of code were needed, pretty neat!
But in the main.go file has just been created a Tab with an empty title; this case should never happen in our domain since there are invariants that need to be protected.
It requires a fix.
It’s possible to protect the domain invariants adding some validation rules:
// tab/tab.go
package tab
import (
"errors"
)
type Tab struct {
Title string
}
func New(t string) (*Tab, error) {
switch l := len(t); {
case l < 1:
return nil, errors.New("tab: could not use title less than 1 char")
case l > 50:
return nil, errors.New("tab: could not use title more than 50 char")
default:
return &Tab{Title:t}, nil
}
}
// cmd/app/main.go
package main
import "tab"
func main() {
t, err := Tab.New("a valid title")
if err != nil {
panic(err)
}
t.Title = ""
// ...
}
It seems better than before now; A validation rule to protect the domain invariants is in the New factory function.
But again, we were still able to invalidate the invariants of the title because the language mechanism provided by Go about the exported identifiers allowed it.
This could be prevented making the fields of the Tab type unexported since it is not possible to access unexported identifiers outside of the package hosting it:
// tab/tab.go
package tab
import (
"errors"
)
type Tab struct {
title string
}
func New(t string) (*Tab, error) {
switch l := len(t); {
case l < 1:
return nil, errors.New("tab: could not use title less than 1 char")
case l > 50:
return nil, errors.New("tab: could not use title more than 50 char")
default:
return &Tab{title:t}, nil
}
}
// cmd/app/main.go
package main
import "tab"
func main() {
t, err := tab.New("a valid title")
if err != nil {
panic(err)
}
t2 := &tab.Tab{}
// ...
}
A valid Title is finally created and assigned to the variable t, which we can’t change anymore apparently.
But t2 in an invalid state, it doesn’t have a title, or to be more precise, it has the zero value of the string type.
It is possible to be even more defensive and returning an error every time a zero value of the Title type is given in any function of the application.
You start noticing that there is no way to achieve the always valid state by design in Golang because of its mechanism.
Instead of digging our own grave trying to achieve the unachievable goal, it is possible to address this problem from a different point of view.
Finding a balance
As an engineer, it is part of my daily work to evaluate trade-offs minimizing as much as possible the tech debt balancing between the safety of a domain type and the simplicity that another alternative may bring.
One of the philosophies I embraced from the Golang community is the package APIs design; a package should be designed having in mind the usage that users are going to do of it.
This philosophy choice is the perfect balance to use for type design and for implementing tactical patterns.
I apply this philosophy exposing the APIs needed to interact with the package safely and empowering the users to decide the usage of it, meaning that it is still possible to create the types in an invalid state if the users won’t use the functions available in the package.
Anyway, there are still some circumstances where acting more strictly around the APIs of a package makes sense, particularly in a company context.
For example, in a team that pushes features non-stop, it’s hard to keep track of all the changes, and adding more protection in some packages helps to respect the domain invariants reducing the number of bugs.
But before adding defensive code, it’s essential to make this need evident, measuring the number of bug, outages, and incidents caused by this lack of protection, since those extra-defensive approaches increase complexity.
Note: Google defined an excellent way you may extend and use to track those occurrences, click here to read more about SRE.
How to implement tactical design patterns using Golang
My main goal from now on is to implement the DDD tactical patterns without falling in the OOP trap, and taking advantage of the language mechanism to reduce the API surface and package coupling.
Value Object (or value type)
I do refer to them as value type in Golang since, in this language, there isn’t the concept of an object; the object word may trick you to think in object-oriented.
The value type is a pattern described in the DDD literature used to group related things as an immutable unit, comparable by the properties that compose it.
package tab
import (
"errors"
"fmt"
"strings"
)
const (
minTitleLength = 1
maxTitleLength = 50
)
var (
// Errors used when an invalid title is given
ErrInvalidTitle = errors.New("tab: could not use invalid title")
ErrTitleTooShort = fmt.Errorf("%w: min length allowed is %d", ErrInvalidTitle, minTitleLength)
ErrTitleTooLong = fmt.Errorf("%w: max length allowed is %d", ErrInvalidTitle, maxTitleLength)
)
// Title represents a tab title
type Title string
// NewTitle returns a title and an error back
func NewTitle(d string) (Title, error) {
switch l := len(strings.TrimSpace(d)); {
case l < minTitleLength:
return "", ErrTitleTooShort
case l > maxTitleLength:
return "", ErrTitleTooLong
default:
return Title(d), nil
}
}
// String returns a string representation of the title
func (t Title) String() string {
return string(t)
}
// Equals returns true if the titles are equal
func (t Title) Equals(t2 Title) bool {
return t.String() == t2.String()
}
Value type design choices and advantages
A value type is beneficial for representing concepts from the domain as code, with a built-in validation of the domain invariants.
The APIs exposed by the Title type allow us to build it in a valid state, since the given NewTitle factory function checks the validity of the incoming attributes.
The major benefit of coupling validation rules with a value type is an easier maintainability of the codebase.
In fact, there will be no more duplicated validation logic since we’ll keep reusing the code from the value type over and over. For example, when decoding a JSON request body:
type addTabReq struct {
Title tab.Title `json:"tab_title"`
}
func (r *addTabReq) UnmarshalJSON(data []byte) error {
type clone addTabReq
var req clone
if err := json.Unmarshal(data, &req); err != nil {
return err
}
var err error
if r.Title, err = tab.NewTitle(req.Title.String()); err != nil {
return err
}
return nil
}
A value type also exposes an Equals method to ensure that the comparisons with other values are made using all the fields it contains and not a memory address, reducing the number of bugs and code duplication for values comparison.
Note: In the title example there is only one field, but a value type can also be composed by several fields and represented as a struct.
The value types are designed as immutable; that’s why the Title type has only value receivers on the methods.
To let people understand the reasons for choosing the immutable design for value types, I always do to the 0 example.
0 is immutable; when a math operation adds a number to 0, it doesn’t change the fact that 0 is still 0.
For the same reason, a value type does not change. It is unique for what it represents.
On top of the design concept, having an immutable value type is safer.
When using a value type as a field of a model, the immutable design keeps it safe from side effects due to a mutable shared state, which is a common source of bugs, especially in a concurrent programming language such as Go.
Where to place it
I place the value type files in the package that owns the invariants implemented by the value types since those should not be shared across packages.
📂 app
┗ 📦tab
┣ 📜 tab.go
┗ 📜 title.go
Some times it may make sense to reuse them in different packages, mainly when the value type is representing pretty general rules (such as an email). But it’s up to you to decide to keep the packages decoupled or not.
Hint: there’s a Go proverb about this you may want to consider A little copying is better than a little dependency
Entity, Aggregate and AggregateRoot
Those patterns are similar, and this similarity leads to some confusion when approaching them for the first time.
Even if similar, those have clear and different use cases, and their usage can be combined to achieve an optimal model design.
Entity
An entity is a domain type that is not defined by its attributes but rather by its identifier.
package tab
import (
"time"
)
// Tab represents a tab
type Tab struct {
ID ID
Title Title
Description Description
Icon Icon
Link Link
Created time.Time
Updated time.Time
}
// New returns a tab created for the first time
func New(id ID, title Title, description Description, icon Icon, link Link) *Tab {
return &Tab{
ID: id,
Title: title,
Description: description,
Icon: icon,
Link: link,
Created: time.Now(),
}
}
// Update updates a tab with new attributes
func (t *Tab) Update(title Title, description Description, icon Icon, link Link) {
t.Title = title
t.Description = description
t.Icon = icon
t.Link = link
t.Updated = time.Now()
}
Entity design choices and advantages
At first view, an entity may look like a value type composed by more fields, but the main difference between an entity and a value type relates to the concepts of the identity. An entity has an identity(ID in the Tab example), instead, a value type has no identity since it represents an identifier of a value.
Having an identity means that a type can change over time and still be representing the same original one, this vision leads to the conclusion that an entity should be designed as a mutable type, and from a code perspective, this leads to the usage of pointer receivers.
Entities are the core domain components, and those need to ensure the validity of the domain concept they are representing.
As an example, consider the New factory function and the Update method of the Tab. Those APIs are going to deal with the created and updated attributes of the Tab without requiring a time.Time value to be passed by, because the type itself needs to guarantee its correctness.
To protect more easily the invariants and to spread the usage of the ubiquitous language of the domain, an entity should use value types as building blocks. Note: the Created and Updated field in the Tab struct are using the built-in value type time.Time, since by design it is immutable and representing the time of the domain
Aggregate
An Aggregate is a cluster of domain types glued together and treated as a single unit of work. An aggregate may also contain more aggregates.
Using the domain description shared at the beginning of the article, it is possible to use the Collection as an aggregate:
package collection
import (
"collection/tab"
"time"
)
// Collection represent a collection
type Collection struct {
ID ID
Name Name
Tabs []*tab.Tab
Created time.Time
Updated time.Time
}
// New returns a collection created for the first time
func New(id ID, name Name) *Collection {
return &Collection{
ID: id,
Name: name,
Tabs: make([]*tab.Tab, 0),
Created: time.Now(),
}
}
// Rename renames a collection
func (c *Collection) Rename(name Name) {
c.Name = name
c.Updated = time.Now()
}
// AddTabs adds tabs to the collection
func (c *Collection) AddTabs(tabs ...*tab.Tab) {
c.Tabs = append(c.Tabs, tabs...)
c.Updated = time.Now()
}
// RemoveTab removes a tab if it exists
func (c *Collection) RemoveTab(id tab.ID) bool {
for i, t := range c.Tabs {
if t.ID == id {
c.Tabs[i] = c.Tabs[len(c.Tabs)-1]
c.Tabs[len(c.Tabs)-1] = nil
c.Tabs = c.Tabs[:len(c.Tabs)-1]
c.Updated = time.Now()
return true
}
}
return false
}
// FindTab returns a tab if it exists
func (c *Collection) FindTab(id tab.ID) (*tab.Tab, bool) {
for _, t := range c.Tabs {
if t.ID == id {
return t, true
}
}
return nil, false
}
// UpdateTab updates a tab if it exists
func (c *Collection) UpdateTab(t *tab.Tab) bool {
for i, tb := range c.Tabs {
if tb.ID == t.ID {
c.Tabs[i] = t
c.Updated = time.Now()
return true
}
}
return false
}
Aggregate design choices and advantages
The aggregate shares the same design choice of the entity;
It is mutable, it has an identity, and uses domain value types as building blocks.
It also uses the ubiquitous language defined for the types incorporated, and it enriches it, adding its one.
The difference between an entity and an aggregate is that it can be a cluster of more domain types, and it may also group more aggregates.
AggregateRoot
The aggregate root represents the same concepts of the aggregate with an only difference: It represents the root of the aggregate that can be used to interact in the domain use cases.
It means, in an extremely simplified way, that the aggregate root owns the identifier used to retrieve it from the database.
As for the aggregate, an entity can be used as an aggregate root depending on the domain.
In the domain shared at the beginning of the article, the Workspace is the aggregate root:
package workspace
import (
"time"
"workspace/collection"
"workspace/collection/tab"
)
// Workspace represent a workspace
type Workspace struct {
ID ID
Name Name
CustomerID CustomerID
Collections []*collection.Collection
Created time.Time
Updated time.Time
}
// New returns a workspace created for the first time
func New(id ID, name Name, customerID CustomerID) *Workspace {
return &Workspace{
ID: id,
Name: name,
CustomerID: customerID,
Collections: make([]*collection.Collection, 0),
Created: time.Now(),
}
}
// Rename change the name of a workspace
func (w *Workspace) Rename(name Name) {
w.Name = name
w.Updated = time.Now()
}
// AddCollections add a collection
func (w *Workspace) AddCollections(collections ...*collection.Collection) {
w.Collections = append(w.Collections, collections...)
w.Updated = time.Now()
}
// RemoveCollection removes a collection if it exists
func (w *Workspace) RemoveCollection(id collection.ID) bool {
for i, coll := range w.Collections {
if coll.ID == id {
w.Collections[i] = w.Collections[len(w.Collections)-1]
w.Collections[len(w.Collections)-1] = nil
w.Collections = w.Collections[:len(w.Collections)-1]
w.Updated = time.Now()
return true
}
}
return false
}
// RenameCollection renames a collection if it exists
func (w *Workspace) RenameCollection(id collection.ID, name collection.Name) bool {
for _, coll := range w.Collections {
if coll.ID == id {
coll.Rename(name)
w.Updated = time.Now()
return true
}
}
return false
}
// ...
Where to place them
Those three patterns are the most important when representing the domain as code because those are the subject of it, and all the features and invariants revolve around them.
Since those are so critical they deserve their own package, acting as entry point to all the domain types that depends on them.
📂 app
┗ 📦workspace
┣ 📜id.go
┣ 📜name.go
┣ 📜workspace.go
┣ 📜 ...
┗ 📦collection
┣ 📜id.go
┣ 📜name.go
┣ 📜collection.go
┣ 📜 ...
┗ 📦tab
┣ 📜id.go
┣ 📜title.go
┣ 📜description.go
┣ 📜 ...
┗ 📜tab.go
In this structure I split the three because a tab, as the collection, does not need their parent in order to exists and be defined. And, most important, having them split in different packages allow to do not import all the domain types if only one is needed.
Repository
The repository pattern is probably the most known from the DDD world.
This pattern represents a mechanism which is used to map domain types with the persistence, exposing APIs mimicking an interaction with an in-memory slice.
I usually represent it as an interface that looks like this:
package tab
import "errors"
var(
//Errors returned by the repository
ErrRepoNextID = errors.New("tab: could not return next id")
ErrRepoList = errors.New("tab: could not list")
ErrNotFound = errors.New("tab: could not find")
ErrRepoGet = errors.New("tab: could not get")
ErrRepoAdd = errors.New("tab: could not add")
ErrRepoRemove = errors.New("tab: could not remove")
)
type Repo interface {
// NextID returns the next free ID and an error in case of failure
NextID() (ID, error)
// List returns a tab slice and an error in case of failure
List() ([]*Tab, error)
// Find returns a tab or nil if it is not found and an error in case of failure
Find(ID) (*Tab, error)
// Get returns a tab and error in case is not found or failure
Get(ID) (*Tab, error)
// Add persists a tab (already existing or not) and returns an error in case of failure
Add(*Tab) error
// Remove removes a tab and returns and error in case is not found or failure
Remove(ID) error
}
If the application has separated read and write operations, it can be represented like this:
package tab
// ...
type ReadRepo interface {
// List returns a tab slice and an error in case of failure
List() ([]*Tab, error)
// Find returns a tab or nil if it is not found and an error in case of failure
Find(ID) (*Tab, error)
// Get returns a tab and error in case is not found or failure
Get(ID) (*Tab, error)
}
type WriteRepo interface {
// NextID returns the next free ID and an error in case of failure
NextID() (ID, error)
// Add persists a tab (already existing or not) and returns an error in case of failure
Add(*Tab) error
// Remove removes a tab and returns and error in case is not found or failure
Remove(ID) error
}
Repository design choices and advantages
The repository pattern offers multiple advantages from a design point of view as well as technical.
Adopting this pattern allows decoupling an application from a specific database (such as MySQL, MongoDB or Google Spanner) and these benefits appear during testing, since it’s possible to write an in-memory repo implementation, and during a migration to a different database.
Migrating an application to use a different database is an expensive operation all the times, but it is possible to reduce the cost of it using this pattern because only one repository implementation needs to be created/updated to use the new database and the repository interface protects from updating the whole codebase.
To facilitate the migration is also essential to place the repository and its errors in the same package, so, even for the error checking, only the package that owns the repository interface is coupled to the whole application.
Is it even possible to enrich the already specified repository error from the package that holds the repository implementation when there’s the need:
func (r *MysqlRepo) Add(t *Tab) error {
// ...
return fmt.Errorf("%w: %s", tab.ErrRepoAdd, "a more detailed reason here")
}
From a design point of view applying the repository pattern, helps to define clear boundaries of your context and keep it decoupled from unrelated sub-domains since its APIs use mainly, but not only, an aggregate root and its ID value type.
The repository patterns APIs also enforce the usage and establishment of the ubiquitous language, for example, it is possible to use filter parameters in the read operations and specify more specific APIs for that need.
Where there is a need for filtering, most of the time, it is possible to declare a specialized function that uses the ubiquitous language, decreasing the cognitive load and the complexity of the application.
// Don't
repo.List(repo.Filter{"active": true})
// Do
repo.ListActive()
Where to place it
As always, I do place the files regarding the repository interface in the package that owns the aggregates.
But the files regarding the implementation, so the one who is going to execute a query over a MySQL database for example, I usually put it into the internal directory since those are highly coupled to the application and should not be reused in different places.
📂 app
┣ 📦internal
┃ ┗ 📦tab
┃ ┗ 📜repo.go // here a MySQL implementation
┗ 📦tab
┗ 📜repo.go // here a the interface and the errors
Conclusion
Tactical design is the 101 when applying DDD methodology to the code, but it is essential to do not force a language to follow an idiom that was not meant for it. It is possible to reach the same goal without using DTO, always valid object or other patterns derived from the object-oriented world.
This, combined with a strategic design approach in Golang gives you a clear and minimal DDD codebase.
Do you disagree with me and my design approach? Feel free to share what are the main decisions you make when developing a codebase in Go in the comment below!
Note: this article part of a series about DDD and Golang, if you are interested to hear more on a specific topic let me know!