Combining Modular Monolith and Hexagonal Architecture while Maintaining Domain Driven Design Principles (part 1)
A short and quick story about how to design a Modular Monolith and Hexagonal Architecture, combined by keeping the Domain Driven Design principle
Today’s story originates from a recent client case at SoftwareArchitect.ID. It began when a team lead from the client company contacted me via email (iman[at]softwarearchitect.id — feel free to email me for technical advice, especially on software architecture, transitioning from monolith to microservices, or migrating to cloud from on-premises). The discussion continued through a few discovery calls, leading us to an agreement in which I will offer consulting services to help them plan for scalability and the shift from a monolith to microservices.
Although the monolith performed well for many years, moving to microservices became necessary due to the current process and scalability challenges they encountered, such as:
The desire for an isolated fault or loose coupling between domains.
Most importantly, per their customer request, they need to scale certain parts of their systems differently: some features require more resources due to high throughput, while others, like back-office services, handle low traffic.
However, the transition to microservices was carried out quite aggressively. As a result, costs have risen roughly 8 to 10 times compared to the previous monolithic setup. And that shocked management when they saw the cost growing 8-10 times more than expected.
After a few weeks of discussion, I learned about their pain points and expectations from management, engineering, and the business needs. Along with many tradeoffs and capabilities that the system must have, such as scalability, loose coupling, isolated faults, and predictive costs, I proposed some options. However, the most suitable option for them at that time was to revert to a monolith, but in a modular way. But also decouple some components into separate microservices. What I told them is, “Modular monolith, decouple as needed.”
Aside from the modular monolith, I also introduced them to Hexagonal architecture and how we can combine it with the modular monolith to create a powerful solution for their current system.
In this article, I will briefly walk through how to design a modular monolith pragmatically. I know there are many examples already available on the internet. But here, I will use Golang. I’m not sure if I can do it entirely in Golang, but I’ll try to make it as close as possible to the Hexagonal architecture combined with the modular monolith approach.
Layered Architecture
I started my journey when the Model-View-Controller (MVC) pattern was a thing. I started web development with Yii Framework and Laravel back then, when I was still in college. And after I graduated, I landed in real adult life, and I met new things. And back then, layered architecture was still a thing, similar to MVC — kinda, but it’s mainly for the backend, consisting of Repository (data layer), Service, and Controller.
In this layered architecture, there are usually about three main layers or sometimes more, with a DTO layer. This practice typically divides the application horizontally, with many services in the service layer, many controllers in the controller package, and so on.
The disadvantage of this approach is that, although it is layered, changes to any layer (repository, service, controller) can affect the entire stack from top to bottom. Moreover, people are beginning to realize that having everything connected in this way is not truly scalable.
The Rise of Hexagonal Architecture
Then, some experts, such as Alistair Cockburn (the inventor of the Hexagonal architecture), began evangelizing it. The principle is similar to applying the classic adapter pattern in a more structured way.
Diagram referenced from: https://github.com/jmgarridopaz/bluezone
The domain contains the system’s business logic and language. It provides ports for both inbound and outbound external connections. These ports are connected to, consumed by, or implemented by adapters.
For example, an inbound port might be used by a REST API controller to receive requests and send the request context to the domain layer through a port or interface. Similarly, in outbound communication, the domain layer uses outbound ports to perform tasks such as querying data or calling external services.
The same idea can be used to develop a “super app” that might include a CLI interface, a queue interface, REST, and more, as shown in the earlier diagram. This approach also eliminates the need to separate the API from the repository of Cron job scripts.
Similarly, Kafka consumers and RabbitMQ consumers do not need to be stored in different repositories. All these components should reside in a single repository because, under the hexagonal architecture, they serve as adapters, while the core logic resides in the domain layer.
However, if we focus closely on the details and use the concept of “layered architecture,” which involves three layers within an application: repository (data), service, and controller—along with hexagonal architecture, it will resemble the diagram below.
If you compare the first layered architecture mentioned earlier to this hexagonal architecture, both are similar. However, in this hexagonal architecture, we use the term ports which are essentially interface contracts. An interface, as in Java, C#, or Go, consists only of function definitions without any implementation.
Several years ago, I made a blog post and a GitHub repository about “Go Clean Architecture” that actually follows this pattern here (https://github.com/bxcodec/go-clean-arch). In a way, it’s similar in that everything is connected through an interface (ports).
Diagram reference from https://github.com/bxcodec/go-clean-arch
From Uncle Bob, who inspired me to write the Go Clean Architecture, I learned that the deeper a layer is, the more it should be abstracted.
And quoting the blog post from Uncle Bob:
No, the circles are schematic. You may find that you need more than just these four. There’s no rule that says you must always have just these four. However, The Dependency Rule always applies. Source code dependencies always point inwards. As you move inwards the level of abstraction increases. The outermost circle is low level concrete detail. As you move inwards the software grows more abstract, and encapsulates higher level policies. The inner most circle is the most general.
In a way similar to Hexagonal, the deeper layer (the domain) will be agnostic to any concrete implementation/technology; the domain layer shouldn’t talk about “SQL”, it shouldn’t talk about “connection pooling”, etc. In fact, it only focuses on the business domain logic.
Problem with this approach?
At least, based on my experience, even with this kind of separation, the structure can get somewhat messier. Since slicing occurs across horizontal layers (repository, service, and controller), and additional files, such as interfaces (ports), are introduced, there are times when certain services import from other repositories. This approach increases complexity and steepens the learning curve, as components tend to import from each other and reside in the same directory.
Vertical Slicing with Domain Drive Design
Actually, the problem I mentioned earlier is quite common. Martin Fowler supports this in his article, where he also encourages vertical slicing alongside horizontal slicing to reduce the complexity of an extensive application with many domains.
Quoting from Martin Fowler's blog post
Although presentation-domain-data separation is a common approach, it should only be applied at a relatively small granularity. As an application grows, each layer can get sufficiently complex on its own that you need to modularize further. When this happens it’s usually not best to use presentation-domain-data as the higher level of modules. Often frameworks encourage you to have something like view-model-data as the top level namespaces; that’s OK for smaller systems, but once any of these layers gets too big you should split your top level into domain oriented modules which are internally layered.
As Martin Fowler stated, we also need to split the application vertically into module groups (domains), while maintaining the layered structure internally. So with the payment example previously, it will be evolved into a diagram like this below,
There were a few times when I also began applying this pattern to some of my projects, organizing them vertically by domain. For example, the payment module includes all payment-related implementations, from the service layer to the data (repository) layer.
This project organization helps build complex systems. Even with the rise of microservices, this pattern can still manage complexity. We can move from a high-level distributed system to more detailed code grouped by domain within each microservice, with each sub-domain divided horizontally by roles such as controllers, services, repositories/data.
Entering the Modular Monolith Era
The previous design, vertically sliced (based on domain) with a Hexagonal architecture, looks really nice and neat. This becomes somewhat cargo-culted among people: “if you build an application, you must do it in hexagonal, and vertically sliced based on domain,” etc.
Nevertheless, it works effectively with microservices or with services that are already set up as such. Properly organizing each service with a similar pattern is very helpful. And since it’s already isolated in a microservice, the domain usually remains within the same bounded context. Therefore, a vertically sliced, hexagonal architecture is invaluable in this situation.
However, microservices are not a cure-all for every problem. They provide solutions but also introduce new challenges, such as high costs and debugging difficulties. As a result, some are returning to a monolithic structure in a modular form, often called a modular monolith. This approach has gained popularity because it is less expensive than microservices and can be split into microservices later if needed.
Rather than creating multiple microservices with associated costs, all functionality stays within a single monolithic service. Internally, however, it is organized into modules based on different domains.
Implementation Challenges
This is how the problem started. If we build a modular monolith with all domains separated into individual packages or directories, cross-boundary calls still pose issues. For example, PaymentService needs to query the PaymentSettings, which belong to a different module.
Typically, based on my observations and past experiences, PaymentService imports the PaymentSettingsRepository directly. This is feasible because all interface modifiers are usually public. Technically, crossing imports to access the repository port or layer between domains is always possible. This can confuse some people, especially when working with a modular monolith. And it only makes the code base messier.
But what’s wrong with that approach?
As I previously mentioned, this issue is not a concern if we view it merely as a simple Monolith. However, our goal is to develop a “modular monolith.” Such a structure should also facilitate easier decoupling into standalone microservices when necessary.
Building on that idea, I foresee a future where we may need to split the domain into a separate microservice. For instance, we could eventually decouple the payment settings domain into its own microservice. In a microservice architecture, it seems unsuitable for the Payment Service microservice to access the Setting data layer (repository) directly.
In microservices, direct database access between services is considered an anti-pattern. When constructing a Modular Monolith, we should adhere to this principle when organizing modules within a monolithic application.
For example, if the Payment Service in a microservice architecture communicates with PaymentSettings via a “public” API such as REST, gRPC, or GraphQL, then in a Modular Monolith, the Payment Domain Module should similarly interact with the Payment Settings Domain or Module through a similar “public” API, interface, or contract. The main difference is that microservice communication occurs over a network, whereas within a Modular Monolith, communication is direct within the same process.
So, based on the previous diagram, ideally, we should design them as shown in the following diagram.
Instead of importing the payment-settings repository, here, PaymentService imports the PaymentSettingService interface (public API). And Payment Settings only exports the public API (interface) on the service while keeping other interfaces isolated internally.
The same pattern can also be combined with the CQRS pattern. If combined, we can view it as shown in the diagram below — I’ve included two options: one for direct command and another for async command via a queue or message broker. But the concept should remain similar.
So even later, we decouple them into separate microservices, with everything already isolated. We only need to change the protocol from a direct method call to a network API call. If you are already using a queue, that’s even better because the messaging is already abstracted in a modular monolith.
Final Words
Okay, enough of the theory — I know you guys are really eager to see how to do it in practice. With Java, C#, or any programming language that fully supports OOP, this should be straightforward. Since the key to building the modular monolith is using the access modifiers (private, protected, public) at the package level, which are built-in features in those programming languages. The main factor is determining which interfaces or functions constitute the “public API” and which should not be exposed to other modules.
But since I haven’t seen any Golang implementation that satisfies what I know (at least), I will write the example in Golang.
For simplicity, I will use the Payment and Payment Settings example. It might be too simple as an example to showcase the modular monolith. But hey, we have to start somewhere before going further to a more complex example, right?
The step-by-step process for building the modular monolith in Golang will be explained in the 2nd article after this. Initially, I put all the content into one article, but due to the newsletter’s limitations, I had to split it into two articles.
Combining Modular Monolith and Hexagonal Architecture while Maintaining Domain Driven Design Principles (part 1) — this article
Developing Modular Monolith and Hexagonal Architecture in Golang while Maintaining Domain Driven Design Principles (part 2) — subsequent article
Since establishing SoftwareArchitect.ID as my brand, I’ve worked as a fractional technical leader, mainly as a fractional software architect, for several years. My experience spans from small startups to medium-sized companies, more recently, serving as an advisor and architect for an enterprise migrating to the cloud and transitioning from a large monolith to microservices. Contact me at iman[at]softwarearchitect.id for consultation or a fractional role as an architect or tech lead related to building a scalable product and platform














