Hexagonal Architecture
Hexagonal Architecture
Section titled βHexagonal ArchitectureβOverview
Section titled βOverviewβHexagonal Architecture, also known as Ports and Adapters, is a software design pattern that emphasizes the separation of concerns by isolating the core business logic from external systems. This architecture allows for easier testing, maintenance, and adaptability to changes in external dependencies.
Key Concepts
Section titled βKey Conceptsβ- Core Domain: The central part of the application that contains the business logic. It is independent of external systems and frameworks.
- Ports: Interfaces that define how the core domain interacts with external systems. They can be inbound (for receiving commands) or outbound (for sending data).
- Adapters: Implementations of the ports that connect the core domain to external systems, such as databases, message queues, or web services. Adapters can be inbound (e.g., REST controllers) or outbound (e.g., repositories).
- Dependency Inversion: The core domain should not depend on external systems. Instead, it should define interfaces (ports) that adapters implement. This allows for easy replacement of external systems without affecting the core logic.
- Testing: The separation of the core domain from external systems allows for easier unit testing of business logic without needing to mock external dependencies. Integration tests can be used to verify the interaction between the core domain and adapters.
- Flexibility: The architecture allows for easy changes to external systems without impacting the core domain. New adapters can be added or existing ones modified without affecting the business logic.
Clean Architecture in Hatchgrid
Section titled βClean Architecture in HatchgridβAt Hatchgrid, we follow a strict Clean Architecture approach to enforce separation of concerns and long-term maintainability. This structure promotes testability, framework independence, and high cohesion within each feature or bounded context.
Folder Structure
Section titled βFolder StructureβEach feature is self-contained and follows this standard structure:
π{feature}βββ πdomain // Core domain logic (pure Kotlin, no framework dependencies)βββ πapplication // Use cases (pure, framework-agnostic)βββ πinfrastructure // Framework integration (Spring Boot, R2DBC, HTTP, etc.)
Layer Breakdown
Section titled βLayer Breakdownβ1. domain
: The Core
Section titled β1. domain: The Coreβ- Pure Kotlin: No framework annotations or external dependencies.
- Domain Building Blocks:
- Value Objects: Immutable objects that represent a descriptive aspect of the domain with no conceptual identity. For example:
FormId
: A special value object that wraps a UUID and extendsBaseId<UUID>
.HexColor
: A validated string-based color value object extendingBaseValidateValueObject<String>
.
- Base Classes:
BaseId<T>
: Abstract class for typed IDs, ensuring type safety and consistent behavior.BaseValueObject<T>
andBaseValidateValueObject<T>
: For defining immutable value objects with or without validation logic.
- Entities and Aggregate Roots:
Form
: A typical aggregate root extendingBaseEntity<FormId>
, responsible for business rules and recording domain events such asFormCreatedEvent
andFormUpdatedEvent
.BaseEntity<ID>
: Provides audit tracking and domain event management.AggregateRoot<ID>
: Marker base class to distinguish aggregate roots from regular entities.
- Domain Contracts:
- Defined through interfaces like
FormRepository
,FormFinderRepository
, etc., which are implemented by adapters in the infrastructure layer.
- Defined through interfaces like
- Domain Events:
- Encapsulate business-relevant occurrences like
FormCreatedEvent
,FormUpdatedEvent
, etc., used to decouple the core from side effects.
- Encapsulate business-relevant occurrences like
- Domain Exceptions:
- Represent business-rule violations and are defined explicitly, e.g.,
FormException
.
- Represent business-rule violations and are defined explicitly, e.g.,
- Value Objects: Immutable objects that represent a descriptive aspect of the domain with no conceptual identity. For example:
- Entities, Value Objects, Events, Exceptions, and Interfaces that define contracts with the outside.
- Example files:
Form.kt
,FormId.kt
,FormCreatedEvent.kt
,FormRepository.kt
.
2. application
: Use Cases
Section titled β2. application: Use Casesβ- Defines commands, queries, and handlers (CQRS-style).
- Contains services that orchestrate interactions between domain logic and ports.
- Completely framework-independent. We even avoid
@Service
from Spring by defining our own annotation:
package com.hatchgrid.common.domain@Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.CLASS)@MustBeDocumentedannotation class Service
-
Example:
CreateFormCommandHandler.kt
coordinates domain logic viaFormCreator.kt
.SearchFormsQueryHandler.kt
reads throughFormFinder.kt
.
-
CQRS (Command Query Responsibility Segregation):
- The application layer implements CQRS by separating write and read concerns into
commands
andqueries
, each with their own handlers. - Commands represent state-changing operations. They are handled by classes implementing
CommandHandler<T>
:- Example:
CreateFormCommandHandler
receives aCreateFormCommand
, authorizes the action, builds a domain object (Form
), and delegates creation toFormCreator
. FormCreator
persists the new entity viaFormRepository
and broadcasts relevant domain events (FormCreatedEvent
) using anEventPublisher
.
- Example:
- Queries represent read-only operations. They are handled by classes implementing
QueryHandler<TQuery, TResult>
:- Example:
FindFormQueryHandler
receives aFindFormQuery
, fetches the entity via aFormFinder
, and maps the result to a DTO (FormResponse
).
- Example:
- All command and query objects are pure data structures implementing the shared
Command
orQuery<T>
interfaces, making them easy to validate, log, and trace. - Command and query handlers are marked with our custom
@Service
annotation to remain decoupled from Spring-specific logic.
- The application layer implements CQRS by separating write and read concerns into
3. infrastructure
: Adapters
Section titled β3. infrastructure: Adaptersβ-
Implements the interfaces defined in
domain
and integrates with:- HTTP layer:
- All controllers extend a shared base class
ApiController
that centralizes common logic:- Authentication via JWT (e.g., extracting user ID).
- Input sanitization.
- Dispatching commands or queries via a
Mediator
, which abstracts handler resolution.
- Controllers are versioned via custom media types in the
Accept
header (e.g.,application/vnd.api.v1+json
). - Fully documented with Swagger (OpenAPI v3) annotations for automatic API documentation.
- Example:
CreateFormController
,FindFormController
.
- All controllers extend a shared base class
- Persistence layer:
- Repositories and entities interact with the database using Spring Data R2DBC.
- Mapping between domain and persistence models is done through dedicated mappers (e.g.,
FormMapper.kt
). - Example:
FormR2dbcRepository
implements domain interfaces likeFormRepository
.
- HTTP layer:
-
This layer is the only one allowed to use framework-specific features (Spring annotations, I/O, etc.).
Example Feature: form
Section titled βExample Feature: formβform/βββ application/β βββ create/β β βββ CreateFormCommand.ktβ β βββ CreateFormCommandHandler.ktβ β βββ FormCreator.ktβ βββ ...βββ domain/β βββ Form.ktβ βββ FormId.ktβ βββ FormRepository.ktβ βββ event/FormCreatedEvent.ktβββ infrastructure/ βββ http/ β βββ CreateFormController.kt βββ persistence/ βββ entity/FormEntity.kt βββ repository/FormR2dbcRepository.kt
Principles Applied
Section titled βPrinciples Appliedβ- Single Responsibility per Layer: Core logic, use cases, and adapters are strictly separated.
- Framework Independence: Core logic remains decoupled from Spring Boot or any infrastructure.
- Port-Driven: Infrastructure implements domain interfaces, not the other way around.
- Isolation for Testing: Domain and application layers can be unit-tested with no framework setup.