Back

How To Implement Domain-Driven Design (DDD) in Golang

The easy way of learning how to use DDD in a Go application

by Percy Bolmér, September 1, 2021

Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

Microservices have become a very popular approach to build software in recent years. Microservices are used to build scalable and flexible software. However, randomly building microservices across many teams can cause a big frustration and complexity.

It wasn’t long ago that I hadn’t heard about Domain-Driven Design — DDD, but now it seems everyone is talking about it, everywhere I virtually go.

All images drawn in this article are drawn by Percy Bolmér, the Gopher is drawn by Takuya Ueda, inspired by the works of Renée French. The gopher has been modified in the images.

If you rather watch an video you can on YouTube

In this article, we will build an online Tavern from scratch while exploring the different parts of DDD, step by step. Hopefully, it will be easier to understand DDD when one piece is implemented at a time. The reason I want to take this approach is that reading about DDD makes my head explode, every single time. There are so many terms and it is so broad and unclear what is what.

If you don’t know why my head explodes when researching DDD, the graph below will probably help you realize it.

Keywords in DDD — Graph from Eric Evans book Domain-Driven Design: Tackling Complexity in the Heart of Software
Keywords in DDD — Graph from Eric Evans book Domain-Driven Design: Tackling Complexity in the Heart of Software

There is a reason why Eric Evans needed about 500 pages to explain it in Domain-Driven Design: Tackling Complexity in the Heart of Software. If you are interested in really learning DDD then don’t be afraid to read Evan’s book about it.

First off, I’d like to point out that this article describes my interpretation of DDD, and the implementation I show in the article is based on what has worked out best in my experience with Go-related projects. The implementation we will create is not in any way a community accepted best practice. I will also be naming folders in the project after the DDD methodology to make it easy to understand and follow, I’m not sure that’s how I would want a real repository to look like though. For this reason, I will also have a separate branch of the repository where I’ve fixed the structure, and that refactor will be explained in How to Structure DDD in Golang.

I’ve seen many heated discussions on the internet about DDD and how a correct implementation is done. One thing that struck me is that most of the time people seem to forget the purpose behind DDD, and instead end up arguing on small implementation details. I think the important thing is that the suggested methodology purposed by Evan is followed, and not if something is named X or Y.

DDD is a huge area and we will mostly look at the implementation of it, but before we implement anything I will make a quick recap of some of the aspects in DDD.

What is DDD

Domain-Driven Design is a way of structuring and modeling the software after the Domain it belongs to. What this means is that a domain first has to be considered for the software that is written. The domain is the topic or problem that the software intends to work on. The software should be written to reflect the domain.

DDD advocates that the engineering team has to meet up with the Subject Matter Experts, SME, which are the experts inside the domain. The reason for this is because the SME holds the knowledge about the domain and that knowledge should be reflected in the software. It makes pretty much sense when you think about it, If I were to build a stock trading platform, do I as an engineer know the domain well enough to build a good stock trading platform? The platform would probably be a lot better off if I had a few sessions with Warren Buffet about the domain

The architecture in the code should also reflect on the domain. We will see how when we start writing our Tavern.

The Journey Of a Gopher in DDD

Two gophers celebrating a successful collaboration. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Two gophers celebrating a successful collaboration. Gopher by Takuya Ueda, Original Go Gopher by Renée French

Let’s start learning how to implement DDD, and to start it out I’d like to tell you a story of a Gopher, Dante, who wants to create an online Tavern. Dante knows how to write code, but he does not know anything about how to run a tavern.

The day that Dante decides to start working on the tavern he reaches a problem, where and how to start? He goes out on a walk to think about his issues. While waiting at a stop sign, a man in a top hat approaches Dante and says

“It look’s like you’re worried about something young man, do you need help building a tavern perhaps?”

Dante and the top hat man have a great walk where they discuss the Tavern and how running one works.

Dante asks how regular drinkers are handled, and the top hat replies that it is called Customers, not drinkers.

The top hat also explains to Dante that a tavern needs a few things to operate, such as Customers, Employees, Banking, and Suppliers.

Domain, Model, Ubiquitous language and Sub-Domains

I hope you liked the story about Dante, there is a reason why I wrote it. We can use the story to explain some keywords used in DDD, words I find hard to explain without putting them into context, such as a short story.

A Domain modeling session has been held between Dante and the top hat. Top hat as a Subject Matter Expert and Dante as the engineer has discussed the domain space and found common ground. This is done to learn the Model, a model is an abstraction of the needed components to handle a domain.

When Dante and the top hat discussed the Tavern, they were talking about what we call Domain. The domain is the area that the software will be operating in, I would call the Tavern the Core/Root domain.

Top hat also points out that it is not called drinkers, rather Customers. This represents how finding a common language between the SMO and the developers is important. It will become very confusing if not everybody in the project has a Ubiquitous Language

We have also gotten a few Sub-domains which are the things top hat mentions that the tavern needs. A subdomain is a separate domain used to solve an area inside the root domain.

Coding a DDD application with Go — Entities and Value Objects

Entities & Value Objects explained with Mutable and Immutable state. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Entities & Value Objects explained with Mutable and Immutable state. Gopher by Takuya Ueda, Original Go Gopher by Renée French

It is time to start coding the Tavern now that we have everything we need to get started. Begin by setting up the project by creating a go module.

mkdir ddd-go
go mod init github.com/percybolmer/ddd-go

We will begin by creating a domain folder in which we will store all the subdomains that are needed, but before we implement any of the domains, we need to create yet another folder in the root domain. For demonstration purposes, we will name it entity as it will hold what is called entities in the DDD approach.

An entity is a struct that has an Identifier and that can change state, by changing state we mean that the values of the entity can change.

We will create two entities, to begin with, Person and Item. I do like to keep my entities in a separate package so that they can be used by all other domains.

Added an entities folder in the root domain
Added an entities folder in the root domain

To keep clean code I do like small files and making the folder structure easily navigatable. So I recommend creating two files, one for each entity, named after the entity. For now, it will only be the struct definitions in them, but later on, some other logic might get added.

The first entities we create for our domains to use
The first entities we create for our domains to use
// Package entities holds all the entities that are shared across all subdomains
package entity

