Tło

Blog

Ports and adapters architecture in Java: A Clear Guide

Java ports and adapters are an architecture that increases application flexibility. Learn how to separate layers and design systems that are resilient to change. The graphic shows a keyboard and a programmer coding.
Ikona

Three-tier architecture in Java – a standard worth knowing.

Ports and Adapters in Java

For many years, the accepted standard in the Java world has been dividing the web application backend into three main layers. Moreover, let’s look at Ports and Adapters in Java.

  1. Controllers – accepting requests from the user interface (the so-called endpoints),
  2. Business logic – consisting of entities and services operating on those entities,
  3. Persistence layer – responsible for reading and writing data to the database or communicating with external services.

Dependencies between layers

Entities, meaning domain objects, represent the nouns gathered during the requirement analysis phase. They contain fields, getters, and setters, and their structure often mirrors database tables (the so-called anemic domain model).

The controller layer utilizes the business logic. On the other hand, the logic relies on the persistence layer. In other words, each higher layer depends on the lower one and operates within its capabilities.

The Drawbacks of the Traditional Approach

While the three-layer approach works well for simple CRUD (Create, Read, Update, Delete) operations, more complex business rules bring a unique set of challenges:

1. Harder Testability

Business logic services become cluttered with conditional statements (`if`), increasing the risk of missing a critical edge case. Therefore, testing often requires a database connection, which slows down the process and complicates automation.

2. High Dependency on the Persistence Layer

Application development usually kicks off with database configuration. Consequently, this forces concrete design decisions right at the start, because the way the business logic operates is constrained by the methods provided by the lower layer.

3. Leaking Technical Details

Sometimes the application relies on external services like message queues or libraries. Consequently, their implementation details “leak” into the logic layer. As a result, any change in the lower layers can require modifications to the business logic itself. This breaks the Single Responsibility Principle (SRP), which states that every class should have only one reason to change.

 

The Solution

The solution to these issues is to invert the dependencies—the business logic layer should not depend on the lower layers; instead, the lower layer should provide exactly what the business logic needs.

Ports and Adapters

Ports and Adapters Architecture (Hexagonal) – How Does It Work?

An architecture that inverts the traditional dependency between layers is called Ports and Adapters. It is also frequently referred to as Hexagonal Architecture, a name derived from how it is visually represented in diagrams.

What is the Application Core?

The central element of this architecture is the Application Core, which contains all the business logic. The core is completely unaware of technical implementation details. This ensures it remains entirely independent of both infrastructure and external technologies.

Ports – Communication Interfaces

The core exposes so-called ports, which are simply interfaces. These are divided into two types:

  • Input ports (primary/driving ports): They allow the outside world to invoke the application’s logic. They are used to perform actions like creating a new resource or retrieving data.
  • Output ports (secondary/driven ports): They are used by the business logic to communicate with the outside world. They enable integration with databases, external APIs, or other modules.

Adapters – Ports and Adapters in Java

Adapters form the layer that implements or utilizes the aforementioned ports:

  • Input adapters (driving adapters): They handle and trigger input ports. An example? REST endpoints that receive HTTP requests, map the data, and pass it along to the business logic.

  • Output adapters (driven adapters): They implement the interfaces defined by the output ports. They are responsible for tasks like writing data to a database or sending messages to an external system.

Where Does the Magic of Hexagonal Architecture Lie?

The greatest advantage of this approach is the complete isolation of business logic from external dependencies. As a result:

  • Deferred technical decisions: You can develop and evolve the system without having to immediately decide on details like which database to use.

  • Effortless unit testing: Testing your core business logic requires no environment setup (such as spinning up database connections), making tests incredibly fast and reliable.

  • Simpler technological migrations: Switching a third-party service provider or database vendor becomes straightforward, as the change is completely isolated to the adapter layer.

A Different Package Structure

The example above is just one of many options for directory structures. For small domains, such a large number of packages might feel like over-engineering. In those cases, you can simplify it by grouping code into just three main packages: api, domain, and infrastructure.
On the other hand, with extensive and complex models, you might want to categorize your ports even further by adding primary and secondary subpackages.
It is perfectly fine to start with a simpler structure and refactor into a more granular setup once you notice the codebase has grown significantly. The most critical step is to align and establish a shared standard across your entire organization, team, or project.

You can find more about the Java language here: Zapytania w języku Java – znaczenie i zastosowanie