Skip to content

Hexagonal Architecture

This page describes the software architecture of the DrinkIt project, which is based on the principles of Hexagonal Architecture (also known as Ports & Adapters). The goal of this architecture is to clearly separate business logic from technical details and external dependencies (like databases, external APIs, or user interfaces).

1️⃣ Principles

The core idea is to place the business logic and data at the heart of the application (the hexagon) and to communicate with the outside world through "ports" and "adapters".

🛸 The Hexagon (The Domain)

Contains the pure business logic, without any dependency on infrastructure technologies.

🧩 Ports

These are interfaces defined by the domain. There are two types of ports:

  • 🔌 Primary Ports (Driving Ports): This is the domain's API, the use cases that the application exposes. They are called by external actors (e.g., a user via a REST API).

  • 🧲 Secondary Ports (Driven Ports / SPI): These are the dependencies required by the domain, such as the need to persist data or send a notification. The domain defines the interface, but the implementation is external.

🔧 Adapters

These are the concrete implementations of the ports that connect the hexagon to the outside world.

  • 🧭 Primary Adapters (Driving Adapters): They drive the application. Examples include REST controllers or message consumers that translate an external request into a call to a primary port of the domain.

  • 🛠️ Secondary Adapters (Driven Adapters): They are driven by the application. These are the implementations of the secondary ports (e.g., a database access class, a client for an external web service).


2️⃣ Structure in Gradle Modules

To enforce this separation, the project is divided into three distinct Gradle modules. This physical structure enforces the dependency flow of the hexagonal architecture.

🧠 drinkit-domain

This is the core of the application, the hexagon. It is completely independent of the other layers.

Responsibilities:

  • 🧱 Contain all business logic, organized into bounded contexts.
  • 📜 Define secondary ports as interfaces (SPI - Service Provider Interface) for infrastructure needs (e.g., UserRepository, NotificationService).
  • 🚪 Expose primary ports (use cases) through services.

Dependency Rules:
This module must not depend on any other project module (drinkit-backend or drinkit-infra). It can have dependencies on external libraries not related to infrastructure (e.g., utils, starters).

📝 Note with Spring:

For pragmatic reasons and to avoid boilerplate code (like creating Spring Beans who instanciate each domain usecase in drinkit-backend), the use of Spring annotations like @Service or @Transactional is allowed in this module. Spring is used here as a simple dependency injector, not as an infrastructure framework.


🏗️ drinkit-infra

This module contains the secondary adapters. It provides the technical implementations for the interfaces defined in the domain.

Responsibilities:

  • 🧩 Implement the interfaces (secondary ports) from the drinkit-domain module.
  • 🗃️ Manage communication with the database (e.g., with Jooq DSL, datasource configuration).
  • 🌐 Contain clients for calling external web services.
  • ⚙️ Handle any other technical aspects (file system, message bus, etc.).

Dependency Rules:
This module only depends on drinkit-domain.


🚪 drinkit-backend

This module contains the primary adapters. It's the application's entry point.

Responsibilities:

  • 🌍 Expose REST APIs (e.g., using OpenAPI generated classes and Spring MVC).
  • 📥 Contain handlers for asynchronous messages (e.g., RabbitMQ Listeners).
  • 🔁 Translate HTTP requests or messages into calls to the services in drinkit-domain.

Dependency Rules:
This module depends on drinkit-domain and drinkit-infra (only to register infra services from the dependency injection framework). It does not have access to classes and dependencies of drinkit-infra, which ensures that a controller cannot directly call a persistence implementation class.


🧭 Architecture Diagram

The diagram below illustrates the communication flows and dependencies between the modules. The call originates from a primary adapter (drinkit-backend), goes through the domain (drinkit-domain), which uses an interface (secondary port) whose implementation is provided by a secondary adapter (drinkit-infra).

MIT Licensed