import (
	"github.com/google/uuid"
)

// Person is a entity that represents a person in all Domains
type Person struct {
	// ID is the identifier of the Entity, the ID is shared for all sub domains
	ID uuid.UUID
	// Name is the name of the person
	Name string
	// Age is the age of the person
	Age int
}
person.go — A entity representing an person
package entity

import "github.com/google/uuid"

// Item represents a Item for all sub domains
type Item struct {
	ID          uuid.UUID 
	Name        string    
	Description string    
}
item.go — The item entity that defines the base for products

Great, now we have defined some entities and learned what an entity is. A struct with a unique identifier to reference it, which has states that can change.

There can be occurrences where we have structs that are immutable and do not need a unique identifier, these structs are called Value Objects. So structs without an identifier and persistent values after creation. Value objects are often found inside domains and used to describe certain aspects in that domain. We will be creating one value object for now which is Transaction, once a transaction is performed, it cannot change state.

In a real world application, it might be a good idea for transactions to be traceable by an ID, but this is for demonstration purposes

package valueobject

import (
	"time"
)

// Transaction is a payment between two parties
type Transaction struct {
	// all values lowercase since they are immutable
	amount    int
	from      uuid.UUID
	to        uuid.UUID
	createdAt time.Time
}
transaction.go — Payment between two parties defined

Aggregates — Combined Entities and Value Objects

Aggregates, a combination of Entities and Value Objects. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Aggregates, a combination of Entities and Value Objects. Gopher by Takuya Ueda, Original Go Gopher by Renée French

It’s time to look at the next component of DDD, aggregates. An Aggregate is a set of entities and value objects combined. So in our case, we can begin by creating a new aggregate which is Customer.

DDD aggregates are domain concepts (order, clinic visit, playlist) — Martin Fowler

The reason for an aggregate is that the business logic will be applied on the Customer aggregate, instead of each Entity holding the logic. An aggregate does not allow direct access to underlying entities. It is also common that multiple entities are needed to correctly represent data in real life, for instance, a Customer. It is a Person, but he/she can hold Products, and perform transactions.

An important rule in DDD aggregates is that they should only have one entity act as a root entity. What this means is that the reference of the root entity is also used to reference the aggregate. For our customer aggregate, this means that the Person ID is the unique identifier.

Let’s create a aggregate folder and a file named customer.go inside of it.

mkdir aggregate
cd aggregate
touch customer.go

In the file, we will add a new struct named Customer and it will hold all needed entities to represent a Customer. Notice that all fields in the struct begins with lower case letters, this is a way in Go to make an object inaccessible from outside of the package the struct is defined in. This is done because an Aggregate should not allow direct access to the data. Neither does the struct define any tags for how the data is formatted such as json.

This has been edited from earlier versions in the article where I decided to make all items accessible to make storing them in database easier, however since it breaks DDD rules as discussed with Miłosz Smółka at Threedotlabs, I’ve decided to change it.

// Package aggregates holds aggregates that combines many entities into a full object
package aggregate

import (
	"github.com/percybolmer/ddd-go/entity"
	"github.com/percybolmer/ddd-go/valueobject"
)

// Customer is a aggregate that combines all entities needed to represent a customer
type Customer struct {
	// person is the root entity of a customer
	// which means the person.ID is the main identifier for this aggregate
	person *entity.Person 
	// a customer can hold many products
	products []*entity.Item 
	// a customer can perform many transactions
	transactions []valueobject.Transaction 
}
customer.go — A aggregate that represents a customer and the needed entities inside it

I set all the entities as pointers, this is because an entity can change state and I want that to reflect across all instances of the runtime that has access to it. The value objects are held as nonpointers though since they cannot change state.

Great, now we have an aggregate created, we can move on.

Factories — Encapsulate complex logic

Factories, a pattern to create complex aggregates or repositories and services. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Factories, a pattern to create complex aggregates or repositories and services. Gopher by Takuya Ueda, Original Go Gopher by Renée French

Up until now, we have only defined different entities, value objects, and aggregates. It is time to start implementing some actual business logic, and we start with factories. The factory pattern is a design pattern that is used to encapsulate complex logic in functions that creates the wanted instance, without the caller knowing anything about the implementation details.

Factory Pattern is a very common pattern, you can use it even outside a DDD application, and you probably already have used it many times. A great example is the official Go Elasticsearch client. You insert a configuration into a NewClient function, which is a factory, that returns a client that is connected to an elastic cluster and you can insert/remove documents. Very simple to use for other developers, what there is quite a lot going on in the NewClient

func NewClient(cfg Config) (*Client, error) {
	var addrs []string

	if len(cfg.Addresses) == 0 && cfg.CloudID == "" {
		addrs = addrsFromEnvironment()
	} else {
		if len(cfg.Addresses) > 0 && cfg.CloudID != "" {
			return nil, errors.New("cannot create client: both Addresses and CloudID are set")
		}

		if cfg.CloudID != "" {
			cloudAddr, err := addrFromCloudID(cfg.CloudID)
			if err != nil {
				return nil, fmt.Errorf("cannot create client: cannot parse CloudID: %s", err)
			}
			addrs = append(addrs, cloudAddr)
		}

		if len(cfg.Addresses) > 0 {
			addrs = append(addrs, cfg.Addresses...)
		}
	}

	urls, err := addrsToURLs(addrs)
	if err != nil {
		return nil, fmt.Errorf("cannot create client: %s", err)
	}

	if len(urls) == 0 {
		u, _ := url.Parse(defaultURL) // errcheck exclude
		urls = append(urls, u)
	}

	// TODO(karmi): Refactor
	if urls[0].User != nil {
		cfg.Username = urls[0].User.Username()
		pw, _ := urls[0].User.Password()
		cfg.Password = pw
	}

	tp, err := estransport.New(estransport.Config{
		URLs:         urls,
		Username:     cfg.Username,
		Password:     cfg.Password,
		APIKey:       cfg.APIKey,
		ServiceToken: cfg.ServiceToken,

		Header: cfg.Header,
		CACert: cfg.CACert,

		RetryOnStatus:        cfg.RetryOnStatus,
		DisableRetry:         cfg.DisableRetry,
		EnableRetryOnTimeout: cfg.EnableRetryOnTimeout,
		MaxRetries:           cfg.MaxRetries,
		RetryBackoff:         cfg.RetryBackoff,

		CompressRequestBody: cfg.CompressRequestBody,

		EnableMetrics:     cfg.EnableMetrics,
		EnableDebugLogger: cfg.EnableDebugLogger,

		DisableMetaHeader: cfg.DisableMetaHeader,

		DiscoverNodesInterval: cfg.DiscoverNodesInterval,

		Transport:          cfg.Transport,
		Logger:             cfg.Logger,
		Selector:           cfg.Selector,
		ConnectionPoolFunc: cfg.ConnectionPoolFunc,
	})
	if err != nil {
		return nil, fmt.Errorf("error creating transport: %s", err)
	}

	client := &Client{Transport: tp}
	client.API = esapi.New(client)

	if cfg.DiscoverNodesOnStart {
		go client.DiscoverNodes()
	}

	return client, nil
}
Elasticsearch — factory for new clients. Only shown for demonstrating how a factory works.

