DEV Community

Tejas Rawat
Tejas Rawat

Posted on • Originally published at Medium on

Hexagonal Architecture (Ports and Adapters) Explained: A Practical Guide from Concept to Code

🏷 Introduction

In this article, we’ll explore one of the most practical and powerful architectural styles for backend systems: Hexagonal Architecture , also known as Ports and Adapters.

It’s a modern evolution of traditional layered architecture that shifts the focus to what matters most the business domain , not the frameworks, databases, or external tools surrounding it.

🏷️ What to expect in this article

In this article we’ll stay language-agnostic. Whether you’re working in Java, Python, JavaScript, or any other technology stack, the principles remain the same. Here’s what we’ll cover:

  1. What is Hexagonal Architecture?
  2. Dependency inversion principle in Hexagonal Architecture
  3. Data flow and dependency flow in Hexagonal Architecture
  4. Traditional Layered Architecture vs Hexagonal Architecture
  5. When to Use Hexagonal Architecture
  6. Conclusion

We’ll also walk through a real-world example based on a Java + Spring Boot Task Management Service project, including its structure, flow, and key architectural decisions, all of which are open-sourced on GitHub.

Ready? Let’s start by breaking down what Hexagonal Architecture is and why it’s worth knowing this architecture style.

🏷️ What is Hexagonal Architecture?

Hexagonal Architecture (also known as Ports and Adapters ) is a software design pattern that structures applications to make the core domain logic :

  • Isolated from frameworks, databases, APIs, and UIs
  • Testable and technology-agnostic
  • Adaptable to change

Image description

Core Concepts

πŸ“Œ Domain:
Contains business rules and core models. Declares interfaces (called ports ) for input and output

πŸ“Œ Ports:
β€’ Input ports
define how external actors interact with the domain.
β€’ Output ports define how the domain communicates with external. systems.

πŸ“Œ Adapters:
Input Adapters
(e.g., GraphQL, REST)
β€’ Receive queries/commands
β€’ Implement the input port
β€’ Translate external formats (e.g., DTOs) to domain models
Output Adapters (e.g., MySQL, Elasticsearch)
β€’ Connect to external systems
β€’ Implement the output port
β€’ Translate domain models to entities (e.g., DB rows)

️🏷️ Dependency Inversion Principle

In Hexagonal architecture:
πŸ”– The domain defines input/output ports (interfaces) that:

β€’ Accept domain models as input
β€’ Return domain models as output

πŸ”– Adapters interact with these ports:
β€’ Input adapters (e.g., GraphQL, REST controllers) call input ports to invoke business logic
β€’ Output adapters (e.g., MySQL, Elasticsearch repositories)implement output ports to handle external side effects

This inverts traditional dependencies:

Instead of the domain depending on external frameworks, frameworks depend on the domain.

πŸ’‘ This ensures:
β€’ Core domain logic remains isolated and independent
β€’ You can swap technologies (e.g., databases, UI) without touching business rules

️🏷 Data flow and dependency flow in Hexagonal Architecture

Let’s break down the data and dependency flow using the example of the createTask operation in a task-management-service. The same principles apply to other operations as well.

  • createTask mutation takes tittle & description as mandatory input field while dueDate is optional πŸ“£ NOTE: This is a basic sample schema for learning Hexagonal Architecture using a simple TODO app. In a real-world TODO application, there would likely be many more fields and operations.
input CreateTaskDto {
 title: String!
 description: String!
 dueDate: Date
}

type Mutation {
    createTask(input: CreateTaskDto!): TaskDto!
}

type TaskDto {
    id: String!
    title: String!
    description: String!
    priority: TaskPriorityDto! (HIGH, MEDIUM, LOW, NONE (default))
    status: TaskStatusDto! (CREATED (default), TODO, IN_PROGRESS, DONE)
    dueDate: Date
    createDate: Instant!
}
Enter fullscreen mode Exit fullscreen mode

Package structure:

β”œβ”€β”€ adapters
β”‚ β”œβ”€β”€ in
β”‚ β”‚ β”œβ”€β”€ graphql
β”‚ β”‚ β”‚ β”œβ”€β”€ mapper
β”‚ β”‚ β”‚ β”‚ └── TaskGQLMapper.java : Maps domain to GraphQL DTO
β”‚ β”‚ β”‚ β”œβ”€β”€ mutation
β”‚ β”‚ β”‚ β”‚ └── TaskMutation.java : Calls TaskService
β”‚ β”‚ └── rest : (Implement later...)
β”‚ └── out
β”‚ └── repository
β”‚ β”œβ”€β”€ elasticsearch : (Implement later...)
β”‚ └── mysql
β”‚ β”œβ”€β”€ entity
β”‚ β”‚ └── TaskMySQLEntity.java
β”‚ β”œβ”€β”€ mapper
β”‚ β”‚ └── TaskMySQLMapper.java : Maps MySQL entity to domain
β”‚ β”œβ”€β”€ repository
β”‚ β”‚ └── TaskMySQLRepository.java : Implements TaskDBPort.java
β”œβ”€β”€ application
β”‚ └── TaskServiceImpl.java : Implements TaskService
β”œβ”€β”€ domain
β”‚ β”œβ”€β”€ model
β”‚ β”‚ β”œβ”€β”€ CreateTask.java
β”‚ β”‚ β”œβ”€β”€ Task.java
β”‚ └── port
β”‚ β”œβ”€β”€ in
β”‚ β”‚ └── TaskService.java : Task createTask(CreateTask input);
β”‚ └── out
β”‚ └── TaskDBPort.java : Task saveTask(Task task);
Enter fullscreen mode Exit fullscreen mode

Package details:

πŸ“Œ domain: Owns the core business logic (domain models : CreateTask, Task) and is framework-agnostic, meaning its plain Java classes in this case without any Spring or other framework annotation.

πŸ“Œ ports (Interfaces):
β€’ TaskService.java(input port) defines operations the domain exposes to the outside like TaskMutation in this case or TaskController if we implement REST.
β€’ TaskDBPort.java(output port) defines how the domain interacts with external systems like databases.

πŸ“Œ adapters:
β€’ input adapters (e.g., GraphQL) invoke the domain by calling input ports.
β€’ output adapters (e.g., MySQL repositories) implement output ports to handle persistence or external interaction.

πŸ“Œ application:
Coordinates the flow between the input port and output port by implementing business logic TaskServiceImpl.. This layer may include minimal framework-specific code, for example, annotations for dependency injection such as @Service in Spring. However, it does not contain any infrastructure logic (like calling DB directly), keeping it close to the domain while enabling practical runtime integration.

Image description

By now, we’ve seen how the Dependency Inversion Principle (DIP) plays a crucial role in shaping the structure of a Hexagonal Architecture. Let’s unpack this with more clarity.

In this design:

  • Input adapters (such as GraphQL or REST controllers) do not contain business logic themselves. Instead, they delegate work to the domain by calling input port interfaces like TaskService.java These ports define the operations the domain exposes (e.g., createTask) and operate entirely on domain model objects.
  • This separation of concerns means input adapters are plug-and-play. For example, if you want to add a REST API alongside GraphQL, you don’t need to duplicate business logicβ€Šβ€”β€Šyou just reuse the existing TaskService interface. The only thing new is the adapter code that translates REST requests into domain calls.

Just like input adapters rely on input ports to interact with the domain, output adapters work through output ports to handle responsibilities such as persistence, messaging, or integrations with external services.

In our case:

  • The domain layer defines an output port interface , for example, TaskDBPort.java which abstracts the idea of saving or retrieving a Task without tying it to any specific storage mechanism.
  • The output adapter , such as a MySQL implementation, provides the actual behavior by implementing this interface e.g., TaskMySQLRepository.java. It takes care of ORM mapping, SQL queries, and data source management. This adapter translates domain models into persistence models (like JPA entities) and back.

