NatterNet: A Deep Dive into Clean Architecture for Golang Chat Servers
Understanding how Clean Architecture provides a seamless, scalable, and maintainable base for applications.
Building real-time chat applications can be a complex endeavor, given the variety of components involved: WebSockets for real-time communication, databases for message storage, and scalable architecture to handle increased load. This article explores how we implemented Clean Architecture principles in NatterNet, an Open Source project that aims to combine best practices around Golang, WebSockets, Domain-Driven Design, and other state-of-the-art technologies.
What is Clean Architecture?
Clean Architecture, proposed by Robert C. Martin, is an architectural pattern that prioritizes the separation of concerns in a system. It aims to make the system flexible, maintainable, and testable, with a strong emphasis on dependency inversion and clear boundaries between different components.
More information : https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Key Components of Clean Architecture
Entities: Central business logic.
Use Cases: Application-specific rules and use-cases.
Interface Adapters: Controllers, gateways, and presenters.
Frameworks and Drivers: Web servers, databases, and libraries.
How is Clean Architecture implemented in NatterNet?
Directory Structure
domain/
: Contains all the domain entities and business logic.chat/
: Chat-related domain logic.user/
: User-related domain logic.
application/
: Houses the application services, which orchestrate the use-cases.infrastructure/
: Contains all the external interfaces and drivers.db/
: MongoDB drivers and mappers.event/
: NATS event system.
presentation/
: Controllers, routes, and API handlers.http/
: HTTP routes and controllers.websocket/
: WebSocket routes and controllers.
WebSockets with FastHTTP (Fiber)
NatterNet utilizes the high-performance FastHTTP package via the Fiber framework to handle HTTP and WebSocket requests.
Event-Driven System with NATS
We use NATS as our event-driven system to ensure real-time updates across various services and user interfaces.
MongoDB as a Database
MongoDB provides us with a scalable, NoSQL database solution, and is integrated through a Repository pattern in the infrastructure/db/
directory.
A flow of NatterNet HTTP Request
1. Interface Layer (Presentation)
HTTP Request Entry Point
When an HTTP request hits the server, the first layer it interacts with is the Interface layer, often through an HTTP router or controller. In NatterNet, this would be housed within the presentation/http/
directory.
For example, let's consider a POST
request to create a new chat message.
func (h *handler) createMessage() fiber.Handler {
// Handler code here
}
2. Application Layer
Use-Case Orchestration
Once initial validation is done in the Interface layer, the request data is passed on to the Application layer. The Application layer orchestrates the use-case and coordinates between various domain services and repositories. This would typically be in the application/
directory.
res, resErr := h.application.MessageCommandHandler.CreateMessage(&request)
3. Domain Layer (Core Business Logic)
Domain Services & Encapsulated Entities
The Application layer then communicates with the Domain layer, which contains the core business logic and domain entities. Here, various domain services process the request and perform operations that are critical to the business rules of the application. This would be in the domain/
directory.
func (s *MessageService) CreateMessage(request *MessageCreateRequest) (*Message, error) {
// Business logic here
}
4. Infrastructure Layer
Repositories & Data Storage
Finally, the Domain layer talks to the Infrastructure layer to persist or retrieve data. This is where we communicate with databases or other external services. In NatterNet, this would be in the infrastructure/db/
directory for MongoDB interactions.
func (repo *MessageRepository) Create(message *Message) (*Message, error) {
// Database operation here
}
5. Data Response
The data then trickles back through the layers in the opposite order, getting transformed at each stage to adhere to HTTP or API response formatting standards. The Interface layer finally sends this formatted response back to the client.
return f.Status(fiber.StatusOK).JSON(res)
In summary, the flow for a given HTTP request goes from Interfaces (HTTP handlers) to Application (use-cases) to Domain (core business logic and entities), and then to Infrastructure (external services and data storage). Then it goes back up this chain to ultimately provide an HTTP response to the client. This separation of concerns allows for better maintainability, testability, and scalability of the application.
What's Next?
Our ongoing efforts aim to refine the architecture, add new features, and improve performance. Your contributions can help shape NatterNet into a go-to template for building Golang applications with Clean Architecture.
In our next blog post, we will take a deep dive into Domain-Driven Design (DDD) and explore how it's implemented in NatterNet. We'll dissect the strategic and tactical patterns, from bounded contexts to entities and aggregates, giving you a comprehensive understanding of how DDD principles are used to solve complex business problems in a real-world project.
For more details and to contribute to the project, visit NatterNet on GitHub.