Skip to content

Latest commit

 

History

History

docker-mariadb-clean-arch

Docker MariaDB Clean Architecture

A slightly complex REST application with Fiber to showcase Clean Architecture with MariaDB as a dependency with Docker.

Prerequisites

  • Docker Compose for running the application.
  • Shell that supports sh, make, and curl for end-to-end testing. UNIX systems or WSL should work fine.
  • Postman if you want to test this API with GUI.

Application

This application is a slightly complex example of a REST API that have four major endpoints. A public user can access the User, Auth, and Misc major endpoints, but they cannot access the City endpoint (as it is protected). If one wants to access said endpoint, they have to log in first via the Auth endpoint, and only after that they can access the City endpoint.

This application uses MariaDB as a database (dockerized), and JWT as an authentication mechanism. This application also showcases how to perform 1-to-many relational mapping in Clean Architecture (one user can have multiple cities), and also the implementation of JOIN SQL clause in Go in general.

Clean Architecture

Clean Architecture

Clean Architecture is a concept introduced by Robert C. Martin or also known as Uncle Bob. Simply put, the purpose of this architecture is to perform complete separation of concerns. Systems made this way can be independent of frameworks, testable (easy to write unit tests), independent of UI, independent of database, and independent of any external agency. When you use this architecture, it is simple to change the UI, the database, or the business logic.

One thing that you should keep in mind when using this architecture is about Dependency Rule. In Clean Architecture, source code dependency can only point inwards. This means that the 'inner circle' of the system cannot know at all about the outside world. For example, in the diagram above, use-cases knows about entities, but entities cannot know about use-cases. Data formats used in outer circle should not be used by an inner circle.

Because of this, when you change something that is located the innermost of the circle (entities for example), usually you have to change the outer circles. However, if you change something that is not the innermost of the circle (controllers for example), you do not need to change the use-cases and the entities (you may have to change the frameworks and drivers as they are dependent on each other).

If you want to learn more about Clean Architecture, please see the articles that I have attached below as references.

System Architecture

For the sake of clearness, here is the diagram that showcases the system architecture of this API.

System Architecture

Please refer to below table for terminologies / filenames for each layers that are used in this application. The project structure is referred from this project. In the internal package, there are packages that are grouped according to their functional responsibilities. If you open the package, you will see the files that represents the Clean Architecture layers.

For the dependency graph, it is straightforward: handler/middleware depends on service, service depends on repository, and repository depends on domain and the database (via dependency injection). All of the layers are implemented with the said infrastructure (Fiber, MariaDB, and Authentication Service) in above image.

I have slightly modified the layers in this application to conform to my own taste of Clean Architecture.

Architecture Layer Equivalent Layer Filename
External Interfaces Presenters and Drivers middleware.go and handler.go
Controllers Business Logic service.go
Use Cases Repositories repository.go
Entities Entities domain.go

Basically, a request will have to go through handler.go (and middleware.go) first. After that, the program will call a repository or a use-case that is requested with service.go. That controller (service.go) will call repository.go that conforms to the domain.go in order to fulfill the request that the service.go asked for. The result of the request will be returned back to the user by handler.go.

In short:

  • handler.go and middleware.go is used to receive and send requests.
  • service.go is business-logic or controller (some might have different opinions, but this is my subjective opinion).
  • repository.go is used to interact to the database (use-case).
  • domain.go is the 'shape' of the data models that the program use.

For the sake of completeness, here are the functional responsibilities of the project structure.

  • internal/auth is used to manage authentication.
  • internal/city is used to manage cities. This endpoint is protected.
  • internal/infrastructure is used to manage infrastructure of the application, such as MariaDB and Fiber.
  • internal/misc is used to manage miscellaneous endpoints.
  • internal/user is used to manage users. This endpoint is not protected.

Please refer to the code itself for further details. I commented everything in the code, so I hope it is clear enough!

API Endpoints / Features

This API is divided into four 'major endpoints', which are miscellaneous, users, authentication, and cities.

Miscellaneous

Endpoints classified here are miscellaneous endpoints.

  • GET /api/v1 for health check.

Users

