Back

How to Structure DDD in Golang

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

by Percy Bolmér, September 1, 2021

Time for a clean up in the code structure. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
Time for a clean up in the code structure. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

It is time to take a look at a better architectural solution when using DDD in Go projects. In this article, we will take a repository that has all the DDD components in place and show how a less complicated project setup can be managed, while still maintaining DDD.

The repository that we will change is from a previous article I wrote on how to implement DDD in Go. In that article, I explained all the components present in DDD as explained by Eric Evans. The repository can be found on GitHub.

The easy way of learning how to use DDD in a Go application
How To Implement Domain-Driven Design (DDD) in Golang

Invalid Date

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

Read more

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

Moving Aggregates to Their Domain Package

The first thing to do is to remove the aggregate package altogether. We learned what an Aggregate is and what rules to apply, we don’t necessarily have to name the package to aggregate. I tend to place the Aggregates in their respective domain package, so the Customer aggregate should go inside the customer domain. It makes a lot more sense in my opinion.

Removing the Customer aggregate from the Aggregate package
Removing the Customer aggregate from the Aggregate package

Of course, after moving the files to the customer domain, you also need to change any reference that is pointing to aggregate.Customer and replace it with customer.Customer or Customer if you’re in the customer package.

// 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) (Customer, error)
	Add(Customer) error
	Update(Customer) error
}
customer/repository.go — Removing all mentions of aggregate package

The same thing has to be performed for the Product aggregate, it should go inside the product domain. I won’t cover all the code changes, it should be easy enough to find and refactor all mentions of the aggregate package.

Product domain with the product aggregate as part of it
Product domain with the product aggregate as part of it

According to me, it looks a lot nicer having the aggregate in their domain package in this way, it also kind of makes more sense.

Deleting the aggregate folder and we have tidied up some of the code smell.

A improved structure for the project, not yet completed though
A improved structure for the project, not yet completed though

Moving ValueObjects and Entities

We still have a folder for both entity and valueobject, which I guess isn’t wrong. One thing that is nice about storing the shared structs in their separate package that way is to avoid Cyclical Imports.

We can achieve this in another way though, which is less bloated. Right now we have no root package at all. Move all files from entity and valueobject to the root and rename the package to tavern

package tavern

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 in the root, with the package name tavern

This leaves us with an even more improved structure.

Moved entities and other shared items into the root package
Moved entities and other shared items into the root package

You will also need to rename your module in go.mod into the appropriate name.

module github.com/percybolmer/tavern

Not only that, we need to change all the imports in all the files to reflect the change, and all references of entities package have to be changed to tavern like the following gist.

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

import (
	"errors"

	"github.com/google/uuid"
	"github.com/percybolmer/tavern"
)

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  *tavern.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: &tavern.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() *tavern.Item {
	return p.item
}

func (p Product) GetPrice() float64 {
	return p.price
}
Replacing all instances of entities with tavern, and all imports.

All the imports starting with github.com/percybolmer/go-ddd are changed to github.com/percybolmer/tavern.

The easiest way to apply this change is to change all the files, remove the go.mod and go.sum file, and reinitialize the go module with the new name go mod init github.com/percybolmer/tavern.

That was quite the refactoring exerciser, but it was worth it. Feel free to rerun any tests so you know that it is working as expected.

Splitting the Services package

Right now, the services package contains all services in the same package. I’d like to split these into two packages, Order and Tavern.

The reason is that as projects grow it is nice to have the services divided into smaller packages. It can in my opinion become bloated storing all of them in the same package. I also like keeping the domain as part of the infrastructure, in this case, we create a new folder named order in the services folder. The reason is that in the future we might see more order-related services appearing, for instance, the current Order service focuses on Customer that orders a beverage, but what about when the Tavern needs to resupply? Using this structure it is easy for the developers to know where to look for related code.