DDD suggests using factories for creating complex aggregates, repositories, and services. We will implement a factory function that will create a new Customer instance. This will result in a function named NewCustomer which accepts a name parameter, what happens on the inside of the function shouldn’t be of concern for the domains that want to initialize a new customer.

The NewCustomer will validate that the input contains all the needed data for creating a Customer

In a real application I would probably suggest having the aggregate Customer inside the domains/customer along with the factory, we cover this in the second article

// Package aggregate holds aggregates that combines many entities into a full object
package aggregate

import (
	"errors"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/entity"
	"github.com/percybolmer/ddd-go/valueobject"
)

var (
	// ErrInvalidPerson is returned when the person is not valid in the NewCustome factory
	ErrInvalidPerson = errors.New("a customer has to have an valid person")
)

// Customer is a aggregate that combines all entities needed to represent a customer
type Customer struct {
	// person is the root entity of a customer
	// which means the person.ID is the main identifier for this aggregate
	person *entity.Person
	// a customer can hold many products
	products []*entity.Item
	// a customer can perform many transactions
	transactions []valueobject.Transaction
}

// NewCustomer is a factory to create a new Customer aggregate
// It will validate that the name is not empty
func NewCustomer(name string) (Customer, error) {
	// Validate that the Name is not empty
	if name == "" {
		return Customer{}, ErrInvalidPerson
	}

	// Create a new person and generate ID
	person := &entity.Person{
		Name: name,
		ID:   uuid.New(),
	}
	// Create a customer object and initialize all the values to avoid nil pointer exceptions
	return Customer{
		person:       person,
		products:     make([]*entity.Item, 0),
		transactions: make([]valueobject.Transaction, 0),
	}, nil
}
customer.go — Creating a factory for customer which validates input name and returns a new customer pointer

The customer factory now helps with validating input, creating a new ID, and making sure all values are properly initialized.

Now we have some business logic in place, so it is also time to start adding tests. I’ll create a customer_test.go in the aggregate package where I test logic related to the Customer.

package aggregate_test

import (
	"testing"

	"github.com/percybolmer/ddd-go/aggregate"
)

func TestCustomer_NewCustomer(t *testing.T) {
	// Build our needed testcase data struct
	type testCase struct {
		test        string
		name        string
		expectedErr error
	}
	// Create new test cases
	testCases := []testCase{
		{
			test:        "Empty Name validation",
			name:        "",
			expectedErr: aggregate.ErrInvalidPerson,
		}, {
			test:        "Valid Name",
			name:        "Percy Bolmer",
			expectedErr: nil,
		},
	}

	for _, tc := range testCases {
		// Run Tests
		t.Run(tc.test, func(t *testing.T) {
			// Create a new customer
			_, err := aggregate.NewCustomer(tc.name)
			// Check if the error matches the expected error
			if err != tc.expectedErr {
				t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
			}

		})
	}

}
Unit test for Customer factory to make sure it works as expected

We won’t get very far with just creating new Customers though, it is time to start looking at the best design pattern that I know off.

Repositories — The Repository Pattern

Repository interfaces used to hide underlying implementation details. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Repository interfaces used to hide underlying implementation details. Gopher by Takuya Ueda, Original Go Gopher by Renée French

DDD describes that repositories should be used to store and manage aggregates. This is one of those patterns that once I learned it, I knew I’d never stop using it. It is a pattern that relies on hiding the implementation of the storage/database solution behind an interface. This allows us to define a set of methods that has to be present, and if they are present it is qualified to be used as a repository.

The advantage of this design pattern is that it allows us to exchange the solution without breaking anything. We can have in-memory storage used during the development phase, and then later switch that out to MongoDB storage for production. Not only does it help with changing the underlying technology used without breaking anything that leverages the repository, but it is also very useful in testing. You can easily implement a new repository simply for unit tests etc.

We will begin by creating a file called repository.go inside the domain/customer package. In that file, we will define the functions needed for a repository. We will want to Get, Add and Update customers. We will not be deleting any customers, once a customer in this tavern you are always a customer. We will also implement some generic errors in the customer package that the different repository implementations can use.

// Package Customer holds all the domain logic for the customer domain.
package customer

import (
	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
)
var (
	// ErrCustomerNotFound is returned when a customer is not found.
	ErrCustomerNotFound = errors.New("the customer was not found in the repository")
	// ErrFailedToAddCustomer is returned when the customer could not be added to the repository.
	ErrFailedToAddCustomer = errors.New("failed to add the customer to the repository")
	// ErrUpdateCustomer is returned when the customer could not be updated in the repository.
	ErrUpdateCustomer = errors.New("failed to update the customer in the repository")
)
// CustomerRepository is a interface that defines the rules around what a customer repository
// Has to be able to perform
type CustomerRepository interface {
	Get(uuid.UUID) (aggregate.Customer, error)
	Add(aggregate.Customer) error
	Update(aggregate.Customer) error
}
repository.go — Defining the rules around a repository for storing customers

Next up we need to implement an actual business logic that fulfills the interface, we will start with memory storage. At the end of the article, we will look at how we can change that to a MongoDB solution, without breaking anything else.

I like keeping each implementation inside its directory, just to make it easier for a fresh developer in the team to find the correct code location. Let’s create a folder called memory to indicate that the repository is using memory as storage.