Endpoints classified here are endpoints to perform operation on 'User' domain.

  • GET /api/v1/users to get all users.
  • POST /api/v1/users to create a user.
  • GET /api/v1/users/<userID> to get a user.
  • PUT /api/v1/users/<userID> to update a user.
  • DELETE /api/v1/users/<userID> to delete a user.

Authentication

Endpoints classified here are endpoints to perform authentication. In my opinion, this is framework-layer / implementation detail, so there is no 'domain' regarding this endpoint and you can use this endpoint as an enhancement to other endpoints. Authentication in this API is done using JSON Web Tokens.

  • POST /api/v1/auth/login to log in as the user with ID of 1 in the database. Will return JWT and said JWT will be stored in a cookie.
  • POST /api/v1/auth/logout to log out. This route removes the JWT from the cookie.
  • GET /api/v1/auth/private to access a private route which displays information about the current (valid) JWT.

Cities

Endpoints classified here are endpoints to perform operation on City domain. Endpoints here are protected via JWT in the cookie, so if you are going to use this endpoint, make sure you are logged in first (or at least have a valid JWT).

  • GET /api/v1/cities to get all cities.
  • POST /api/v1/cities to create a new city.
  • GET /api/v1/cities/<cityID> to get a city.
  • PUT /api/v1/cities/<cityID> to update a city.
  • DELETE /api/v1/cities/<cityID> to delete a city.

Installation

In order to run this application, you just need to do the following commands.

  • Clone the repository.
git clone git@github.com:gofiber/recipes.git
  • Switch to this repository.
cd recipes/docker-mariadb-clean-arch
  • Run immediately with Docker. After you run this command, migration script will be automatically run to populate your dockerized MariaDB.
make start
  • Test with Postman (set the request URL to localhost:8080) or with the created end-to-end testing script. Keep in mind that the end-to-end script is only available for the first run. If you are trying to run it the second time, you might not be able to get all of the perfect results (because of the auto-increment in the MariaDB). Please run make stop and make start first if you want to run the test suite again.
make test
  • Teardown or stop the container. This will also delete the Docker volume created and will also delete the created image.
make stop

You're done!

FAQ

Some frequently asked questions that I found scattered on the Internet. Keep in mind that the answers are mostly subjective.

Q: Is this the right way to do Clean Architecture?

A: Nope. There are many ways to perform clean architecture - this example being one of them. Some projects might be better than this example.

Q: Why is authentication an implementation detail?

A: Authentication is an implementation detail because it does not interact with the use-case or the repository / interface layer. Authentication is a bit strange that it can be implemented in any other routes as a middleware. Keep in mind that this is my subjective opinion.

Q: Is this the recommended way to structure Fiber projects?

A: Nope. Just like any other Gophers, I recommend you to start your project by using a single main.go file. Some projects do not require complicated architectures. After you start seeing the need to branch out, I recommend you to split your code based on functional responsibilities. If you need an even more strict structure, then you can try to adapt Clean Architecture or any other architectures that you see fit, such as Onion, Hexagonal, etcetera.

Q: Is this only for Fiber?

A: Nope. You can simply adjust handler.go and middleware.go files in order to change the external interfaces / presenters and drivers layer to something else. You can use net/http, gin-gonic, echo, and many more. If you want to change or add your database, you just need to adjust the repository.go file accordingly. If you want to change your business logic, simply change the service.go file. As long as you the separation of concerns is done well, you should have no need to change a lot of things.

Q: Is this production-ready?

A: I try to make this as production-ready as possible 😉

Improvements

Several further improvements that could be implemented in this project:

  • Add more tests and mocks, especially unit tests (Clean Architecture is the best for performing unit tests).
  • Add more API endpoints.
  • Add a caching mechanism to the repository layer, such as Redis.
  • Add transaction support.
  • Maybe try to integrate S3 backend to the repository layer (MinIO is a good choice).
  • Maybe add a domain folder in the internal package where we can leave the entities there?

Discussion

Feel free to create an issue in this repository (or maybe ask in Fiber's Discord Server) in order to discuss this together!

References

Thanks to articles and their writers that I have read and found inspiration in!