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.
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.
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:
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.
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.
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 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.
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.
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.
The core exposes so-called ports, which are simply interfaces. These are divided into two types:
Adapters form the layer that implements or utilizes the aforementioned ports:
The greatest advantage of this approach is the complete isolation of business logic from external dependencies. As a result:
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