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!