loading...
Cover image for Using Domain-Driven Design(DDD)in Golang

Using Domain-Driven Design(DDD)in Golang

stevensunflash profile image Steven Victor Updated on ・7 min read

Domain-Driven Design pattern is the talk of the town today.
Domain-Driven Design(DDD) is an approach to software development that simplifies the complexity developers face by connecting the implementation to an evolving model.

Note

This is not an article that explains the "ideal" way to implement DDD in Golang because the author is no way an expert on it. This article is rather the author's understanding of DDD based on his research. The author
will be very grateful to contributions on how to improve this article.

Check the github repo for the updated code:
https://github.com/victorsteven/food-app-server

Why DDD?
The following are the reasons to consider using DDD:

  • Provide principles & patterns to solve difficult problems
  • Base complex designs on a model of the domain
  • Initiate a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses domain problems.

The idea of Domain-Driven Design was inverted by Eric Evans. He wrote about it in a book which you can find some of the highlights here

DDD comprises of 4 Layers:

  1. Domain: This is where the domain and business logic of the application is defined.
  2. Infrastructure: This layer consists of everything that exists independently of our application: external libraries, database engines, and so on.
  3. Application: This layer serves as a passage between the domain and the interface layer. The sends the requests from the interface layer to the domain layer, which processes it and returns a response.
  4. Interface: This layer holds everything that interacts with other systems, such as web services, RMI interfaces or web applications, and batch processing frontends.

Alt Text
To have a thorough definition of terms of each layer, please refer to this

Let's get started.

We are going to build a food recommendation API.

You can get the code if you don't have all the time to read.
Get the API code here.
Get the Frontend code here.

The very first thing to do is to initialize the dependency management. We will be using go.mod. From the root directory(path: food-app/), initialize go.mod:

go mod init food-app

This is how the project is going to be organized:

Alt Text

In this application, we will use postgres and redis databases to persist data. We will define a .env file that has connection information.
The .env file looks like this:


This file should be located in the root directory(path: food-app/)

Domain Layer

We will consider the domain first.
The domain has several patterns. Some of which are:
Entity, Value, Repository, Service, and so on.

Alt Text

Since the application we are building here is a simple one, we consider just two domain patterns: entity and repository.

Entity

This is where we define the "Schema" of things.
For example, we can define a user's struct. See the entity as the blueprint to the domain.

From the above file, the user's struct is defined that contains the user information, we also added helper functions that will validate and sanitize inputs. A Hash method is called that helps hash password. That is defined in the infrastructure layer.
Gorm is used as the ORM of choice.

The same approach is taken when defining the food entity. You can look up the repo.

Repository

The repository defines a collection of methods that the infrastructure implements. This gives a vivid picture of the number of methods that interact with a given database or a third-party API.

The user's repository will look like this:

The methods are defined in an interface. These methods will later be implemented in the infrastructure layer.

Almost the same applies to the food repository here.

Infrastructure Layer

This layer implements the methods defined in the repository. The methods interact with the database or a third-party API. This article will only consider database interaction.

Alt Text

We can see how the user's repository implementation looks like:

Well, you can see that we implemented the methods that were defined in the repository. This was made possible using the UserRepo struct which implements the UserRepository interface, as seen in this line:

//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{}

You can check the repository on how the food repository was implemented here.

So, let's configure our database by creating the db.go file with the content:



From the above file, we defined the Repositories struct which holds all the repositories in the application. We have the user and the food repositories. The Repositories also have a db instance, which is passed to the "constructors" of user and food(that is, NewUserRepository and NewFoodRepository).

Application Layer

We have successfully defined the API business logic in our domain. The application connects the domain and the interfaces layers.

We will only consider the user's application. You can check out that of the food in the repo.

This is the user's application:

The above have methods to save and retrieve user data. The UserApp struct has the UserRepository interface, which made it possible to call the user repository methods.

Interfaces Layer

The interfaces is the layer that handles HTTP requests and responses. This is where we get incoming requests for authentication, user-related stuff, and food-related stuff.

Alt Text

User Handler

We define methods for saving a user, getting all users and getting a particular user. These are found in the user_handler.go file.

I want you to observe that when returning the user, we only return a public user(which is defined in the entity). The public user does not have sensitive user details such as email and password.

Authentication Handler

The login_handler takes care of login, logout and refresh token methods. Some methods defined in their respective files are called in this file. Do well to check them out in the repository following their file path.

Food Handler

In the food_handler.go file, we have methods for basic food crud: creating, reading, updating, and deleting food. The file has explanations of how the code works.

Please note, when testing creating or updating food methods via the API using postman, use form-data not JSON. This is because the request type is multipart/form-data.

Running the Application

So, let's test what we've got. We will wire up the routes, connect to the database and start the application.

These will be done in the main.go file defined in the directory root.

The router(r) is of type Engine from the gin package we are using.

Middleware

As seen from the above file, Some routes have restrictions. The AuthMiddleware restricts access to an unauthenticated user. The CORSMiddleware enables data transfer from different domains. This is useful because VueJS is used for the frontend of this application and it points to a different domain.
The MaxSizeAllowed middleware stops any file with size above the one the middleware specifies. Since the food implementation requires file upload, the middleware stops files greater than the specified to be read into memory. This helps to prevent hackers from uploading an unreasonable huge file and slowing down the application. The middleware package is defined in the interfaces layer.

