This is an example of a Ports and Adapters (P&A for short) architecture in Java 21 and Spring Boot.
We can define a simple P&A architecture as follows:
graph LR
RestControllerAdapter --> ApplicationService
ApplicationService --> Entity
Entity --> RepositoryPort
RepositoryPort --> RepositorySQLAdapter
subgraph Outside
RestControllerAdapter
RepositorySQLAdapter
end
subgraph Inside
ApplicationService
Entity
RepositoryPort
end
PAdapter(Primary Adapter) -.-> RestControllerAdapter
PPort(Primary Port) -.-> ApplicationService
SAdapter(Secondary Adapter) -.-> RepositorySQLAdapter
SPort(Secondary Port) -.-> RepositoryPort
A possible Spring Boot implementation of the adapters is:
// RestControllerAdapter (Primary Adapter)
@RestController
class TeamController {
private final TeamService teamService; // Primary Port
@GetMapping("/teams")
public List<TeamData> getTeams() {
return teamService.getTeams();
}
}
// TeamRepositoryPort (Secondary Port)
interface TeamRepository extends ListCrudRepository<Team, Long> {
}
// The SQL Adapter of the TeamRepositoryPort is provided by Spring Data JPA. It is the Secondary Adapter.
Yes, a ports and adapters architecture is only this.
Patterns like Domain Events, Aggregates, Value Objects, CQRS and Event Sourcing, all those patters go beyond the intention or responsibility of a ports and adapters architecture. Indeed, a P&A architecture works well with CRUD models (or models with not many domain logic) as well as Domain Models.
The beauty of it is that it adapts to each part of your business, and you can choose the right amount of abstraction/complexity for each scenario.
IMHO, this is where a P&A shines. It really helps you create a solid and adaptable test suite. Here a short example of test types.
End-to-end tests
Integration test
- HTTP adapter. TeamControllerIntegrationTest
- Service and SQL Repository. TeamServiceIntegrationTest
- SQL Repository. TeamRepositoryIntegrationTest
Unit test
- HTTP adapter. TeamControllerUnitTest
- Service. TeamServiceTest
- Domain. TeamTest
./gradlew bootTestRun
โน๏ธ It uses Spring Boot integration with Testcontainers to spin up a PostgreSQL container.
curl -X GET http://localhost:8080/teams
curl -X POST -H "Content-Type: application/json" -d '{"name": "Team 1"}' http://localhost:8080/teams
curl -X PATCH -H "Content-Type: application/json" -d '{"name": "Team 1"}' http://localhost:8080/teams/{id}/rename
./gradlew test
You will see how the thin ports and adapters architecture allows us to test the application with multiple approaches:
- Unit tests for the domain logic.
- Integration tests for the application service.
- End-to-end tests for the REST API.