How I implement Clean Code Architecture on Golang projects
It’s a long time since the last article that I published because I have many tasks from my current company. so I can’t write a new article.
After a long time since I build my own Golang starter kit (actually, it has to implement clean code architecture, but at the time I didn’t know how to write a unit test so when I open my first project using that architecture and realized some of the parts of my project is not testable even I write a unit test, some parts make the code not testable). The starter uses Fiber as HTTP framework, and GORM as the ORM. and after my current company doesn’t implement any ORM because the queries are too complex and it makes a painful when implementing the ORM so after I got Golang project I decided to use my starter kit and not to use ORM again even for the new projects. then when I start to write a code and it always happens every I want to make a new project, I feel ‘where should I put the transaction’. I always think what is the best fit to place the transaction? in the repositories, or the service? and I know the answer is I should put the transaction in service because The transaction is a part of business logic. so all of the transactions should be put at the services. but then I think, “How do to do it with my starter?” and after research, I got the answer. and I rewrite my starter
In the clean code architecture concept, All of the components should be independent and not depend on third-party libraries or frameworks. the projects should not depend on specific databases or frameworks, so switching databases or frameworks are more easily, and it is also testable
as we know the concept of clean code architecture:
- Independent of Frameworks. The architecture does not depend on the existence of some library of feature-laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
- Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
- Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
- Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
- Independent of any external agency. In fact, your business rules simply don’t know anything at all about the outside world.
so the components must be independent and testable. in uncle bob’s clean code architecture has 4 layer
- use cases
- frameworks or drivers
basically built my own architecture based on modules and every module has
so the example I have a user module, and In the user module has its own entities, repositories, services, and handlers. and If I make a new module like products it also has entities, repositories, services, and handlers.
actually, the name is similar to Uncle Bob’s clean code architecture. In my starter kit, I use entities as a struct to decode data from the database, as the result from data query from repositories then parse the result to the entities. the entities will store the object’s struct and its method
the repository has a responsibility to execute a query (select), and SQL commands (insert, update, delete). actually, the repository will handle CRUD operation or act as data persistent. in the repository, there is no business logic here, and don’t write business logic in the repository.
if you have multiple databases, you must define all of the databases in your repository, e.g: you have MySQL and MongoDB, where your MySQL uses as to store user data, and MongoDB uses for store user log, you should make your user_mysql_repository.go, and user_mongodb_repository.go
if you use ORM, take your ORM query in the repository.
the database module need to be injected into the repository because the repository depends on the database
the service has a responsibility to manage business logic, so all of your business logic must be placed here. and don’t put your database query in here, but you should put your database transaction in services because the transaction is business logic.
the repository needs to be injected into the services because the services depend on the repository. you also be able to inject other services into another service, e.g: you have product service, and it depends on your user service, so you must inject your user service into your product service
handler basically is a controller in the MVC pattern. I called it the handlers because it’s handling a request and response based on the protocols. while I use RabbitMQ with amqp, I just make an amqp_handler.go or rabbitmq_router, but if I want to make a REST API, I will create a http_handler.go.
in my starter kit, the handler’s responsibility is only to get the request body, request params, and query string. and make a data as a response for the clients. if I make an HTTP rest so I usually will make a JSON response, while I use RabbitMQ I return the data to the consumers. so don’t write any business logic or database query in handlers.
the handlers depend on the services, so you must inject your services into your handlers. but if you read on my description about the services, “you also be able to inject other services into another service”. it doesn’t happen on the handlers, you can’t inject the other handler into another handler, because the handler’s responsibility is only to get the data and to send the responses. and all of the business logic has placed on the services, so when you need another logic, just inject the services that you need into the service that have a responsibility for the handlers, or the services isn’t proper if you inject to another service, and it should be standalone, just inject it directly to your handlers.
it’s similar to entities but the differences are:
- DTO use as a struct to store objects from request body, request param, query param, or response object
- as a response object, DTO has a responsibility to respond to the entity object to its object and respond as the JSON in the handlers
- don’t use DTO as a variable on the repository, and vice versa. don’t use an entity as a request, or response object
the router’s responsibility is to make a router and injected the dependencies (DB into repository, repository into services, and services into handlers). if I want to make a REST API I’ll make http_router.go. and if I want to make a RabbitMQ’s queue, usually I’ll make amqp_router.go or rabbitmq_router.go
Let’s build the Example
for this example project, I’ll use fiber, but without ORM. in the bellow section how I structure my projects
| |-- middleware
| |-- routes
| |-- utils
| |-- auth
| |-- encryption
| |-- web
I structured my projects based on modules (modules stored on src). and the modules have their dto, entities, repositories, services, handlers, and the router
first I usually make a DTO, the DTO looks like this:
I make two separate files, called as ProductsRequestBody and ProductsResponseBody. the ProductsRequestBody is responsible for decoding JSON data from request.body object and the ProductsResponseBody is responsible for encoding from entities.Products, because the response data should not an entity directly
here the example of the entities:
here looks like the repository of the products
the repository store all of the CRUD operations. In the repository, because it’s a dependency, I make the interface as the contract
in the service, I do the business logic, like deleting products, updating, or getting the products. I just call the repository here. and usually, I write a database transaction here, I also convert the dto object into entities object here
in the handlers, I only parsing the data from request.body into the DTO, then I pass the DTO object as the params for the services
and all of the dependencies injected here, like in productRepository need mysqlDB, in the productService neet productRepository, and the handler needs productService. after I inject the dependency I make the endpoint
You can see the example project here https://github.com/nurcahyaari/golang-starter
In this article, I didn’t implement the unit test yet, maybe in the next article, I want to show about implementing the unit test. and I also will update my starter project with a unit test. usually, I write a SQL mock with testify and use it as the assertion library.
Thank you for reading my article :D