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
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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
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
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.
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.
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.
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.
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.
Let’s add some very basic functionality to our in-memory repository so that it works as expected.
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.
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.
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.
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.
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.
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
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.
Next, you should add unit tests for the aggregate to make sure any logic inside works as expected.
Create a file in the product domain /domain/product/repository.go. Here we will define the ProductRepository that will allow us 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.
And, of course, we need a few tests.
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.
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.
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.
I will update the test in order_test.go to create the OrderService with all the repositories needed and products.
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
To try it out, we can create a unit test.
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.
The next thing to do is add an OrderConfiguration so we can apply the repository to the OrderService.
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.
Voila, simple as that! We now have a tavern that works with both an in-memory repository and a MongoDB repository.
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
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