Another important thing is the naming of the Configuration functions, if we keep creating functions like WithMemoryCustomerRepository it is hard to maintain what Configuration goes where. It is a lot easier if we see order.WithMemoryCustomerRepository to know what is happening.

How the services package is structured into sub packages
How the services package is structured into sub packages

Making this change requires the Tavern package to reference order.OrderService instead of the only OrderService

Change all references to match the new structure. We will also add a new function to the OrderService to add new customers because right now we can only do that by using the customer repository inside the struct. This won’t work since the service will be exposed to other packages. Also, a Service should never assume that the user of the service knows how to operate like this, so it is only natural that this logic is maintained in the service’s own domain.

// AddCustomer will add a new customer and return the customerID
func (o *OrderService) AddCustomer(name string) (uuid.UUID, error) {
	c, err := customer.NewCustomer(name)
	if err != nil {
		return uuid.Nil, err
	}
	// Add to Repo
	err = o.customers.Add(c)
	if err != nil {
		return uuid.Nil, err
	}

	return c.GetID(), nil
}
AddCustomer in the OrderService to help creating new Customers.

After applying these changes, the test to order a product is even easier to follow.

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

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

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

	uid, err := os.AddCustomer("Percy")
	if err != nil {
		t.Error(err)
	}
	order := []uuid.UUID{
		products[0].GetID(),
	}
	// Execute Order
	err = tavern.Order(uid, order)
	if err != nil {
		t.Error(err)
	}

}
taver_test.go — Creating a new customer and ordering is even easier

Now we finally have the final solution. A very simple to navigate, clean structured project.

The final architecture to use
The final architecture to use

How to Run The Tavern

One last thing I like to recommend is the use of the cmd folder as found in many Go repositories. In that folder, all command-line tools should be found, and it can be more than one. For the sake of concluding this article, I will create a main program in the cmd folder.

mkdir cmd
touch main.go

Let’s order a beer to celebrate.

// Package main runs the tavern and performs an Order
package main

import (
	"github.com/google/uuid"
	"github.com/percybolmer/tavern/domain/product"
	"github.com/percybolmer/tavern/services/order"
	servicetavern "github.com/percybolmer/tavern/services/tavern"
)

func main() {

	products := productInventory()
	// Create Order Service to use in tavern
	os, err := order.NewOrderService(
		order.WithMongoCustomerRepository("mongodb://localhost:27017"),
		order.WithMemoryProductRepository(products),
	)
	if err != nil {
		panic(err)
	}
	// Create tavern service
	tavern, err := servicetavern.NewTavern(
		servicetavern.WithOrderService(os))
	if err != nil {
		panic(err)
	}

	uid, err := os.AddCustomer("Percy")
	if err != nil {
		panic(err)
	}
	order := []uuid.UUID{
		products[0].GetID(),
	}
	// Execute Order
	err = tavern.Order(uid, order)
	if err != nil {
		panic(err)
	}
}

func productInventory() []product.Product {
	beer, err := product.NewProduct("Beer", "Healthy Beverage", 1.99)
	if err != nil {
		panic(err)
	}
	peenuts, err := product.NewProduct("Peenuts", "Healthy Snacks", 0.99)
	if err != nil {
		panic(err)
	}
	wine, err := product.NewProduct("Wine", "Healthy Snacks", 0.99)
	if err != nil {
		panic(err)
	}
	products := []product.Product{
		beer, peenuts, wine,
	}
	return products
}
main.go — Try running the program to see that everything is in order, get the pun?

Conclusion

This concludes this very long two-part series about DDD.

You can find the full code on GitHub.

There is still a lot more about DDD to learn, this only touches the basics and how I find structuring projects. We haven’t touched topics as Domain events, CQRS and EventSourcing yet. Stay tuned for more.

If you’re eager to learn more about DDD I do suggest Eric Evans book Domain-Driven Design: Tackling Complexity in the Heart of Software.

As always, feel free to reach out to me on any of my social media which you find below.

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

Sign up for my Awesome newsletter