HexaFun
A functional, declarative Java framework for building Hexagonal Architecture apps.
Built for JDK 17+ with a clean API that puts your use cases front and center. Forget the ceremony. Focus on composing behavior.
What is HexaFun?
HexaFun brings Hexagonal Architecture into the functional age with minimal boilerplate and maximum clarity.
- Define use cases with clear port interfaces
- Pure business logic, no frameworks leaking in
- Plug in real adapters (HTTP, DB, Messaging) when you want
- Composable, testable, functional-by-design
Architecture Overview
┌────────────────────────┐
│ Driving Adapter │ (REST / CLI, Events)
└────────▲───────────────┘
│
Input Port (UseCase<I, O>)
│
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ Use Cases │ ◄──► │ Domain Model │ │
│ └──────────────┘ └───────────────────┘ │
│ │
└────────────────────▼──────────────▼─────────────────────────┘
│ │
Output Port Output Port
(e.g. DB) (e.g. Email)
│ │
┌───▼────┐ ┌───▼──────┐
│ Adapter│ │ Adapter │
└────────┘ └──────────┘
Hexagonal Architecture (also known as Ports and Adapters) organizes application logic around a core domain, isolated from external concerns.
Fluent DSL Example
The Fluent DSL provides a concise, declarative way to compose use cases with type safety:
// Define type-safe keys
public interface CounterUseCases {
UseCaseKey<IncrementInput, Result<Counter>> INCREMENT =
UseCaseKey.of("increment");
UseCaseKey<AddInput, Result<Counter>> ADD =
UseCaseKey.of("add");
}
// Build the app
HexaApp app = HexaFun.dsl()
.useCase(INCREMENT)
.validate(CounterValidators::validateIncrement)
.handle(input -> Result.ok(input.counter().increment()))
.useCase(ADD)
.validate(CounterValidators::validateCounter)
.validate(CounterValidators::validateAmount) // Chained validators
.handle(input -> Result.ok(input.counter().add(input.amount())))
.build();
// Invoke with type safety
Result<Counter> result = app.invoke(INCREMENT, new IncrementInput(counter));
Key features:
- Type-safe keys:
UseCaseKey<I, O>for compile-time checking - Clear naming:
validate/handleinstead offrom/to - Implicit closure: No
.and()chaining needed - Validator chaining: Multiple
.validate()calls - Port registry: Type-safe dependency injection with
.withPort()
See the Fluent DSL Guide for complete documentation, or follow the Tutorial to build a complete task manager from scratch.
Core Concepts
| Concept | How HexaFun Supports It |
|---|---|
| Use Cases | UseCase<I, O> interface |
| Type-Safe Keys | UseCaseKey<I, O> for safe dispatch |
| Input Validation | ValidationPort<I> interface |
| Validator Chaining | Multiple .validate() calls |
| Port Registry | port(Class<T>, impl) for DI |
| Functional Pipelines | Chain .validate(...).handle(...) |
| Clean Architecture | Strict separation of concerns |
| Error Handling | Result<T> monadic error handling |
| Testing | Declarative testing DSL |
Testing
Easily test your use cases with a fluent API:
// Test successful execution
app.test(INCREMENT)
.with(new IncrementInput(Counter.zero()))
.expectOk(counter -> assertEquals(1, counter.value()));
// Test validation failure
app.test(ADD)
.with(new AddInput(null, 10))
.expectFailure(error -> assertEquals("Counter cannot be null", error));
Packages
com.guinetik.hexafun.hexa- Hexagonal ports (UseCase,UseCaseKey,ValidationPort)com.guinetik.hexafun.fun- Functional primitives (Result)com.guinetik.hexafun- Core application container (HexaApp,HexaFun)com.guinetik.hexafun.testing- Testing frameworkcom.guinetik.hexafun.examples- Example applications