Another solution can be having the memory.go in the customer package, but I find it getting cluttered fast in bigger systems

mkdir memory
touch memory/memory.go

Let’s first set up the correct structure in the memory file, we want to create a struct that has methods to fulfill the CustomerRepository, and let’s not forget the factory to create a new repository.


// Package memory is a in-memory implementation of the customer repository
package memory

import (
	"sync"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
)

// MemoryRepository fulfills the CustomerRepository interface
type MemoryRepository struct {
	customers map[uuid.UUID]aggregate.Customer
	sync.Mutex
}

// New is a factory function to generate a new repository of customers
func New() *MemoryRepository {
	return &MemoryRepository{
		customers: make(map[uuid.UUID]aggregate.Customer),
	}
}

// Get finds a customer by ID
func (mr *MemoryRepository) Get(uuid.UUID) (aggregate.Customer, error) {
	return aggregate.Customer{}, nil
}

// Add will add a new customer to the repository
func (mr *MemoryRepository) Add(aggregate.Customer) error {
	return nil
}

// Update will replace an existing customer information with the new customer information
func (mr *MemoryRepository) Update(aggregate.Customer) error {
	return nil
}
The structure needed for a new CustomerRepository to be added

We need to add a way of retrieving information from the Customer aggregate, such as an ID from the root entity. So we should update the aggregate with a little function for grabbing the ID, and a function to change the name.

// GetID returns the customers root entity ID
func (c *Customer) GetID() uuid.UUID {
	return c.person.ID
}
// SetID sets the root ID
func (c *Customer) SetID(id uuid.UUID) {
	if c.person == nil {
		c.person = &entity.Person{}
	}
	c.person.ID = id
}

// SetName changes the name of the Customer
func (c *Customer) SetName(name string) {
	if c.person == nil {
		c.person = &entity.Person{}
	}
	c.person.Name = name
}
// SetName changes the name of the Customer
func (c *Customer) GetName() string {
	return c.person.Name
}
aggregate/customer.go — Adding a function to grab the ID of the root entity

Let’s add some very basic functionality to our in-memory repository so that it works as expected.

// Package memory is a in-memory implementation of the customer repository
package memory

import (
	"fmt"
	"sync"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
	"github.com/percybolmer/ddd-go/domain/customer"
)

// MemoryRepository fulfills the CustomerRepository interface
type MemoryRepository struct {
	customers map[uuid.UUID]aggregate.Customer
	sync.Mutex
}

// New is a factory function to generate a new repository of customers
func New() *MemoryRepository {
	return &MemoryRepository{
		customers: make(map[uuid.UUID]aggregate.Customer),
	}
}

// Get finds a customer by ID
func (mr *MemoryRepository) Get(id uuid.UUID) (aggregate.Customer, error) {
	if customer, ok := mr.customers[id]; ok {
		return customer, nil
	}

	return aggregate.Customer{}, customer.ErrCustomerNotFound
}

// Add will add a new customer to the repository
func (mr *MemoryRepository) Add(c aggregate.Customer) error {
	if mr.customers == nil {
		// Saftey check if customers is not create, shouldn't happen if using the Factory, but you never know
		mr.Lock()
		mr.customers = make(map[uuid.UUID]aggregate.Customer)
		mr.Unlock()
	}
	// Make sure Customer isn't already in the repository
	if _, ok := mr.customers[c.GetID()]; ok {
		return fmt.Errorf("customer already exists: %w", customer.ErrFailedToAddCustomer)
	}
	mr.Lock()
	mr.customers[c.GetID()] = c
	mr.Unlock()
	return nil
}

// Update will replace an existing customer information with the new customer information
func (mr *MemoryRepository) Update(c aggregate.Customer) error {
	// Make sure Customer is in the repository
	if _, ok := mr.customers[c.GetID()]; !ok {
		return fmt.Errorf("customer does not exist: %w", customer.ErrUpdateCustomer)
	}
	mr.Lock()
	mr.customers[c.GetID()] = c
	mr.Unlock()
	return nil
}
CustomerRepository — Implementing logic for the customer repository

And as always, we should add tests for the code. I want to point out how great the repository pattern is from a test perspective. It is so easy in unit tests to replace parts of the logic with a repository created just for the tests, making it easier the replicate knew bugs in the tests.

package memory

import (
	"testing"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
	"github.com/percybolmer/ddd-go/domain/customer"
)

func TestMemory_GetCustomer(t *testing.T) {
	type testCase struct {
		name        string
		id          uuid.UUID
		expectedErr error
	}

	// Create a fake customer to add to repository
	cust, err := aggregate.NewCustomer("Percy")
	if err != nil {
		t.Fatal(err)
	}
	id := cust.GetID()
	// Create the repo to use, and add some test Data to it for testing
	// Skip Factory for this
	repo := MemoryRepository{
		customers: map[uuid.UUID]aggregate.Customer{
			id: cust,
		},
	}

	testCases := []testCase{
		{
			name:        "No Customer By ID",
			id:          uuid.MustParse("f47ac10b-58cc-0372-8567-0e02b2c3d479"),
			expectedErr: customer.ErrCustomerNotFound,
		}, {
			name:        "Customer By ID",
			id:          id,
			expectedErr: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {

			_, err := repo.Get(tc.id)
			if err != tc.expectedErr {
				t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
			}
		})
	}
}

func TestMemory_AddCustomer(t *testing.T) {
	type testCase struct {
		name        string
		cust        string
		expectedErr error
	}

	testCases := []testCase{
		{
			name:        "Add Customer",
			cust:        "Percy",
			expectedErr: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			repo := MemoryRepository{
				customers: map[uuid.UUID]aggregate.Customer{},
			}

			cust, err := aggregate.NewCustomer(tc.cust)
			if err != nil {
				t.Fatal(err)
			}

			err = repo.Add(cust)
			if err != tc.expectedErr {
				t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
			}

			found, err := repo.Get(cust.GetID())
			if err != nil {
				t.Fatal(err)
			}
			if found.GetID() != cust.GetID() {
				t.Errorf("Expected %v, got %v", cust.GetID(), found.GetID())
			}
		})
	}
}
Unit Tests for most of the Customer Repository

