Hexagonal Architecture in Java
Hexagonal Architecture in Java
Want to learn more about the hexagonal architecture?
Join the DZone community and get the full member experience.
Join For FreeHexagonal architecture is a model or pattern for application design. It is also known as a ports-and-adapter architecture. The core logic is embedded inside a hexagon, and the edges of the hexagon are considered the input and output.
It divides the application into the inside and outside parts. Inside parts are the core logic of the application. The outside part could be the UI, database, messaging connectors, etc. Thus, the application's business logic is isolated from outside concerns. Communication between the two happens using so-called ports and adapters.
Port
The port is simply a gateway to your application, allowing inbound and outbound flow. An inbound port exposes application functionality to the outside world. For example, a service interface exposing the core logic is an inbound port. An outbound port defines the core’s view of the outside world. A repository interface that enables communication from the core application to a data source is an outbound port.
Adapters
Adapters are an implementation of ports.
Primary Adapters
Also called driving adapters, primary adapters actually drive the application and/or invoke actions on the application using the inbound ports of application.
REST controllers or WebViews are examples of primary adapters. They use service interfaces (inbound ports of the application) to communicate with the core logic.
Secondary Adapters
Also called driven adapters, secondary adapters are implementations of the outbound port that are invoked/driven by the core application. However, since they are invoked using outbound ports, the application is not tightly coupled to the secondary adapters.
Connections to the database and external API calls are both examples of secondary adapters.
Putting it Together
Application users want to connect to the system or core application. The application has exposed the inbound ports; however, application users are presented with primary adapters, which use the inbound port to connect to the system. The system, in turn, uses the outbound port to connect to some external system for its data needs, etc. An outbound port is implemented by secondary adapters to expose a way to connect to the external system.
Let’s try to understand all of this using a simple example.
Example
We will use a Pizza Service
application as an example to understand this architecture. Pizza Service
has three features:
- It allows us to create
Pizza
- List a particular
Pizza
(by name) - List all
Pizzas
available inStore
Implementation
The Value Object
Let’s start with the core of the application. At the center of application, you will have a Pizza
object.
public class Pizza implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private String[] toppings;
// Constructors , Getters/Setters etc.
}
Inbound port
Applying the hexagonal architecture, our core application will expose its functionality using an inbound port. Hence, we need to define the inbound port, PizzaService
.
public interface PizzaService {
public void createPizza(Pizza pizza);
public Pizza getPizza(String name);
public List<Pizza> listPizza();
}
This inbound port exposes the application to the outside world.
Outbound Port
We have an external system for creating Pizza
s. The core application will use an outbound port to access this external system to create or access Pizza
s. Let’s define an outbound port, PizzaRepo
.
public interface PizzaRepo {
void createPizza(Pizza pizza);
Pizza getPizza(String name);
List<Pizza> getAllPizza();
}
Primary Adapters
Applying the hexagonal architecture principles, you would have primary adapters using inbound ports. Let’s define a REST controller as our primary adapter, providing endpoints for creating and accessing Pizza
s . This REST controller secondary adapter will use PizzaService
to connect to the core application. The code for PizzaRestUI interface is available here.
@RestController
@RequestMapping("/pizza")
public class PizzaRestController implements PizzaRestUI {
@Autowired
private PizzaService pizzaService;
@Override
public void createPizza(@RequestBody Pizza pizza) {
pizzaService.createPizza(pizza);
}
@Override
public Pizza getPizza(@PathVariable String name) {
return pizzaService.getPizza(name);
}
@Override
public List<Pizza> listPizza() {
return pizzaService.listPizza();
}
}
Secondary Adapters
According to the definition above, we need an implementation of the outbound port. PizzaRepo
is our outbound port; let’s implement this.
@Repository
public class PizzaRepoImpl implements PizzaRepo {
private Map<String, Pizza> pizzaStore = new HashMap<String, Pizza>();
@Override
public void createPizza(Pizza pizza) {
pizzaStore.put(pizza.getName(), pizza);
}
@Override
public Pizza getPizza(String name) {
return pizzaStore.get(name);
}
@Override
public List<Pizza> getAllPizza() {
return pizzaStore.values().stream().collect(Collectors.toList());
}
}
Communication From Core to DownStream System
The core application uses the outbound port to communicate to the downstream system. So, let’s implement the implementation for core service.
@Service
public class PizzaServiceImpl implements PizzaService {
@Autowired
private PizzaRepo pizzaRepo;
@Override
public void createPizza(Pizza pizza) {
pizzaRepo.createPizza(pizza);
}
@Override
public Pizza getPizza(String name) {
return pizzaRepo.getPizza(name);
}
@Override
public List<Pizza> listPizza() {
return pizzaRepo.getAllPizza();
}
}
Running the PizzaService
The code is available on GitHub. You can download and run the application and hit the following endpoints:
POST http://localhost:8080/pizza
Input:
{
"name" : "Margherita",
"toppings" : ["tomato","onion","cucumber"]
}
POST http://localhost:8080/pizza
Input:
{
"name" : "Margherita",
"toppings" : ["tomato","onion","cucumber"]
}
- GET http://localhost:8080/pizza/Margherita
- GET http://localhost:8080/pizza/Veg_Exotica
- GET http://localhost:8080/pizza
Conclusion
To conclude, the hexagonal architecture offers the following advantages:
- It layers your classes/objects in such a way that the core logic is isolated from external elements. In our application above, we see that the external user will see REST endpoints, which internally use inbound ports to communicate with the core application. The core application uses an outbound port to communicate with the downstream system, and you achieve a higher degree of decoupling with this.
- It enables you to replace one adapter with another without any impact on the core logic or application. For example, if you want to implement it the way
PizzaRepoImpl
is implemented, you just need to define a new implementation ofPizzaRepo
and register it using the@Repository
annotation while removing the current implementation. This will lead cause no change to thePizzaServiceImpl
(core logic). - Testing is eased out as you can easily mock the ports using mock adapters.
- The ports-based architecture makes your application very flexible to adapt or connect to new channels or use new communication protocols.
Published at DZone with permission of Milind Joshi. See the original article here.
Opinions expressed by DZone contributors are their own.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}