Hexagonal Architecture (also known as Ports and Adapters) is an architectural pattern that aims to create loosely coupled application components that can be easily connected to their software environment by means of ports and adapters.
Ports are interfaces defined in the domain layer that specify what the application needs from the outside world, without knowing the implementation details.
Implements GameRepositoryPort using MongoDB as the persistence mechanism.Location: infrastructure/persistence/mongo/MongoGameRepositoryAdapter.java:1-36
Implements PlayerRepositoryPort using MySQL with R2DBC for reactive access.Location: infrastructure/persistence/mysql/MySqlPlayerRepositoryAdapter.java:1-64
package cat.itacademy.s05.t01.blackjack_api.infrastructure.persistence.mysql;import cat.itacademy.s05.t01.blackjack_api.domain.model.Player;import cat.itacademy.s05.t01.blackjack_api.domain.model.PlayerId;import cat.itacademy.s05.t01.blackjack_api.domain.model.PlayerName;import cat.itacademy.s05.t01.blackjack_api.domain.port.PlayerRepositoryPort;import org.springframework.stereotype.Component;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;@Componentpublic class MySqlPlayerRepositoryAdapter implements PlayerRepositoryPort { private final SpringDataR2dbcPlayerRepository repo; public MySqlPlayerRepositoryAdapter(SpringDataR2dbcPlayerRepository repo) { this.repo = repo; } @Override public Mono<Player> save(Player player) { return repo.findByExternalId(player.id().value()) .defaultIfEmpty(new PlayerRow( null, player.id().value(), player.name().value(), player.wins(), player.losses() )) .flatMap(existing -> { existing.setName(player.name().value()); existing.setWins(player.wins()); existing.setLosses(player.losses()); return repo.save(existing).map(this::toDomain); }); } @Override public Mono<Player> findById(PlayerId id) { return repo.findByExternalId(id.value()) .map(this::toDomain); } @Override public Mono<Player> findOrCreateByName(PlayerName name) { return repo.findByName(name.value()) .map(this::toDomain) .switchIfEmpty(Mono.defer(() -> save(Player.newPlayer(name)))); } @Override public Flux<Player> findRanking() { return repo.findRanking().map(this::toDomain); } private Player toDomain(PlayerRow row) { return new Player( new PlayerId(row.getExternalId()), new PlayerName(row.getName()), row.getWins(), row.getLosses() ); }}
The adapter handles the impedance mismatch between:
Domain model: Immutable Player with value objects
Database model: Mutable PlayerRow JPA entity with primitive types
Controllers act as driving adapters that receive input from the outside world and invoke use cases.Location: infrastructure/web/controller/GameController.java:43-47
public class CreateGameUseCase { private final MongoGameRepository repo; // Direct dependency on infrastructure! public CreateGameUseCase(MongoGameRepository repo) { this.repo = repo; }}
public class CreateNewGameUseCase { private final GameRepositoryPort gameRepo; // Depends on abstraction! public CreateNewGameUseCase(GameRepositoryPort gameRepo) { this.gameRepo = gameRepo; }}
The use case depends on the port interface (abstraction), not the concrete adapter implementation.
public class CreateNewGameUseCaseTest { @Test void shouldCreateGameForNewPlayer() { // Mock the port, not the adapter! GameRepositoryPort mockGameRepo = mock(GameRepositoryPort.class); PlayerRepositoryPort mockPlayerRepo = mock(PlayerRepositoryPort.class); CreateNewGameUseCase useCase = new CreateNewGameUseCase( mockPlayerRepo, mockGameRepo ); // Test business logic without MongoDB or MySQL when(mockPlayerRepo.findOrCreateByName(any())) .thenReturn(Mono.just(testPlayer)); when(mockGameRepo.save(any())) .thenReturn(Mono.just(testGame)); StepVerifier.create(useCase.execute(command)) .assertNext(result -> { assertNotNull(result.gameId()); assertEquals("IN_PROGRESS", result.status()); }) .verifyComplete(); }}
Adapters are wired together using Spring configuration:Location: infrastructure/config/UseCaseConfig.java
@Configurationpublic class UseCaseConfig { @Bean public CreateNewGameUseCase createNewGameUseCase( PlayerRepositoryPort playerRepo, GameRepositoryPort gameRepo) { return new CreateNewGameUseCase(playerRepo, gameRepo); } // Spring auto-detects @Component adapters: // - MongoGameRepositoryAdapter implements GameRepositoryPort // - MySqlPlayerRepositoryAdapter implements PlayerRepositoryPort}
Spring’s dependency injection wires the correct adapter implementations to the use cases at runtime, but the application core remains unaware of which adapters are being used.