String Boot Hexagonal Architecture Example
By Ercan - 25/10/2025
Architectural concepts like ports, adapters, domain, infrastructure frequently feel abstract when you first read about them. To move from theory to intuition I built a small, multi-module Spring Boot project: a calculator that performs addition, subtraction, multiplication and division, and logs every calculation to a database.
The point of this project is not complex business logic — it’s to show how Hexagonal Architecture (aka Ports & Adapters) organizes code so the domain remains framework- and I/O-agnostic, while adapters provide concrete interfaces to the outside world (REST, SOAP, DB, etc.).
TL;DR: If you want to skip the explanation and dive straight into the code, check out the full implementation on GitHub: https://github.com/ercansormaz/hexagonal-architecture
Motivation
When learning architecture, two things really help:
- Separate concerns clearly so each piece has only one responsibility.
- Keep the domain code framework-free so tests and reasoning remain simple.
I designed this project as a multi-module Maven project to make dependencies and responsibilities explicit. That makes it easier to explain the why and how of Hexagonal Architecture to others — and to yourself.
Project overview
Top-level structure:
hexagonal-calculator ├── domain ├── application ├── infrastructure ├── rest-api-adapter └── soap-service-adapter
Short purpose of each module:
- domain: core business model and interfaces (ports). Pure Java, no framework dependencies.
- application: use case implementations — orchestration layer that calls domain services and ports.
- infrastructure: persistence adapters (JPA entity + repository + adapter implementing domain output port).
- rest-api-adapter: REST controllers and DTOs; wires application beans into Spring.
- soap-service-adapter: SOAP endpoints and DTOs; same wiring pattern as REST adapter.
This separation highlights the main promise of Hexagonal Architecture: we can swap adapters or add new ones without touching the domain or use-case logic.
Domain (the heart)
The domain module contains model classes and port interfaces only:
Calculation— domain model that represents an operation and result.CalculationType— enum:ADD,SUBTRACT,MULTIPLY,DIVIDE.port.in.CalculateUseCase— input port (what the application exposes as use cases).port.out.CalculationLogger— output port (how the domain asks to persist/log calculations).service.CalculatorService— pure-java service that performs the arithmetic.
Key idea: domain code should express business intent without mentioning persistence, HTTP, or Spring.
Application (use-case implementation)
CalculateUseCaseImpl sits in the application module and implements CalculateUseCase. It orchestrates domain services and calls output ports as needed.
Example (simplified add implementation — this is your actual code):
public Calculation add(double a, double b) {
Calculation calculation = calculatorService.calculate(a, b, CalculationType.ADD);
calculationLogger.log(calculation);
return calculation;
}
Why this placement is correct:
- The application layer is responsible for orchestrating the flow of a use case.
- It calls the domain service that performs the pure calculation and then delegates persistence to a port (
CalculationLogger). - The application knows what must be done (run the calculation and persist it), but it doesn’t know how persisting is implemented — that’s the adapter’s job.
Alternative approaches (not necessary here): using domain events or an event-driven pipeline for logging/auditing. Those make sense at larger scale but add complexity not needed in this tutorial example.
Infrastructure (persistence adapter)
The infrastructure module contains:
CalculationEntity— JPA entity representing persisted calculation records.CalculationRepository— Spring Data JPA repository.CalculationLoggerJpaAdapter— implementsCalculationLoggerport and maps domainCalculation→CalculationEntityfor DB persistence.
With this layout, DB details live only in the infrastructure module; domain and application modules are free of JPA and Spring types.
Adapters: REST and SOAP
I intentionally added two external adapters to demonstrate the plug-and-play nature of Hexagonal Architecture:
- REST adapter (
rest-api-adapter) exposes HTTP endpoints for the four operations, defines DTOs (CalculationRequest,CalculationResponse), and registers domain/application beans into the Spring context withBeanConfig. - SOAP adapter (
soap-service-adapter) exposes equivalent SOAP endpoints and DTOs and uses the sameBeanConfigapproach.
Both adapters depend on application and infrastructure. Because business rules and use cases live in the inner modules, either adapter can be added or replaced without touching the application/domain logic.
Wiring beans (example)
Both adapter modules include a small config that registers the necessary beans (as Spring beans) so controllers/service endpoints can call the CalculateUseCase:
@Configuration
public class BeanConfig {
@Bean
public CalculatorService calculatorService() {
return new CalculatorService();
}
@Bean
public CalculateUseCase calculateUseCase(CalculatorService calculatorService,
CalculationLogger calculationLogger) {
return new CalculateUseCaseImpl(calculatorService, calculationLogger);
}
}
This keeps adapters responsible for wiring, while the application and domain remain POJOs.
Running the project
From the module directories:
Run REST adapter:
cd rest-api-adapter mvn spring-boot:run
Run SOAP adapter:
cd soap-service-adapter mvn spring-boot:run
Both adapters reuse the same domain and application logic — exactly the point of the architecture.
Example Requests
To illustrate how the adapters work in practice, here are sample requests for REST and SOAP endpoints.
REST API
Addition
curl --location 'http://localhost:8080/rest/calculator/add' \
--header 'Content-Type: application/json' \
--data '{
"operand1": 10,
"operand2": 5
}'
Response:
{
"result": 15.0
}
Multiplication
curl --location 'http://localhost:8080/rest/calculator/multiply' \
--header 'Content-Type: application/json' \
--data '{
"operand1": 10,
"operand2": 5
}'
Response:
{
"result": 50.0
}
SOAP API
The same operations can be invoked via SOAP as well:
Addition
curl --location 'http://localhost:8080/SOAP/CalculatorService' \
--header 'Content-Type: text/xml' \
--data '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cal="https://ercan.dev/poc/hexagonal-architecture/calculator">
<soapenv:Header/>
<soapenv:Body>
<cal:add>
<request>
<operand1>10</operand1>
<operand2>5</operand2>
</request>
</cal:add>
</soapenv:Body>
</soapenv:Envelope>'
Response:
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:addResponse xmlns:ns2="https://ercan.dev/poc/hexagonal-architecture/calculator">
<response>
<result>15.0</result>
</response>
</ns2:addResponse>
</S:Body>
</S:Envelope>
Multiplication
curl --location 'http://localhost:8080/SOAP/CalculatorService' \
--header 'Content-Type: text/xml' \
--data '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cal="https://ercan.dev/poc/hexagonal-architecture/calculator">
<soapenv:Header/>
<soapenv:Body>
<cal:multiply>
<request>
<operand1>10</operand1>
<operand2>5</operand2>
</request>
</cal:multiply>
</soapenv:Body>
</soapenv:Envelope>'
Response:
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:multiplyResponse xmlns:ns2="https://ercan.dev/poc/hexagonal-architecture/calculator">
<response>
<result>50.0</result>
</response>
</ns2:multiplyResponse>
</S:Body>
</S:Envelope>
These examples show that both REST and SOAP adapters invoke the same domain and application logic, demonstrating the flexibility and decoupling offered by Hexagonal Architecture.
Design notes & tradeoffs
- Logging/persisting in the application layer (as you do) is correct. The application layer knows the use-case flow and is the right place to call an output port for persistence/audit.
- Domain events would be useful if you later need multiple independent consumers (e.g., audit service, analytics, notification). For the current simple example, a direct port call is clearer and easier to understand.
- Multi-module layout adds some upfront complexity, but for teaching and architecture clarity it’s extremely valuable.
👉 You can explore the full source code on GitHub:
https://github.com/ercansormaz/hexagonal-architecture
Tags: spring boot, software architecture, hexagonal architecture, java
