How to Achieve More with Less Complexity

What is a Modular Monolith Architecture?

Modular Monoliths present a unique approach to software architecture, balancing the robustness of monolithic systems with the scalability of microservices. As a result, it allows for increased scalability and maintainability while avoiding the complexity and overhead commonly associated with microservices. In this essay, we will explore the advantages of utilizing a modular monolith architecture in Go and provide guidance on implementing it in your projects.

A modular monolith architecture is a single codebase divided into smaller, self-sufficient modules. Each module is responsible for a specific domain or functionality, communicating with other modules through well-defined interfaces. This approach enables easy scalability, as new functionality can be added by creating new modules, or existing modules can be replaced or updated without affecting the rest of the system. Additionally, having a single codebase for the entire application makes it easy to maintain, as all the necessary logic is in one place. It also allows for better organization, as developers can focus on specific application parts without navigating a complex codebase.

Why Go is Ideal for a Modular Monolith Architecture

Go is an excellent language for a modular monolith architecture. It provides strong typing and concurrency features that allow developers to utilize the language’s capabilities to define and implement modules. Go’s function types, interfaces, and structs provide a solid foundation for building modules, while the language’s built-in support for concurrency allows for easy parallelization and scalability.

Implementing a Modular Monolith in Go

Given its nature, there are countless ways to achieve this with Go. And here, I’ll speak abstractly to keep it as flexible as possible. To implement a modular monolith in Go, the initial step is to create a package for each module and define interfaces for the modules to communicate with one another. This is achieved by using Go’s `interface` type to define these interfaces, which grants ease in testing and flexibility. The interfaces should be clearly defined and specify the input and output of each function. This helps ensure that the modules are loosely coupled and can be easily replaced or updated.

“Go provides the solid foundation and flexibility to build modular monoliths, allowing for easy scalability and maintainability without the complexity of microservices.”

When organizing the code within each package, it’s advisable to adopt a cmd and internal package structure. The cmd package comprises the application’s main entry point, while the internal package contains the module’s implementation. This structure allows for better encapsulation, keeping the implementation details hidden from other packages. This makes testing the modules and making changes to the application without affecting other parts of the system more manageable.

The Importance of Testing in a Modular Monolith

Testing is crucial when building a modular monolith, and it is imperative to test each module individually. To accomplish this, mock implementations of the interfaces can be created for testing purposes. Go’s testing package can be utilized for unit testing, and Go’s testify package for more advanced testing, or even go-check  for those not faint of heart! This approach guarantees that each module functions correctly and that changes made to the application do not break existing functionality.

Propagating Request-Scoped Information

Managing dependencies between modules can be handled within a modular monolith using an inbuilt dependency manager. Additionally, Go’s context package can propagate request-scoped information through the application. This enables better data flow control and makes handling errors and timeouts simpler.

“Testing is the foundation upon which a modular monolith stands, guaranteeing the stability and reliability of the entire structure.”

Conclusion: The Benefits of a Modular Monolith Architecture in Go

In conclusion, a modular monolith architecture in Go is an efficient means of achieving scalability and maintainability in your application. It allows for a single codebase that can be easily divided into smaller, self-contained modules. Creating well-defined interfaces and utilizing Go’s standard library makes building and maintaining the application easier. Additionally, the application’s robustness and reliability can be guaranteed by implementing best practices such as testing and dependency management. The modular monolith architecture allows for a better codebase organization and encourages good practices such as encapsulation, decoupling, and testing. It is an ideal choice for projects that must be maintainable and scalable in the long term while avoiding the complexity and overhead of a microservices architecture.

Clarified with a simple example

Abstract of the proposed hierarchy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.
├── cmd
│   ├── main.go
│   └── [module_name]
│       ├── main.go
│       └── [module_name].go
├── internal
│   ├── [module_name]
│   │   ├── [module_name].go
│   │   ├── [module_name]_test.go
│   │   └── interfaces.go
│   └── [module_name2]
│       ├── [module_name2].go
│       ├── [module_name2]_test.go
│       └── interfaces.go
├── pkg
│   ├── [dependency]
│   └── [dependency2]
└── vendor
  1. The cmd directory houses the main entry point of the application and individual directories for each module. Each module’s directory comprises a main.go file for the module’s command-line interface and a [module_name].go file for the module’s implementation.
  2. The internal directory holds the implementation of each module, with a dedicated directory for each module. Each module’s directory includes the [module_name].go file for the module’s implementation, and a [module_name]_test.go file for the module’s tests.
  3. To adhere to idiomatic Go and ensure clear communication between modules, interfaces are declared within the implementation of each module, in the interfaces.go file. Do not be steered away if interfaces.go seem to collide with idiomatic practices - this is to avoid duplication and to provide a proposed interface, but it is still up to the caller of the module to pick whether that one will be inherited or if a leaner solution would take its place.
  4. The pkg directory contains any additional packages or libraries used by the application, such as third-party packages or common utilities that are shared between modules. This helps to keep the application’s codebase organized and makes it easier to manage dependencies.

As we’ve previously discussed, simplicity is key when it comes to structuring a modular monolith architecture in Go. The proposed directory structure is a tried and true approach that has proven to be effective for the majority of projects. In fact, from my own experience, I’ve found that over 70% of projects would benefit more from this straightforward approach, rather than the complexity and overhead of a microservices environment. However, for a small portion of projects, a slight level of additional segregation, such as separating handlers, services, and repositories vertically per HTTP request, may be beneficial and even required. This topic will be covered in a separate article. And, while it’s true that only a select few projects truly require a microservices approach, those that do require a significant investment in funding, manpower, and a willingness to navigate the added complexity. But for those who are up to the challenge, it can truly be a tech ninja’s El Dorado. 🚀

Boilerplate

Update 25th of January, 2023.

The followup article in which I’ve provided a more detailed Project Structure -> Modular Monolith - Bolerplate . 🚀


🎨 Crafting software is an art, and our canvas is simplicity. We believe in creating solutions that are not only elegant in design but also robust and tested to withstand the test of time. Our approach is to provide a solution that meets stakeholders’ requirements and ensures long-term maintainability and scalability. Our ultimate aim is to deliver efficient, effective, and adaptable software to the ever-evolving needs of businesses without succumbing to the allure of unnecessary complexity.

If that is what you seek, then contact us at contact@decantera.dev or via our site decantera.dev . 🚀