Hexagonal Architecture: How to Isolate Domain from Frameworks
Learn how to structure software systems by decoupling business rules from frameworks, databases, and external interfaces using hexagonal architecture.
Summary
- Structural decoupling protects core logic against sudden shifts in external libraries.
- Ports act as formal contracts defining the entry and exit points of the application core.
- Adapters function as translators to connect the outside world, such as HTTP APIs and relational databases.
- Automated testing becomes significantly faster by eliminating heavy infrastructure dependencies.
- Long-term maintainability offsets the initial complexity of project setup.
The Silent Problem of Framework Coupling
When we start building a system, it is common to choose a popular framework and build all business logic directly on top of it. Initially, this brings speed. However, as the system grows, vital company rules become entangled with third-party libraries, web routers, and ORMs, which are object-relational mapping tools used to translate code into database tables. In practice, this means updating a major framework version can turn into a technical nightmare.
Excessive coupling turns software into a fragile structure. If your database changes or if the user interface needs to migrate technology, the entire system suffers deep restructuring. Hexagonal architecture, also known as ports and adapters, emerges precisely to solve this fundamental software engineering dilemma.
What Is Hexagonal Architecture and Its Origin
Conceived by Alistair Cockburn in the early 2000s, hexagonal architecture proposes a radical change in how we view software. Instead of organizing code into traditional horizontal layers like presentation, business, and data, it separates the application into concentric zones. At the absolute center sits the application domain, representing the heart of the business, completely free of technical details.
The hexagonal shape has no strict geometric meaning; it merely illustrates that the application has multiple faces to interact with the outside. In practice, this means the core does not know whether it is responding to an HTTP request, a message queue, or a command line. It simply executes pure business operations and returns results.
Understanding Ports: The Communication Contracts
Ports act as formal interfaces that establish strict rules on how the outside world can interact with the application core. There are essentially two types of ports: input ports, which receive requests from users or external systems, and output ports, which allow the core to request external services, such as data persistence or email sending.
To illustrate in practice, imagine an output port called 'UserRepository'. It defines only that the system needs to save and fetch users, without mentioning whether the database used will be PostgreSQL, MongoDB, or disk JSON files. This independence protects business logic from technological fluctuations.
Adapters: The Real World Translators
If ports define contracts, adapters are responsible for implementing those contracts for the real world. An input adapter could be a REST controller that receives web JSON requests and translates them into calls the core understands. An output adapter could be a concrete class implementing the repository interface using the database ORM.
In practice, the adapter acts as a linguistic translator. The application core speaks only the pure language of business rules. The adapter translates this language into the technical dialect required by the framework, database, or communication protocol used at that specific moment.
Practical Code Implementation
To visualize the architecture in practice, observe the clear separation between the interface defining the port and the adapter implementation talking to external infrastructure.
# Output Port (Inside Domain)
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def save(self, user):
pass
# Output Adapter (In Infrastructure)
class PostgresUserAdapter(UserRepository):
def __init__(self, db_connection):
self.db_connection = db_connection
def save(self, user):
self.db_connection.execute('INSERT INTO users...', (user.name,))
This code block demonstrates how the domain defines the need to save a user through an abstract class. The infrastructure implements this need using a specific technology, keeping the domain isolated from database details.
Real Advantages and Operational Trade-offs
Adopting hexagonal architecture brings expressive benefits, especially for testability. Since the core does not depend on databases or web servers, it is possible to write extremely fast automated tests that run directly in memory. Furthermore, maintainability improves drastically over the years, as different teams can work on distinct adapters without interfering with central logic.
However, not everything is an advantage. The main trade-off is the initial increase in complexity and the amount of boilerplate code, which refers to repetitive translation classes. Small projects or MVPs, meaning minimum viable products created to quickly validate ideas, may suffer from excessive structural bureaucracy if they use this approach from day one.
Final Thoughts on Design Scalability
Hexagonal architecture is not a silver bullet applicable to any software, but rather a powerful tool for complex systems demanding high longevity and technological independence. By shielding the domain against constant changes in frameworks and tools, companies gain the freedom to evolve their technical infrastructure without rewriting their business rules.
Investing time in the correct design of ports and adapters requires discipline from the engineering team, but the return manifests as more flexible systems, easier to test, and prepared to absorb new market demands with minimal technical friction.