Great, we have the first repository in place. Remember to keep your repository related to their domain. In this case, the repository only handles the Customer aggregate and it should only do so. Never make the repository coupled to any other aggregate, we want louse coupling.

So how do we handle the logical flow of the tavern then, we can’t simply rely on the Customer Repository? We will at one point start coupling the different repositories and build a flow that represents the tavern logic.

Enter Services, the final part we need to learn.

Services — Connecting the Business Logic

Services — Connecting repositories into the actual business logic. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Services — Connecting repositories into the actual business logic. Gopher by Takuya Ueda, Original Go Gopher by Renée French

We have all these entities, an aggregate, and a repository for it, but it doesn’t look like an application yet does it? That’s why we need the next component Service.

A service will tie all loosely coupled repositories into a business logic that fulfills the needs of a certain domain. In the tavern case, we might have an Order service, responsible for chaining together repositories to perform an order. So the service will hold access to a CustomerRepository and a ProductRepository

A service typically holds all the repositories needed to perform a certain business logic flow, such as an Order, Api, or Billing. What’s great is that you can even have a service inside a service.

We will implement the Order service, which can then later be part of the Tavern service for instance.

Let’s create a new folder named services that will hold the services that we implement. We will begin by creating a file named order.go which will hold the OrderService that we will use to handle new orders in the Tavern. We are still missing some domains, so we will start only with the CustomerRepository, but add more soon.

I want to begin with the Factory for creating a new Service and show a very super neat trick that I’ve learned from Jon Calhoun in his book for web development. We will be creating an alias for a function that takes in a Service as a pointer and modifies it, and then allow a variable amount of these aliases. This way changing the behavior of the service, or replacing repositories is really easy.


// Package services holds all the services that connects repositories into a business flow
package services

import (
	"github.com/percybolmer/ddd-go/domain/customer"
)

// OrderConfiguration is an alias for a function that will take in a pointer to an OrderService and modify it
type OrderConfiguration func(os *OrderService) error

// OrderService is a implementation of the OrderService
type OrderService struct {
	customers customer.CustomerRepository
}

// NewOrderService takes a variable amount of OrderConfiguration functions and returns a new OrderService
// Each OrderConfiguration will be called in the order they are passed in
func NewOrderService(cfgs ...OrderConfiguration) (*OrderService, error) {
	// Create the orderservice
	os := &OrderService{}
	// Apply all Configurations passed in
	for _, cfg := range cfgs {
		// Pass the service into the configuration function
		err := cfg(os)
		if err != nil {
			return nil, err
		}
	}
	return os, nil
}
order.go — The factory function for a new Order Service accepts variable amount of configurations

See how we can take in a variable amount of OrderConfiguration in the factory method? It is a very neat way of allowing dynamic factories and allows the developer to configure the architecture, given that it is implemented. This trick is very good for unit tests, as you can replace certain parts in service with the wanted repository.

Small side note, for smaller services this approach may seem a bit overkill. I’d like to point out that in the examples we only use configurations to modify the repositories, but this can be used for internal settings and options as well. For smaller services one could also create a simple factory which accepts the CustomerRepository for instance.

Let’s create an OrderConfiguration that applies the CustomerRepository so we can begin creating the business logic of the Order.

// WithCustomerRepository applies a given customer repository to the OrderService
func WithCustomerRepository(cr customer.CustomerRepository) OrderConfiguration {
	// return a function that matches the OrderConfiguration alias,
	// You need to return this so that the parent function can take in all the needed parameters
	return func(os *OrderService) error {
		os.customers = cr
		return nil
	}
}

// WithMemoryCustomerRepository applies a memory customer repository to the OrderService
func WithMemoryCustomerRepository() OrderConfiguration {
	// Create the memory repo, if we needed parameters, such as connection strings they could be inputted here
	cr := memory.New()
	return WithCustomerRepository(cr)
}
WithCustomerRepository — function that returns a OrderConfiguration so that it can be used as input parameter to the NewOrderService

Now to use this you can simply chain all the configurations when we create the service allowing us to switch out parts easily.

// In Memory Example used in Development
NewOrderService(WithMemoryCustomerRepository())
// We could in the future switch to MongoDB like this
NewOrderService(WithMongoCustomerRepository())

Let’s begin adding functions to the Order service so a customer can purchase something in the tavern.

// CreateOrder will chaintogether all repositories to create a order for a customer
func (o *OrderService) CreateOrder(customerID uuid.UUID, productIDs []uuid.UUID) error {
	// Get the customer
	c, err := o.customers.Get(customerID)
	if err != nil {
		return err
	}

	// Get each Product, Ouchie, We need a ProductRepository

	return nil
}
order.go — Creating a order for a customer.

Yikes, our Tavern doesn’t have any products to serve. Surely, you know how to fix that right? Let’s implement some more repositories and apply them to the service by using the OrderConfiguration

ProductRepository — The final piece of the Tavern

Product Repository — Handel the product aggregate. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Product Repository — Handel the product aggregate. Gopher by Takuya Ueda, Original Go Gopher by Renée French

Now that we know what all the components we need in DDD do, it is time to practice a bit with them. We will begin by fixing a ProductRepository so we can find the products the customers are ordering.

From this point on I will be moving a bit faster and explain less, since we covered the basics I won’t explain them twice.

Let’s create the product.go and product_test.go inside the aggregate folder. We begin by creating the Product aggregate and a factory function for it.

// Package aggregate
// File: product.go
// Product is an aggregate that represents a product.
package aggregate

import (
	"errors"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/entity"
)

var (
	// ErrMissingValues is returned when a product is created without a name or description
	ErrMissingValues = errors.New("missing values")
)

// Product is a aggregate that combines item with a price and quantity
type Product struct {
	// item is the root entity which is an item
	item  *entity.Item
	price float64
	// Quantity is the number of products in stock
	quantity int
}

// NewProduct will create a new product
// will return error if name of description is empty
func NewProduct(name, description string, price float64) (Product, error) {
	if name == "" || description == "" {
		return Product{}, ErrMissingValues
	}

	return Product{
		item: &entity.Item{
			ID:          uuid.New(),
			Name:        name,
			Description: description,
		},
		price:    price,
		quantity: 0,
	}, nil
}