This design pattern ensures:

  • Isolation of business logic: The domain doesn’t know or care where the data goes or comes from. It simply calls an interface.
  • Plugability: You can introduce a new data source, such as Elasticsearch or a remote service, by implementing the same output port without touching the domain or application logic.
  • Resilience to tech shifts: If your storage changes (say, from MySQL to MongoDB), only the output adapter needs to change not the core logic.

️🏷 Traditional Layered architecture vs Hexagonal architecture

Image description

While the data flow ( Controller > Service > Repository ) remain the same in both the architecture, the major difference lies in the direction of dependency.

In a traditional layered architecture, the core business logic depends directly on outer layers like the controller and repository. Let’s break this down with an example:

  • TaskController: createTask(CreateTaskRequestDto)
  • TaskService: createTask(CreateTaskRequestDto)
  • TaskRepository: saveTask(TaskEntity)

Here’s where tight coupling comes into play:

  • The service layer depends on specific DTOs defined in the controller layer (e.g., CreateTaskRequestDto), meaning changes in the controller can ripple into the business logic.
  • The service also directly uses repository implementations or specific ORM entities like TaskEntity , tying it to a particular database technology or persistence framework.
  • Apart from dependency there is framework (Like Spring in case of Java backend) code across the layer.

This tight coupling makes the system harder to test, evolve, or switch components (e.g., swapping from MySQL to Elasticsearch, REST to GraphQL) without modifying core logic.

️🏷 When to use Hexagonal Architecture

Hexagonal Architecture introduces a clean separation between core business logic and infrastructure concerns, making systems more modular, testable, and adaptable to change. However, this comes with some added complexity, such as:

  • A steeper learning curve due to more structured (and sometimes non-intuitive) package organization.
  • Additional transformation logic especially in controllers and repositories to conform to input/output port interfaces.

βœ… Use Hexagonal Architecture when:

πŸ“Œ Your application relies on multiple and evolving integration points

If your business logic interacts with various external systems (databases, messaging systems, third-party APIs), and you foresee potential changes to any of these, Hexagonal is a great fit. For example:

  • Migrating from SQL to NoSQL due to scalability.
  • Switching from Kafka to Google Pub/Sub.
  • Replacing ServiceNow with Jira as the ticketing system. The architecture allows you to isolate and swap out these integrations with minimal impact to the core business logic.

πŸ“Œ You’re building a large-scale monolith

Monoliths often contain significant business logic and numerous integration points. Here, the clear separation Hexagonal Architecture offers helps manage complexity and improve maintainability.

However, in microservices , where the scope is narrow and focused on a single business capability the benefits may not outweigh the added complexity. Changing a database or integration in a small microservice may not justify introducing multiple architectural layers.

πŸ“Œ The application is business-logic heavy, not infrastructure-heavy

Hexagonal Architecture shines when your application is centered around complex business rules or workflows. It enables clear encapsulation of those rules, independent of technical choices.

On the other hand, if your app is mostly CRUD-heavy with high database read/writes and minimal business logic, layering ports/adapters might be unnecessary overhead. In such cases, keeping things simple may be more pragmatic.

️🏷 Conclusion

Hexagonal Architecture is a great choice when:

  • You need flexibility around infrastructure decisions.
  • Your app’s complexity lies in the business domain.
  • You want a loosely coupled, testable system that’s resilient to change.

But it might be overkill for:

  • Small microservices with limited logic.
  • Simple CRUD-based apps that are more infrastructure-driven than domain-driven.

Source Code

GitHub - TejasRawat/task-management-service: A Spring Boot application for managing tasks CRUD operations. Supports both GraphQL and REST APIs, with interchangeable database implementations (Elasticsearch and MySQL) to suit different needs. Built with Hexagonal Architecture for flexibility and scalability.

Resources and related links:

Top comments (0)