We can now run the application using:

go run main.go

Bonus

  • Vue and VueX are used to consume the API. Get the repository here, You can also visit the url and play with it.
  • Test cases are written for most of the functionality. If you have time, you can add to it. To achieve unit testing for each of the methods in our handler, we created a mock package(in the utils directory) that mocks all the dependencies that are used in the handler methods.
  • Circle CI is used for Continuous Integration.
  • Heroku is used to deploy the API
  • Netlify is used to deploy the Frontend.

Conclusion

I hope you didn't have a hard time following this guideline on how to use DDD in when building a golang application. If you have questions or any observations, please don't hesitate to use the comment section. As said early, the author is not an expert on this. He simply wrote this article based on his use case.
Get the API code here.
Get the Frontend code here.
You can also visit the application url and play with it.

Check out other articles on medium here.

You can as well follow on twitter

Happy DDDing.

Posted on by:

Discussion

markdown guide
 

Hi Steven, thank you for the article, there is not much DDD articles so this is good thing.

However, I have some observations regarding implementation details.

1) You start by "DDD comprises of 4 Layers" Domain, Infrastructure, Application, Interfaces then ... You put Infrastructure inside Domain ! Infrastructure package should be on the same level as Domain, Application and Interfaces. That is because Infrastructure layer brings support to all 3 others layers, not only domain. You can define go service interface inside your application package then have its implementation inside infrastructure because the implementation brings some weird dependencies.

2) packages "utils" :S in DDD you should not have an "utils" package. Everything have a place.
Auth and Security are infrastructure stuff

Middleware and FileUpload are Interfaces stuff

3) You talk about Entities but not about Agregates, you separate Repositories from Entities, their should be colocated in the same package , the "agregate" which name should be the aggregate name. Modularity is a big principle of DDD. Interchangeable component losely coupled with big cohesion.

4) In food_app.go in the application layer, there is a Service Interface , good. But there is no Factory for it, the struct implementation is just public to everyone "type FoodApp struct{...}" , it should be private and be called something like "type foodService struct {}" Factories are important because they help construct the object (inject services if needed, instrument it, etc )

5) The factory of your repository should
a) be inside the infrastructure package
func NewUserRepository(db *gorm.DB) repository.UserRepository
b) it takes an infrastructure object gorm.DB that the caller should not handle

6) You could use the DomainRegistry pattern to handle the restitution of the different factories (service, repo, aggregates). DomainRegistry interface in domain root then the DomainRegistry implementation in the infrastructure

In fact, there are other concerns but I think you get the point. Tactical patterns of DDD are direction to follow this is not strict if we respect certain rules.

Don't get me wrong this is just a fair critics. Because every one is learning, me the first. But from what I see the article can confuse first time DDD players because you break some principles.

If you want I can fork you repository and propose something we can discuss on.

 

Great. Thanks, a lot Epo for the feedback.

For the first point, The Infrastructure Layer is actually on the same level as the Domain. This was not the case in the first push. It had long been corrected. Please check.

For the second point, I'm currently refactoring to have the Auth in the infrastructure(have been a bit busy at work), I will probably update before the week runs out.

For the third point, I will refactor to that. Thanks.

For the fourth point, that was an oversight. The change has been effected now.

For the fifth point, I didn't quite get you. But this no longer applies in the implementation above.
func NewUserRepository(db *gorm.DB) repository.UserRepository
The article is currently in a constant state of editing as constructive feedback such as yours are given. If you don't mind, can you elaborate on your fifth point?

Please go ahead and fork the repository, your changes are welcome.

Thanks again for the feedback.

 

Hello Victor. First of all I wanted to thank you because your project have been very helpful as an example for implementing a project of my own. I have a question for you about the code tho. Why in the api in the refresh token method you just erase the refresh token and not the previous generated access token in the redis db. It seems like it will always remain an orphan access token in the redis database if you do this I think. Is there any reason for you to do that?. Thanks again.

 

Hi Jose,

For we to ever use the Refresh Token, it means the Access Token must have expired(and Redis deletes it automatically).

 

Hi Steven. Thanks for the answer. That makes sense. I will check it out it might be something on my implementation. The other thing is I don't see in the code in the frontend where you are encripting the password with the Bcrypt. Is that missing? or am I just don't seeing it?.

Thank you.

Hi,
The password is encrypted on the backend.
Go to this path: infrastructure/security/password.go
The function that hashes the password is defined there.

 

Hi Steven, thank you for the article, I have a question!

Why do you pass a repository as param when init interface?
Such as services.Food is passed as params of interfaces.NewFood initialization.
foods := interfaces.NewFood(services.Food, services.User, fd, redisService.Auth, tk)

I think that pass a application is better choice! How do you think about it?

 

Hi bro, Sorry for the late reply. Just seeing this now. interfaces.NewFood() is a constructor, so we inject dependencies of other services. This is considered best practice, so you avoid the usage of globals in your code.

 

Why did you choose to return interfaces e.g. in your repository creation functions? Isn't that against recommended Go best practices?

 

Hi Martin. Thanks for the feedback.
In order to answer your question properly, can you include a code snippet to a comment where you observed this?

Thanks.