Table of Contents
In the software development field, microservices, hexagonal architecture and DDD (Domain Driven Design) are the most popular topics.
These paradigms are now the hallmark of scalable, maintainable, and solid enterprise applications. But there’s a growing misunderstanding: To use hexagonal architecture or DDD you need to use microservices.
This myth has led many developers to choose complex, distributed systems because they believe these are the only way to achieve a clean architecture and model a domain correctly. The fact is that you don’t even need microservices to use hexagonal architecture or DDD. These patterns are independent of implementation models but are based on design principles.
And we can end up with a modular monolith and still receive all the benefits of these architectural patterns without adding the cost and complexity of a distributed system.
This article will dispel the myth, stepping through hexagonal architecture and DDD even without the presence of microservices, and illustrating how monoliths, when done right, are just as potent.
Now, let’s take a glimpse at hexagonal architecture and DDD and inspect what their core principles are before seeing how do concepts apply to the beyond of microservices.
Hexagonal Architecture and DDD
What Is Domain-Driven Design?
Domain-driven design (DDD) is a software application development technology that brings the technical and domain together. The aim is to implement complex business rules and workflows in software that are based on real-world behavior and concepts.
The key building blocks of DDD include:
- Entities: Objects with a distinct identity that persists over time.
- Value Objects: Immutable objects that describe aspects of the domain.
- Aggregates: Clusters of entities and value objects treated as a single unit.
- Repositories: Abstractions for retrieving and storing aggregates.
- Domain Events: Events that reflect something important that happened in the domain.
- Bounded Contexts: Explicit boundaries within which a particular domain model is defined and applicable.
DDD is about a common language, understanding, and modularity – not about how the software is built.
What Is Hexagonal Architecture?
The hexagonal (or ports-and-adapters) architecture was first conceived by Alistair Cockburn as a spatial metaphor to help describe independent interfaces to an application from its implementation and use. The main point is to have a clean architecture not tied to delivery (HTTP APIs, UIs), persistence (databases), and completely testable.
The architecture consists of:
- Core (Inside): The domain model and business rules.
- Ports: Interfaces that define how the core communicates with the outside world.
- Adapters (Outside): Implementations of the ports that connect the application to databases, APIs, UIs, etc.
This allows for an extremely modular and flexible system where business rules are separated from technicalities.
This allows developers to:
- Replace infrastructure pieces without affecting business logic.
- Test the business logic only.
- Keep issues separate.
There is also nothing here that requires microservices. This is a structure, not an implementation.
Why the Confusion?
The emergence of cloud-native computing and the DevOps movement has made microservices the standard architecture to build scalable software. A lot of info and discussions tend to bundle DDD and hex architecture as well as microservices, so the association is somewhat implied.
But this relationship is correlational, not causal.
- DDD and hexagonal architecture are especially suitable for microservices because they enable dealing with complexity and maintaining a separation of concerns.
- That said, microservices are not DDD and hexagonal architecture.
In reality, attempting to DDD and hex architecture out in most distributed service architectures is often enough to cause more pain than it solves if you don’t know the domain pretty damn well. Complex orchestration, funky data consistency, and challenging debugging become the new normal.
Debunking the Myth of Microservices
1. Hexagonal architecture and DDD focus on modularity, not distribution.
Pros and cons of Hexagonal Architecture. That means splitting the application into different services is not the only way. The same goes for DDD – bounded contexts are a definition of logical boundaries, they are not necessarily physical service boundaries.
2. Monolithic applications benefit from the DDD and hexagonal principles.
- The benefit of having well-implemented monoliths with hexagon architecture and DDD is:
- Better separation of concerns: domain logic is independent of other systems.
- Better testability: Business rules can be tested without a database or API.
- Reduced maintenance/headache: A clean domain model is easier to refactor.
- Transition to services over time: Modularity inside a monolith is a path to microservices as a system evolves.
3. Microservices introduce complexity
Microservices have their own set of challenges, however, such as:
- Operational complexity: You need to orchestrate, log, and monitor many different services.
- Distributed consistency: Transactions across microservices involve patterns for the Saga pattern.
- Infrastructure expenses: Additional services increase the costs of deployment and maintenance.
Personally, I believe that for most projects, starting with a well-organized monolith is better than microservices.
The Case for the Modular Monolith
The modular monolith is the system deployed as a whole, but with a clear seam between its different components and with separation at a logical level. Every module represents a domain/subdomain and follows DDD and hexagonal architecture principles.
Benefits:
- Simplicity: Whole lot simple for debugging, testing, deploying, and monitoring.
- Runtime performance: No network calls between modules.
- Cohesion: You can still have rich domain models and bounded contexts.
- Evolution: Modules can evolve into microservices if and when required.
What begins with a modular monolith:
- You prove your domain model.
- Keep it clean, literally.
- Simplify the infrastructure.
If your team can’t grasp the complexity of a modular monolith, you are not ready for microservices.
Implementing Hexagonal Architecture and DDD in a Monolithic System
1. Define Clear Bounded Contexts
Even monolithic codebases should define bounded Contexts for organizing domain logic well. For example:
- User Management Context (Handles user authentication and profiles)
- Order Processing Context (Manages orders, payments, and shipping)
- Billing Context (Handles invoices and financial transactions)
There should be one domain model per Bounded Context, which should be isolated from other domain models.
2. Use Ports and Adapters for Flexibility
Bring in Ports and Adapters as a way to separate business logic from infrastructure
Example:
Define a Port (Interface):
public interface IOrderRepository
{
void Save(Order order);
Order FindById(Guid orderId);
}
Implement an Adapter (Infrastructure Layer):
public class OrderRepository : IOrderRepository
{
private readonly DbContext _context;
public OrderRepository(DbContext context)
{
_context = context;
}
public void Save(Order order) => _context.Orders.Add(order);
public Order FindById(Guid orderId) => _context.Orders.Find(orderId);
}
That way, the main domain logic only depends on the interfaces and not the actual implementations of the database.
3. Separate Application Logic from Domain Logic
Use case orchestration is supposed to be handled by Application Services, but business rules belong to the domain model.
public class OrderService
{
private readonly IOrderRepository _orderRepository;
public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void PlaceOrder(Guid customerId, List<OrderItem> items)
{
var order = new Order(customerId, items);
_orderRepository.Save(order);
}
}
4. Ensure Testability with Dependency Injection
Separating business logic from infrastructure makes it possible to unit test domain logic in separation.
[Fact]
public void Should_Create_Order_Successfully()
{
var repositoryMock = new Mock<IOrderRepository>();
var service = new OrderService(repositoryMock.Object);
service.PlaceOrder(Guid.NewGuid(), new List<OrderItem> { new OrderItem("Product1", 2) });
repositoryMock.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
}
When (and how) to Evolve to Microservices
If your monolith has been properly designed -DDD and hexagonal architecture for example- the extraction to microservices will become simpler:
- Discover bounded contexts that change at different rates.
- Extract them as standalone services with the domain and adapter infrastructure that already exists.
- Introduce service communication (REST, messaging, etc.).
- And ensure atomicity if you need to use the event or distributed transactions.
But don’t do it too soon. Extract only when:
- There is an obvious limit for scalability.
- The frequency of deployment from one module to another varies.
- The module is also stable and valuable in its own right.
Conclusion
When you design your software solutions using hexagonal architecture and DDD, you can make them more powerful and easier to maintain in a structured and organized manner. But they are design philosophies, not implementation plans.
You do not need to immediately adopt microservices to enjoy the advantages of these patterns. You can make a monolith that is easy to manage and test by following the principles of DDD, which is a way of designing software that focuses on the domain or the problem that the software solves. Starting with a well-designed modular monolith is not just sufficient; it is often the optimal approach. This method enables teams to develop a thorough understanding of the subject matter, define precise limits, and delay expensive expenses related to distributed systems until they are necessary.
Microservices can lead to a complex system that is spread out, so they should only be used when it is necessary. When you use bounded contexts, ports, adapters, and a well-defined domain model, you can make modular monoliths that can turn into microservices when necessary.
• Safeguard your domain from being tangled up in infrastructure issues.
• Boost your testability and flexibility.
• Set the stage for a seamless shift to microservices when the moment calls for it.
The way you design your software, whether it is a single large system or many smaller ones, matters a lot for how well it works. In summary, prioritize creating a well-structured system before implementing a distributed one. Start simple and scale up as needed.
If you’re interested in learning more about software architecture, be sure to check out ApiumAcademy, which offers specialized courses and workshops for companies looking to strengthen their technical teams.
Author
-
I am a Computer Engineer by training, with more than 20 years of experience working in the IT sector, specifically in the entire life cycle of a software, acquired in national and multinational companies, from different sectors.
View all posts