Hexagonal Architecture, coined by Alistair Cockburn, is a design pattern that places the business logic at the center (the “hexagon”) and isolates it from external concerns through well-defined ports and adapters.
Also known as Ports and Adapters architecture, this pattern ensures that the core business logic is independent of frameworks, databases, UIs, and external APIs.
The core layer in src/main/java/com/acamus/telegrm/core/ contains:
Domain Model (core/domain/model/)
// User.java - Pure domain entitypublic class User { private String id; private Email email; private Password password; private String name; private boolean enabled; private LocalDateTime createdAt; public static User create(String name, Email email, Password password) { User user = new User(); user.id = UUID.randomUUID().toString(); user.name = name; user.email = Objects.requireNonNull(email); user.password = Objects.requireNonNull(password); user.enabled = true; user.createdAt = LocalDateTime.now(); return user; } // Factory method for reconstruction from persistence public static User reconstruct(String id, String name, Email email, Password password, boolean enabled, LocalDateTime createdAt, LocalDateTime lastLoginAt) { // ... }}
Notice: No @Entity, no @Table, no Spring annotations. Pure Java.
Value Objects (core/domain/valueobjects/)
// Email.java - Guarantees validitypublic record Email(String value) { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"); public Email { Objects.requireNonNull(value, "Email cannot be null"); if (value.isBlank()) { throw new IllegalArgumentException("Email cannot be blank"); } if (!EMAIL_PATTERN.matcher(value).matches()) { throw new IllegalArgumentException("Invalid email format: " + value); } value = value.trim(); }}
Value objects enforce business rules at construction time.
Use cases can be tested with mocks, no Spring context needed
@Testvoid shouldProcessTelegramUpdate() { var mockRepo = mock(ConversationRepositoryPort.class); var mockAI = mock(AiGeneratorPort.class); var useCase = new ProcessTelegramUpdateUseCase(...); useCase.processUpdate(command); verify(mockRepo).save(any());}
Spring assembles the hexagon through dependency injection:
// UseCaseConfig.java (Infrastructure Layer)@Configurationpublic class UseCaseConfig { @Bean public ProcessTelegramUpdatePort processTelegramUpdateUseCase( ConversationRepositoryPort conversationRepository, MessageRepositoryPort messageRepository, TelegramPort telegramPort, AiGeneratorPort aiGeneratorPort) { return new ProcessTelegramUpdateUseCase( conversationRepository, messageRepository, telegramPort, aiGeneratorPort ); }}
The infrastructure layer knows about everything. It creates concrete instances and wires them together. The core and application layers know only abstractions.