Developing a Web Application in Go using the Layered Architecture
Writing a web-server using Go is very simple. But the challenge comes when the code has to be testable, structured, clean and maintainable.
If this is your first time writing a web application using Go, refer to my previous article:
Below, we are writing a simple server that stores and retrieves data from MySQL.
The code above needs refinement. Why?
- One function is doing more than one thing. When this application grows, it becomes hard to manage and test.
- There is no way to test the handlers without the database. The datastore is not injected and it is a global variable, thus making it hard to mock the database in the handler.
In an ideal scenario, the handler should not depend on the underlying datastore. A good approach to tackle this problem is to follow a layered architecture. Each layer will do only one thing.
The Layered Architecture
The three independent layers are delivery, use-case and datastore.
Delivery Layer:
The delivery layer will receive the request and parse anything that is required from the request. It calls the use case layer, ensures that the response is the required format and writes it to the response writer.
Use-Case Layer
The use case layer does the business logic that is required for the application. This layer will communicate with the datastore layer. It takes whatever it needs from the delivery layer and then calls the datastore layer. Before and after calling the datastore layer, it applies the business logic that is required.
Datastore Layer
The datastore stores the data. It can be any data storage. The use case layer is the only layer that communicates with datastore. This way each layer can be tested independently without depending on the other.
Since each layer is independent of each other, if the application grows to have gRPC support, only the delivery layer will change. Datastore and use case layer will remain the same. Even when there is a change in datastore, the entire application need not change. Only the datastore layer will change. This way, it is easy to isolate any bugs, maintain the code and grow the application.
Note: If you don’t have any business logic you can skip the use-case layer and have only delivery and datastore layer.
Each layer will communicate with each other through interfaces.
Refer to the following schema(datastore/animal.sql)
DROP DATABASE IF EXISTS animal;
CREATE DATABASE animal;
USE animal;CREATE TABLE animals(
id int NOT NULL AUTO_INCREMENT,
name varchar(50),
age int,
PRIMARY KEY(id));INSERT INTO animals VALUES(1,'Hippo',10);
INSERT INTO animals VALUES(2,'Ele',20);
For better structure, including a package entities and driver. entities will maintain all the structs that represent each entity in the application.
driver will have the functions to connect to datastores.
entities/animal.go
package entitiestype Animal struct {
ID int
Name string
Age int
}
driver/mysql.go
The directory structure:
├── datastore
│ ├── animal
│ │ ├── mysql.go
│ │ ├── mysql_test.go
│ ├── interface.go
│ │
├── delivery
│ ├── animal
│ │ └── http.go
│ │ └── http_test.go
│ │
├── driver
│ ├── mysql.go
│ │
├── entities
│ ├── animal.go
│ │
├── animal.sql
│ │
├── main.go
Datastore Layer:
This layer will use MySQL to store and retrieve the data related to Animal.
datastore/interface.go
package datastore
import "web-app/entities"type Animal interface {
Get(id int) ([]entities.Animal, error)
Create(entities.Animal) (entities.Animal, error)
}
Delivery Layer
This will receive the HTTP request and validate the filter for GET request and validate the request body in POST request.
This layers needs the store layer to be injected since it communicates with datastore layer to store and retrieve data.
This handler can be tested without depending on the datastore. You can mock the responses from the datastore and write the unit tests, testing only the handlers.
main.go
This will create the server. It connects to db by fetching the config from environment and then injects this db to the delivery layer.
As you can see, with this layered architecture it becomes easy to maintain the code.
- When there is a bug, it becomes easier to isolate and fix it.
- When the application grows and you decide to have another datastore, let us say you want to include caching then only the datastore layer will change without touching the delivery or use-case layer.
- Due to the independent layers, writing unit tests is simpler.
Happy Going.