func (p Product) GetID() uuid.UUID {
	return p.item.ID
}

func (p Product) GetItem() *entity.Item {
	return p.item
}

func (p Product) GetPrice() float64 {
	return p.price
}
product.go — An aggregate that holds Item and a price and quantity.

Next, you should add unit tests for the aggregate to make sure any logic inside works as expected.

package aggregate_test

import (
	"testing"

	"github.com/percybolmer/ddd-go/aggregate"
)

func TestProduct_NewProduct(t *testing.T) {
	type testCase struct {
		test        string
		name        string
		description string
		price       float64
		expectedErr error
	}

	testCases := []testCase{
		{
			test:        "should return error if name is empty",
			name:        "",
			expectedErr: aggregate.ErrMissingValues,
		},
		{
			test:        "validvalues",
			name:        "test",
			description: "test",
			price:       1.0,
			expectedErr: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.test, func(t *testing.T) {
			_, err := aggregate.NewProduct(tc.name, tc.description, tc.price)
			if err != tc.expectedErr {
				t.Errorf("Expected error: %v, got: %v", tc.expectedErr, err)
			}
		})
	}
}
Unit test all the aggregate logic

Create a file in the product domain /domain/product/repository.go. Here we will define the ProductRepository that will allow us to access products.

// Package product holds the repository and the implementations for a ProductRepository
package product

import (
	"errors"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
)

var (
	//ErrProductNotFound is returned when a product is not found
	ErrProductNotFound = errors.New("the product was not found")
	//ErrProductAlreadyExist is returned when trying to add a product that already exists
	ErrProductAlreadyExist = errors.New("the product already exists")
)

// ProductRepository is the repository interface to fulfill to use the product aggregate
type ProductRepository interface {
	GetAll() ([]aggregate.Product, error)
	GetByID(id uuid.UUID) (aggregate.Product, error)
	Add(product aggregate.Product) error
	Update(product aggregate.Product) error
	Delete(id uuid.UUID) error
}
product.go — The ProductRepository used to access products

Great, just as with the CustomerRepository we will implement an in-memory solution for the ProductRepository. Create a folder called memory in the product domain and insert the following code.

// Package memory is a in memory implementation of the ProductRepository interface.
package memory

import (
	"sync"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
	"github.com/percybolmer/ddd-go/domain/product"
)

type MemoryProductRepository struct {
	products map[uuid.UUID]aggregate.Product
	sync.Mutex
}

// New is a factory function to generate a new repository of customers
func New() *MemoryProductRepository {
	return &MemoryProductRepository{
		products: make(map[uuid.UUID]aggregate.Product),
	}
}

// GetAll returns all products as a slice
// Yes, it never returns an error, but
// A database implementation could return an error for instance
func (mpr *MemoryProductRepository) GetAll() ([]aggregate.Product, error) {
	// Collect all Products from map
	var products []aggregate.Product
	for _, product := range mpr.products {
		products = append(products, product)
	}
	return products, nil
}

// GetByID searches for a product based on it's ID
func (mpr *MemoryProductRepository) GetByID(id uuid.UUID) (aggregate.Product, error) {
	if product, ok := mpr.products[uuid.UUID(id)]; ok {
		return product, nil
	}
	return aggregate.Product{}, product.ErrProductNotFound
}

// Add will add a new product to the repository
func (mpr *MemoryProductRepository) Add(newprod aggregate.Product) error {
	mpr.Lock()
	defer mpr.Unlock()

	if _, ok := mpr.products[newprod.GetID()]; ok {
		return product.ErrProductAlreadyExist
	}

	mpr.products[newprod.GetID()] = newprod

	return nil
}

// Update will change all values for a product based on it's ID
func (mpr *MemoryProductRepository) Update(upprod aggregate.Product) error {
	mpr.Lock()
	defer mpr.Unlock()

	if _, ok := mpr.products[upprod.GetID()]; !ok {
		return product.ErrProductNotFound
	}

	mpr.products[upprod.GetID()] = upprod
	return nil
}

// Delete remove an product from the repository
func (mpr *MemoryProductRepository) Delete(id uuid.UUID) error {
	mpr.Lock()
	defer mpr.Unlock()

	if _, ok := mpr.products[id]; !ok {
		return product.ErrProductNotFound
	}
	delete(mpr.products, id)
	return nil
}
memory.go — The ProductRepository that uses in memory logic to store products

And, of course, we need a few tests.

package memory

import (
	"testing"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
	"github.com/percybolmer/ddd-go/domain/product"
)

func TestMemoryProductRepository_Add(t *testing.T) {
	repo := New()
	product, err := aggregate.NewProduct("Beer", "Good for you're health", 1.99)
	if err != nil {
		t.Error(err)
	}

	repo.Add(product)
	if len(repo.products) != 1 {
		t.Errorf("Expected 1 product, got %d", len(repo.products))
	}
}
func TestMemoryProductRepository_Get(t *testing.T) {
	repo := New()
	existingProd, err := aggregate.NewProduct("Beer", "Good for you're health", 1.99)
	if err != nil {
		t.Error(err)
	}

	repo.Add(existingProd)
	if len(repo.products) != 1 {
		t.Errorf("Expected 1 product, got %d", len(repo.products))
	}

	type testCase struct {
		name        string
		id          uuid.UUID
		expectedErr error
	}

	testCases := []testCase{
		{
			name:        "Get product by id",
			id:          existingProd.GetID(),
			expectedErr: nil,
		}, {
			name:        "Get non-existing product by id",
			id:          uuid.New(),
			expectedErr: product.ErrProductNotFound,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := repo.GetByID(tc.id)
			if err != tc.expectedErr {
				t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
			}

		})
	}

}
func TestMemoryProductRepository_Delete(t *testing.T) {
	repo := New()
	existingProd, err := aggregate.NewProduct("Beer", "Good for you're health", 1.99)
	if err != nil {
		t.Error(err)
	}

	repo.Add(existingProd)
	if len(repo.products) != 1 {
		t.Errorf("Expected 1 product, got %d", len(repo.products))
	}

	err = repo.Delete(existingProd.GetID())
	if err != nil {
		t.Error(err)
	}
	if len(repo.products) != 0 {
		t.Errorf("Expected 0 products, got %d", len(repo.products))
	}
}
memory_test.go — Unit tests for the ProductRepository

