Develop an API with Go using dev container - 2

hero image Photo by C Dustin on Unsplash

In the previous post, we set up a development environment using a dev-container and verified that our Go code successfully connects to MariaDB. In this chapter, we will discuss an architectural pattern for the API we are building. Before we get started, I’ll introduce a pattern called “Fat controllers”.

The fat controllers

Fat controllers are controllers that contain all related business logic and handle request/response-related tasks.

However, maintaining the codebase with fat controllers can become difficult as the project scales up. The downsides of this pattern include:

  1. Difficult to test
  2. May contain lots of duplicate logic
  3. Less flexibility
  4. Does not follow the Single Responsibility Principle (SRP)

While it is acceptable to build this kind of controller with a demo or proof-of-concept project, if you truly care about maintainability and scalability, I suggest avoiding this pattern in production.

Service / Repository Pattern

As I will demonstrate a more maintainable and scalable way of writing a Go API, I will implement it using the Service/Repository Pattern.

service repository pattern

The diagram demonstrates a cart API using the Service/Repository Pattern. We separate the “Fat controller” into three layers based on their responsibilities.

Controller:
  1. check whether a user can access the specific resource.
  2. check whether a user has permission to use the specific resource.
  3. validating the incoming request .
  4. handle error.
  5. send response.
  6. place to write Open API annotation
Service:
  1. process the business logic
Repository:
  1. Provide a layer to abstract the implementation to communicate the DB

By encapsulating logic in the service layer, we can make it reusable across other services. Using the repository to hide the implementation from the database ensures that swapping out different types of databases will not break the service or controller layer.

We can improve unit testing by mocking the service and repository layers that are not related to the unit test. However, how can we elegantly swap the dependencies of a specific service or controller? The answer is “Dependency injection (DI)“.

Dependency injection (DI)

Dependency injection is a method to decouple the dependency between two related classes.

First, we will use the interface to describe the method of the class. Then, we will implement the class method according to the interface.

DI with service

You can think of this interface as an “API document” between the services and controller. It promises the consumed service that it will output a specific value with the specific input.

Once we have created the interface to describe the service, we can inject it into the consumed service. Because the injected service will implement the interface (the API document), you can swap any service that implements the specific interface. Since the interface is not changed, we can expect the service to output the same type of value for the same input.

DI with mock service

With Dependency Injection, we can replace a service with a mock service that implements the same interface during unit testing

Summary

We have gone through the basic terminology and techniques that will be used to implement our Go API. Go has many awesome DI libraries, such as Wire and dig. However, in this document, we will implement our own DI to better understand what DI really does in the next chapter.