Understanding the ports and adapters pattern implementation in the User Management API
The User Management API implements Hexagonal Architecture (also known as Ports and Adapters pattern), which ensures that business logic remains isolated from external concerns like databases, web frameworks, and third-party services.
Hexagonal architecture organizes code into three main areas:
Core (Domain) - Business logic and domain models
Ports - Interfaces defining how to interact with the core
Adapters - Concrete implementations of ports for specific technologies
The hexagon shape is symbolic - it represents that the core can have any number of ports (not just 6). The key is that the core is at the center, isolated from external concerns.
// src/main/java/com/fbaron/user/core/service/UserService.javapublic class UserService implements RegisterUserUseCase, GetUserUseCase { private final UserQueryRepository userQueryRepository; private final UserCommandRepository userCommandRepository; @Override public User register(User user) { // Business rule: username must be unique if (userQueryRepository.existsByUsername(user.getUsername())) { throw new UserAlreadyExistsException("username", user.getUsername()); } // Business rule: email must be unique if (userQueryRepository.existsByEmail(user.getEmail())) { throw new UserAlreadyExistsException("email", user.getEmail()); } user.activate(); return userCommandRepository.save(user); }}
Define what the application can do - exposed as use case interfaces:
// src/main/java/com/fbaron/user/core/usecase/RegisterUserUseCase.javapublic interface RegisterUserUseCase { /** * Creates and persists a new user with a generated unique ID. */ User register(User user);}
// src/main/java/com/fbaron/user/core/usecase/GetUserUseCase.javapublic interface GetUserUseCase { List<User> findAll(); User findById(UUID id);}
Ports are defined in the core module, ensuring the core controls its own interfaces. External adapters must conform to what the core needs, not vice versa.
// src/main/java/com/fbaron/user/web/rest/UserRestAdapter.java@RestController@RequestMapping("/api/v1/users")public class UserRestAdapter { private final RegisterUserUseCase registerUserUseCase; private final GetUserUseCase getUserUseCase; private final EditUserUseCase editUserUseCase; private final RemoveUserUseCase removeUserUseCase; private final UserDtoMapper userDtoMapper; @PostMapping public ResponseEntity<UserDto> registerUser(@Valid @RequestBody RegisterUserDto dto) { // 1. Map DTO to domain model var user = userDtoMapper.toModel(dto); // 2. Call use case var registeredUser = registerUserUseCase.register(user); // 3. Map domain model to DTO return ResponseEntity.status(HttpStatus.CREATED) .body(userDtoMapper.toDto(registeredUser)); }}
Want to add GraphQL support? Just create a new driving adapter:
@Controllerpublic class UserGraphQLAdapter { private final RegisterUserUseCase registerUserUseCase; private final GetUserUseCase getUserUseCase; @MutationMapping public User registerUser(@Argument RegisterUserInput input) { User user = // map input to domain model return registerUserUseCase.register(user); } @QueryMapping public List<User> users() { return getUserUseCase.findAll(); }}
No changes to core needed - just implement the same use case interfaces.