To begin using the ProductRepository we need to change the OrderService so that It can hold the repository. Open services/order.go and add a new field for it.

// OrderService is a implementation of the OrderService
type OrderService struct {
	customers customer.CustomerRepository
	products  product.ProductRepository
}
OrderService — Now with two repositories

Remember a Service can hold multiple repositories, and also other services.

Next, we need to add a new OrderConfiguration function that will apply the in-memory repository. Notice how I now insert a parameter to the function, a slice of products. Since we return an OrderConfiguration we can still use this function in the factory.

// WithMemoryProductRepository adds a in memory product repo and adds all input products
func WithMemoryProductRepository(products []aggregate.Product) OrderConfiguration {
	return func(os *OrderService) error {
		// Create the memory repo, if we needed parameters, such as connection strings they could be inputted here
		pr := prodmemory.New()

		// Add Items to repo
		for _, p := range products {
			err := pr.Add(p)
			if err != nil {
				return err
			}
		}
		os.products = pr
		return nil
	}
}
OrderConfiguration that is used to apply a in-memory repository to the OrderService

Let’s update the CreateOrder function in OrderService to look up the products that are ordered, we will also return the price of all products ordered.

// CreateOrder will chaintogether all repositories to create a order for a customer
// will return the collected price of all Products
func (o *OrderService) CreateOrder(customerID uuid.UUID, productIDs []uuid.UUID) (float64, error) {
	// Get the customer
	c, err := o.customers.Get(customerID)
	if err != nil {
		return 0, err
	}

	// Get each Product, Ouchie, We need a ProductRepository
	var products []aggregate.Product
	var price float64
	for _, id := range productIDs {
		p, err := o.products.GetByID(id)
		if err != nil {
			return 0, err
		}
		products = append(products, p)
		price += p.GetPrice()
	}

	// All Products exists in store, now we can create the order
	log.Printf("Customer: %s has ordered %d products", c.GetID(), len(products))

	return price, nil
}
CreateOrder — Now uses the ProductRepository to fetch product information

I will update the test in order_test.go to create the OrderService with all the repositories needed and products.


package services

import (
	"testing"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
)

func init_products(t *testing.T) []aggregate.Product {
	beer, err := aggregate.NewProduct("Beer", "Healthy Beverage", 1.99)
	if err != nil {
		t.Error(err)
	}
	peenuts, err := aggregate.NewProduct("Peenuts", "Healthy Snacks", 0.99)
	if err != nil {
		t.Error(err)
	}
	wine, err := aggregate.NewProduct("Wine", "Healthy Snacks", 0.99)
	if err != nil {
		t.Error(err)
	}
	products := []aggregate.Product{
		beer, peenuts, wine,
	}
	return products
}
func TestOrder_NewOrderService(t *testing.T) {
	// Create a few products to insert into in memory repo
	products := init_products(t)

	os, err := NewOrderService(
		WithMemoryCustomerRepository(),
		WithMemoryProductRepository(products),
	)

	if err != nil {
		t.Error(err)
	}

	// Add Customer
	cust, err := aggregate.NewCustomer("Percy")
	if err != nil {
		t.Error(err)
	}

	err = os.customers.Add(cust)
	if err != nil {
		t.Error(err)
	}

	// Perform Order for one beer
	order := []uuid.UUID{
		products[0].GetID(),
	}

	_, err = os.CreateOrder(cust.GetID(), order)

	if err != nil {
		t.Error(err)
	}

}
Order_test.go — Create a OrderService with both repositories and perform an Order

Tavern — Service that holds sub-services & and MongoDB

Tavern — The final solution. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Tavern — The final solution. Gopher by Takuya Ueda, Original Go Gopher by Renée French

Now it’s time for the final piece, the Tavern service. This service will hold the OrderService as a sub-service, allowing the Tavern to create Orders.

The reason why you want to stack services like this can be applying extra logic. For instance, the Tavern service will most likely want to be able to add Billing. Notice how easily we can implement the Ordering logic to the Tavern without worrying about the implementation details, and then expanding upon it.

I will create a tavern.go inside the services folder. In this file we create the Tavern struct, it holds the OrderService and has a NewTavern factory to apply an OrderService

package services

import (
	"log"

	"github.com/google/uuid"
)

// TavernConfiguration is an alias that takes a pointer and modifies the Tavern
type TavernConfiguration func(os *Tavern) error

type Tavern struct {
	// orderservice is used to handle orders
	OrderService *OrderService
	// BillingService is used to handle billing
	// This is up to you to implement
	BillingService interface{}
}

// NewTavern takes a variable amount of TavernConfigurations and builds a Tavern
func NewTavern(cfgs ...TavernConfiguration) (*Tavern, error) {
	// Create the Tavern
	t := &Tavern{}
	// Apply all Configurations passed in
	for _, cfg := range cfgs {
		// Pass the service into the configuration function
		err := cfg(t)
		if err != nil {
			return nil, err
		}
	}
	return t, nil
}

// WithOrderService applies a given OrderService to the Tavern
func WithOrderService(os *OrderService) TavernConfiguration {
	// return a function that matches the TavernConfiguration signature
	return func(t *Tavern) error {
		t.OrderService = os
		return nil
	}
}

// Order performs an order for a customer
func (t *Tavern) Order(customer uuid.UUID, products []uuid.UUID) error {
	price, err := t.OrderService.CreateOrder(customer, products)
	if err != nil {
		return err
	}
	log.Printf("Bill the Customer: %0.0f", price)

	// Bill the customer
	//err = t.BillingService.Bill(customer, price)
	return nil
}
Tavern.go — Tavern can now Order products by using the OrderService

To try it out, we can create a unit test.

package services

import (
	"testing"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
)

