_ _ _ _ _ ___ _ _ ___
| | | (_) | | | | / (_) | | | | |_ |
| |_| |_ __ _| |__ ___ _ __ ______| |/ / _ _ __ __| | ___ __| |______ | |
| _ | |/ _` | '_ \ / _ \ '__|______| \| | '_ \ / _` |/ _ \/ _` |______| | |
| | | | | (_| | | | | __/ | | |\ \ | | | | (_| | __/ (_| | /\__/ /
\_| |_/_|\__, |_| |_|\___|_| \_| \_/_|_| |_|\__,_|\___|\__,_| \____/
__/ |
|___/
Higher-Kinded-J brings two capabilities that Java has long needed: composable error handling through the Effect Path API, and type-safe immutable data navigation through the Focus DSL. Each is powerful alone. Together, they form a unified approach to building robust applications, where effects and structure compose seamlessly.
No more pyramids of nested checks. No more scattered validation logic. Just clean, flat pipelines that read like the business logic they represent.
Every Java application battles the same chaos: nulls here, exceptions there, Optional when someone remembered. Each approach speaks a different dialect. None compose cleanly.
// Traditional Java: pyramid of nested checks
User user = userRepository.findById(userId);
if (user == null) {
return OrderResult.error("User not found");
}
try {
ValidationResult validation = validator.validate(request);
if (!validation.isValid()) {
return OrderResult.error(validation.getErrors().get(0));
}
// ... more nesting, more checks
} catch (ValidationException e) {
return OrderResult.error("Validation error: " + e.getMessage());
}Higher-Kinded-J's Effect Path API models computation as a railway: success travels one track, failure travels another. Operations like map, via, and recover work identically across all effect types.
// Effect Path API: flat, composable, readable
public EitherPath<OrderError, Order> processOrder(String userId, OrderRequest request) {
return Path.maybe(userRepository.findById(userId))
.toEitherPath(() -> new OrderError.UserNotFound(userId))
.via(user -> Path.either(validator.validate(request))
.mapError(OrderError.ValidationFailed::new))
.via(validated -> Path.tryOf(() -> paymentService.charge(user, amount))
.toEitherPath(OrderError.PaymentFailed::new))
.map(payment -> createOrder(user, request, payment));
}The nesting is gone. Failures propagate automatically. The business logic reads top-to-bottom.
What makes Higher-Kinded-J unique is the seamless integration between Effect Paths and the Focus DSL. Where Effect Paths navigate computational effects, Focus Paths navigate data structures. Both use the same vocabulary. Both compose with via. And when you need to cross between them, the bridge API connects both worlds.
THE EFFECT-OPTICS BRIDGE
EFFECTS DOMAIN OPTICS DOMAIN
ββββββββββββββ βββββββββββββ
EitherPath<E, User> βββββ βββββ FocusPath<User, Address>
TryPath<Config> βββββ€ βββββ AffinePath<User, Email>
IOPath<Data> βββββ€ βββββ TraversalPath<Team, Player>
VTaskPath<A> βββββ€ βββββ
ValidationPath<E, A> βββββ
β β
βΌ βΌ
βββββββββββββββββββ
β .focus(path) β
β .toEitherPath β
β .toMaybePath β
βββββββββββββββββββ
β
βΌ
UNIFIED COMPOSITION
ββββββββββββββββββββ
userService.findById(id) // Effect: fetch
.focus(UserFocus.address()) // Optics: navigate
.via(validateAddress) // Effect: validate
.focus(AddressFocus.city()) // Optics: extract
.map(String::toUpperCase) // Transform
// Fetch user (effect) β navigate to address (optics) β validate (effect)
EitherPath<Error, String> result =
userService.findById(userId) // EitherPath<Error, User>
.focus(UserFocus.address()) // EitherPath<Error, Address>
.focus(AddressFocus.postcode()) // EitherPath<Error, String>
.via(code -> validatePostcode(code));Effects and structure, composition and navigation, all speaking the same language.
Java lacks native support for abstracting over type constructors. Higher-Kinded-J simulates HKTs using defunctionalisation, enabling:
- Generic functions that work across
Optional,List,CompletableFuture, and custom types - Type classes like
Functor,Applicative, andMonad - Monad transformers for composing effect stacks
Higher-Kinded-J provides the most comprehensive optics implementation available for Java. Working with immutable records means verbose "copy-and-update" logic; the Optics library treats data access as first-class values:
- Complete optic hierarchy: Lenses, Prisms, Isos, Affines, Traversals, Folds, and Setters
- Automatic generation via annotation processor for Java records and sealed interfaces
- External type import via
@ImportOpticsfor types you don't own (e.g.,java.time.LocalDate) - Filtered traversals for predicate-based focusing within collections
- Indexed optics for position-aware transformations
- Focus DSL for type-safe, fluent path navigation
- Effect integration bridging optics with the Effect Path API
Higher-Kinded-J offers the most advanced optics implementation in the Java ecosystem, combined with a unified effect system that no other library provides.
| Feature | Higher-Kinded-J | Functional Java | Fugue Optics | Derive4J |
|---|---|---|---|---|
| Lens | β | β | β | β* |
| Prism | β | β | β | β* |
| Iso | β | β | β | β |
| Affine/Optional | β | β | β | β* |
| Traversal | β | β | β | β |
| Filtered Traversals | β | β | β | β |
| Indexed Optics | β | β | β | β |
| Code Generation | β | β | β | β* |
| External Type Import | β | β | β | β |
| Java Records Support | β | β | β | β |
| Sealed Interface Support | β | β | β | β |
| Effect Integration | β | β | β | β |
| Focus DSL | β | β | β | β |
| Profunctor Architecture | β | β | β | β |
| Fluent API | β | β | β | β |
| Modern Java (21+) | β | β | β | β |
| Virtual Threads | β | β | β | β |
* Derive4J generates getters/setters but requires Functional Java for actual optic classes
| Path Type | When to Use |
|---|---|
MaybePath<A> |
Absence is normal, not an error |
EitherPath<E, A> |
Errors carry typed, structured information |
TryPath<A> |
Wrapping code that throws exceptions |
ValidationPath<E, A> |
Collecting all errors, not just the first |
IOPath<A> |
Side effects you want to defer and sequence |
TrampolinePath<A> |
Stack-safe recursion |
CompletableFuturePath<A> |
Async operations |
ReaderPath<R, A> |
Dependency injection, configuration access |
WriterPath<W, A> |
Logging, audit trails, collecting metrics |
WithStatePath<S, A> |
Stateful computations (parsers, counters) |
ListPath<A> |
Batch processing with positional zipping |
StreamPath<A> |
Lazy sequences, large data processing |
NonDetPath<A> |
Non-deterministic search, combinations |
LazyPath<A> |
Deferred evaluation, memoisation |
IdPath<A> |
Pure computations (testing, generic code) |
OptionalPath<A> |
Bridge for Java's standard Optional |
FreePath<F, A> / FreeApPath<F, A> |
DSL building and interpretation |
VTaskPath<A> |
Virtual thread-based concurrency with Par combinators |
Each Path provides map, via, run, recover, and integration with the Focus DSL.
See Effect Path and Focus DSL applied in a realistic e-commerce scenario:
- Composing multi-step workflows with
EitherPathandvia()chains - Modelling domain errors with sealed interfaces for exhaustive handling
- Using
ForPathcomprehensions for readable sequential composition - Implementing resilience patterns: retry policies, timeouts, and recovery
- Integrating Focus DSL for immutable state updates
@GenerateLenses
public record Player(String name, int score) {}
@GenerateLenses
@GenerateTraversals
public record Team(String name, List<Player> players) {}
@GenerateLenses
@GenerateTraversals
public record League(String name, List<Team> teams) {}
// Compose traversals for deep navigation
Traversal<League, Integer> leagueToAllPlayerScores =
LeagueTraversals.teams()
.andThen(TeamTraversals.players())
.andThen(PlayerLenses.score());
// Filter and modify with predicates
Traversal<League, Player> activePlayers =
leagueToAllPlayers.filtered(Player::isActive);- Java Development Kit (JDK): Version 25 or later.
- Gradle (the project includes a Gradle wrapper).
| Higher-Kinded-J | Spring Boot | Jackson | Java |
|---|---|---|---|
| 0.3.x | 4.0.1+ | 3.x (tools.jackson) | 25+ |
The hkj-spring-boot-starter requires Spring Boot 4.0.1 or later with Jackson 3.x (using the tools.jackson package namespace).
Add the following dependencies to your build.gradle.kts:
dependencies {
implementation("io.github.higher-kinded-j:hkj-core:LATEST_VERSION")
annotationProcessor("io.github.higher-kinded-j:hkj-processor-plugins:LATEST_VERSION")
}The annotation processor generates Focus paths and Effect paths for your records, enabling seamless integration between effects and data navigation.
For SNAPSHOTS:
repositories {
mavenCentral()
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
}
}The hkj-spring-boot-starter brings functional patterns into REST APIs with zero configuration:
dependencies {
implementation("io.github.higher-kinded-j:hkj-spring-boot-starter:LATEST_VERSION")
}@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public Either<DomainError, User> getUser(@PathVariable String id) {
return userService.findById(id);
// Right(user) β HTTP 200 with JSON
// Left(UserNotFoundError) β HTTP 404 with error details
}
@PostMapping
public Validated<List<ValidationError>, User> createUser(@RequestBody UserRequest request) {
return userService.validateAndCreate(request);
// Valid(user) β HTTP 200
// Invalid(errors) β HTTP 400 with ALL validation errors
}
}Auto-configuration handles Either to HTTP response conversion, error accumulation with Validated, and async operations with CompletableFuturePath.
For complete documentation, see:
Nine interactive tutorial journeys with hands-on exercises:
| Journey | Focus | Exercises |
|---|---|---|
| Core: Foundations | HKT simulation, Functor, Monad | 24 |
| Effect API | Effect paths, ForPath, Contexts | 15 |
| Concurrency: VTask | Virtual threads, VTaskPath, Par | 16 |
| Optics: Focus DSL | Type-safe path navigation | 18 |
graph TD;
root["higher-kinded-j (root)"] --> hkj_api["hkj-api"];
root --> hkj_annotations["hkj-annotations"];
root --> hkj_core["hkj-core"];
root --> hkj_processor["hkj-processor"];
hkj_processor --> hkj_processor_plugins["hkj-processor-plugins"];
root --> hkj_examples["hkj-examples"];
root --> hkj_book["hkj-book"];
- hkj-api: Public API for HKTs and Optics
- hkj-annotations: Annotations for code generation (
@GenerateLenses, etc.) - hkj-core: Core implementation of HKT simulation, Effect Path API, and Optics
- hkj-processor: Annotation processor for generating boilerplate
- hkj-processor-plugins: Extensible plugins for code generation
- hkj-examples: Examples demonstrating all features
- hkj-book: Documentation built with mdbook
While powerful, the HKT simulation has inherent trade-offs:
- Boilerplate: Requires setup code for each simulated type
- Verbosity: Usage involves explicit wrapping/unwrapping
- Type Inference: Java's inference sometimes needs help with complex generics
The Effect Path API significantly reduces this friction by providing a consistent, fluent interface.
Higher-Kinded-J evolved from a simulation originally created for the blog post Higher Kinded Types with Java and Scala. Since then it has grown into a comprehensive functional programming toolkit, with the Effect Path API providing the unifying layer that connects HKTs, type classes, and optics into a coherent whole.
Contributions are very welcome! See CONTRIBUTING.md for details.
Areas for Contribution:
- Enhance the Effect Path API with new patterns and Path types
- Improve annotation processor capabilities
- Add HKT simulations for more Java types
- Extend optics with new combinators
- Create more examples and tutorials
- Improve documentation
How to Contribute:
- Fork the repository
- Create a feature branch
- Implement and test your changes
- Submit a Pull Request
If you're unsure where to start, feel free to open a GitHub Issue first.