Applying Layered Architecture in Golang
This is an adobtion from clean architecture but with some tweaks that had made my projects very easy to maintain and manage. The tweak is to make everything about data models and taking control of the data; note that it doesn’t have much in common with the data driven structure.
In software architecture, layered design patterns are commonly used to structure applications into distinct levels of responsibility. This article discusses a unique layered architecture that adopts an infrastructure-first approach.
Layers:
This architecture is composed of four distinct layers, maintaining a strict separation of concerns. The design is about data and how to control the data and by what mean.
The data and the persistent sit on the first layer, business logic sits above it and interactions with outside on the most top layer. Each layer is ignorant of layers above them. For example, the data layer does not have any understanding of business logic, and business which sits above the data layer logic does not know anything about the APIs.
- Level 0: Infrastructure
The infrastructure layer provides foundational services for data interactions, such as database connectors, API clients, googleDocs client, message queue clients, even AWS in some cases, etc. Only the persistent layer should use this layer, and none of the above ones.
- Level 1: persistent (data source layer)
The persistent interacts with the infrastructure layer, providing a consistent interface for data access to the layers above. Uses the implemented classes in Infrastructure layer to manage the data. It contains models
folder that is used by all above layers.
- level 2: Business Logic / Service (actions on data)
This is the business logic. It might use persistent or directly use clients from Infrastructure layer. The methods in this layer receive and return persistent/models
from layer 3. They also wrap the errors from layer 1 and 2 with it’s own errors and return the error to API layer.
- level 3: APIs and Communication Layer (means of action)
Any API that receives requests from outside is considered as the api layer. For example graphql, restful api, event consumer, kafka consumer controller, gRPC controller, etc.
This layer can have more layers above it depending on the nature of it. For example, In the MVC structure, the View
appears as the layer 3 in a subdirectory in api/http
package. I can be placed at api/mvc/view
.
It has it’s own DTO models to bind the data from incoming sources (like http request) and to return the data to the caller.
It has responsibility of transforming persistent/models
and service/models
to api/http/dto
and back since the business logic is ignorant of the DTO and different api controllers might have different DTOs and data binding. In other words, DTOs in the API level will be converted to persistent/models
before sending to business logic layer. This makes it possible to have multiple controllers live beside each other, like api/http/restful
, api/restful
, api/tasks
, api/restful
, etc …
The benefit with this structure is ease of managing api versions
Example:
.
├── infrastructure ---> layer 0
│ ├── googleAPI
│ ├── postgresClient
│ ├── mysqlClient
│ ├── KafkaClient
│ ├── AuthenticationAPIClient
│ └── ...
│
├── persistent ---> layer 1 (Data management)
│ ├── event-bus : for publishing and consuming events. Publishing events happens by calling this library. It uses `infrastructure/X` clients to send and receive. It receives and returns `persistent/models` or `persistent/event-bus/dto`
│ │ └── dto
│ ├── exceptions
│ ├── cache
│ ├── repositories
│ │ ├── PostRepository
│ │ ├── UserRepository
│ │ └── ...
│ └── models : Data models
│ ├── User
│ ├── Post
│ └── ...
│
├── services ---> layer 2 (Business logic)
│ ├── exceptions
│ ├── authenticationService
│ │ └── dto
│ ├── postsService
│ ├── userService
│ └── ...
│
└── APIs ---> layer 3
├── graphQL └──> Layer 3-1 is responsible of restful APIs
│ ├── controller
│ ├── exceptions
│ └── dto : only and only used in the graphql controller. they should not be passed to business logic
│ ├── requests
│ └──responses
│
├── event-bus └──> layer 3-2
│ └── consumers : events arrive at this controller. it uses `infrastructure/event-bus/dto`
│
├── mvc: └──> Layer 3-3 for example if the service has a UI panel
│ ├── dto
│ ├── controller
│ ├── exceptions
│ └── view : imported by `api/mvc/controller` and just converts the `persistent/models` or `api/mvc/dto/` to html.
│
│
└── RESTful └──> layer 3-4
├── V1
│ ├── controller
│ │ ├── PostController
│ │ └── UserController
│ ├── dto : only and only used in the RESTful api package. they should not be passed to business logic.
│ │ ├── requests
│ │ └── responses
│ └── exeptions
│ ├── UserNotFound
│ └── ...
└── V2
├── controller
│ ├── PostController
│ └── UserController
├── dto : only and only used in the RESTful api package. they should not be passed to business logic.
│ ├── requests
│ └── responses
└── exeptions
├── UserNotFound
└── ...
Notes: dto
s and models
should not contain any methods that contain the business logic, since it makes it accessible to the layers above and it becomes unclear which layer should execute them.
Benefits:
Modularity and Separation of Concerns: Each layer is isolated from the layers above it. This separation makes the codebase more modular and easier to manage. For example, data models remain agnostic of business logic, improving code clarity and maintainability. Also adding new APIs would be very easy without need of changing anything in any of the layers, following Open-Closed Principle .
Layer Independence: Each layer operates independently, enhancing flexibility. This makes it easier to replace or modify specific layers without impacting others.
Code Clarity: The architecture’s design ensures that each layer has a well-defined role. This clarity fosters easier understanding, maintenance, and extension of the software.
Consistency: By centralizing data models in the persistent layer and ensuring uniform usage across layers, this architecture promotes consistency in data handling.
Clear Transformation Responsibility: The architecture clarifies that transformation of service models into different DTOs is handled by the API layer, avoiding ambiguity and code duplication.
Reduced Complexity in Controllers: By separating request coordination and data transformation, controllers become less complex and easier to manage.
Challenges and Solutions
Complex Infrastructure Layer: The infrastructure layer can become complex with multiple data sources. Good software design principles such as encapsulation, modularity, and readability should be enforced to manage this complexity.
Transformation Responsibility: Managing the transformation of service models into different DTOs for each controller can lead to code duplication. One solution is to perform this transformation at the controller level, as each controller knows its DTO requirements by adding a transform package.
Duplicated DTo and models: no need for explaination. This is the most troublesome part of this design which has it’s own benefits at the same time.
Conclusion
The layered architecture offers an organized and modular design that facilitates software development. By ensuring each layer has a distinct role and maintains separation of concerns, this architecture makes the software easier to understand, maintain, and extend.