func Test_Tavern(t *testing.T) {
	// Create OrderService
	products := init_products(t)

	os, err := NewOrderService(
		WithMemoryCustomerRepository(),
		WithMemoryProductRepository(products),
	)
	if err != nil {
		t.Error(err)
	}

	tavern, err := NewTavern(WithOrderService(os))
	if err != nil {
		t.Error(err)
	}

	cust, err := aggregate.NewCustomer("Percy")
	if err != nil {
		t.Error(err)
	}

	err = os.customers.Add(cust)
	if err != nil {
		t.Error(err)
	}
	order := []uuid.UUID{
		products[0].GetID(),
	}
	// Execute Order
	err = tavern.Order(cust.GetID(), order)
	if err != nil {
		t.Error(err)
	}

}
Tavern_test.go — Testing the Tavern by ordering a beer

Now that we have a Tavern, I want to take the time to show you how to implement a MongoDB solution to the CustomerRepository. This is where the respository design pattern really starts shining. I love being able to switch out repositories at ease.

Begin by adding a new package named mongo in the customer domain. We will create a struct that fulfills the CustomerRepository so we can use it.

One important thing to notice here is the internal mongoCustomer struct which is used to store the customer. We don’t store the aggregate.Customer since that would tie the aggregate storage to the repository. Instead each repository is responsible for formatting and structuring the data as needed, no connections to other packages. This is the reason why we don’t use json or bson tags directly on the aggregate, as that would make couple the implementations. To make the switch we also add a factory function that converts between the two.

// Mongo is a mongo implementation of the Customer Repository
package mongo

import (
	"context"
	"time"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type MongoRepository struct {
	db *mongo.Database
	// customer is used to store customers
	customer *mongo.Collection
}

// mongoCustomer is an internal type that is used to store a CustomerAggregate
// we make an internal struct for this to avoid coupling this mongo implementation to the customeraggregate.
// Mongo uses bson so we add tags for that
type mongoCustomer struct {
	ID   uuid.UUID `bson:"id"`
	Name string    `bson:"name"`
}

// NewFromCustomer takes in a aggregate and converts into internal structure
func NewFromCustomer(c aggregate.Customer) mongoCustomer {
	return mongoCustomer{
		ID:   c.GetID(),
		Name: c.GetName(),
	}
}

// ToAggregate converts into a aggregate.Customer
// this could validate all values present etc
func (m mongoCustomer) ToAggregate() aggregate.Customer {
	c := aggregate.Customer{}

	c.SetID(m.ID)
	c.SetName(m.Name)

	return c

}

// Create a new mongodb repository
func New(ctx context.Context, connectionString string) (*MongoRepository, error) {
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(connectionString))
	if err != nil {
		return nil, err
	}

	// Find Metabot DB
	db := client.Database("ddd")
	customers := db.Collection("customers")

	return &MongoRepository{
		db:       db,
		customer: customers,
	}, nil
}

func (mr *MongoRepository) Get(id uuid.UUID) (aggregate.Customer, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result := mr.customer.FindOne(ctx, bson.M{"id": id})

	var c mongoCustomer
	err := result.Decode(&c)
	if err != nil {
		return aggregate.Customer{}, err
	}
	// Convert to aggregate
	return c.ToAggregate(), nil
}

func (mr *MongoRepository) Add(c aggregate.Customer) error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	internal := NewFromCustomer(c)
	_, err := mr.customer.InsertOne(ctx, internal)
	if err != nil {
		return err
	}
	return nil
}

func (mr *MongoRepository) Update(c aggregate.Customer) error {
	panic("to implement")
}
Mongo.go — A implementation of CustomerRepository with MongoDB

The next thing to do is add an OrderConfiguration so we can apply the repository to the OrderService.

func WithMongoCustomerRepository(connectionString string) OrderConfiguration {
	return func(os *OrderService) error {
		// Create the mongo repo, if we needed parameters, such as connection strings they could be inputted here
		cr, err := mongo.New(context.Background(), connectionString)
		if err != nil {
			return err
		}
		os.customers = cr
		return nil
	}
}
order.go — A orderConfiguration that applies mongo repository instead of in-memory

Then change the input in tavern_test.go to accept the MongoDB configuration instead. Notice how easy we can switch between in-memory and MongoDB, amazing.

func Test_MongoTavern(t *testing.T) {
	// Create OrderService
	products := init_products(t)

	os, err := NewOrderService(
		WithMongoCustomerRepository("mongodb://localhost:27017"),
		WithMemoryProductRepository(products),
	)
	if err != nil {
		t.Error(err)
	}

	tavern, err := NewTavern(WithOrderService(os))
	if err != nil {
		t.Error(err)
	}

	cust, err := aggregate.NewCustomer("Percy")
	if err != nil {
		t.Error(err)
	}

	err = os.customers.Add(cust)
	if err != nil {
		t.Error(err)
	}
	order := []uuid.UUID{
		products[0].GetID(),
	}
	// Execute Order
	err = tavern.Order(cust.GetID(), order)
	if err != nil {
		t.Error(err)
	}

}
Tavern_test.go — Replacing in-memory with a MongoDB repository

Voila, simple as that! We now have a tavern that works with both an in-memory repository and a MongoDB repository.

Conclusion

Concluding the first article. Gopher by Takuya Ueda, Original Go Gopher by Renée French
Concluding the first article. Gopher by Takuya Ueda, Original Go Gopher by Renée French

In this article we have covered the basics of Domain-Driven Design, in short.

  • Entities — Mutable Identifiable Structs.
  • Value Objects — Immutable Unidentifiable Structs.
  • Aggregates — Combined set of Entities and Value objects, stored in Repositories.
  • Repository — A implementation of storing aggregates or other information
  • Factory — A constructor to create complex objects and make creating new instance easier for the developers of other domains
  • Service — A collection of repositories and sub-services that builds together the business flow

Remember, in this example, we have named every component and package after the appropriate DDD term to make it easier to understand and relate. This is a learning example, in a real repository I would probably not name packages this way, and for that reason, there is a second article in which we refactor this project into a more clean architectural solution.

Fair warning, the second article is way shorter than this lengthy piece

A follow-up on how to organize the structure of code and packages in a DDD approach in Go
How to Structure DDD in Golang

October 1, 2021

A follow-up on how to organize the structure of code and packages in a DDD approach in Go

Read more

So, what is your opinion, does this architectural approach seem like something you could use?

You can find the full code on GitHub.

Thanks for reading and feel free to reach out to me in any way possible, you can find my Social Media below.

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter