Quickstart
- How to add Higher-Kinded-J to a Gradle or Maven project
- Java 25 preview mode configuration (required)
- Your first Effect Paths in under 5 minutes
Prerequisites
Higher-Kinded-J is built on Java 25 today, and parts of it are compiled with preview features, so the build sets --enable-preview.
Preview ties the build to that one release, which makes this a version to match rather than a floor:
javacaccepts--enable-previewonly for the release it is running on, so a later JDK cannot target 25 with preview enabled (error: invalid source release 25 with --enable-preview).- The classes compiled that way carry a preview marker: the structured concurrency behind
VTask'sScopeandPar, and the virtual-thread stream's parallel operations. A JVM of any other version refuses to load them:UnsupportedClassVersionError: Preview features are not enabled for org/higherkindedj/hkt/vstream/VStreamPar (class file version 69.65535).
So build on Java 25 until the library itself moves to a later release.
Most of the library needs no flag at all: code using Either, Validated, the mapper, the plain VStream operations or sequential optics compiles and runs on Java 25 without --enable-preview. The flag is needed for code that reaches the structured-concurrency classes: VTask's Scope and Par, and the parallel stream operations in VStreamPar. Several everyday methods reach them, such as VTaskPath.zipWith, parallel ForPath steps and TraversalPath.traverseWith, as do the stream paths, throttles and bulkheads built on VStreamPar. Code needs the flag at compile time where it names one of these classes, and at run time wherever it reaches one.
The build plugins add it everywhere rather than asking you to work out which of your code touches those paths. If you configure the flags by hand and see the UnsupportedClassVersionError above at run time, that is the one you have missed.
Gradle Setup
With HKJ Gradle Plugin (Recommended)
// build.gradle.kts
plugins {
id("io.github.higher-kinded-j.hkj") version "LATEST_VERSION"
}
This single line configures dependencies, preview features, annotation processors, -parameters (constructor parameter names read by copy strategies), and compile-time Path type checking automatically. See the Gradle Plugin documentation for the full DSL reference.
For SNAPSHOT versions of the plugin, add the Sonatype snapshots repository to your settings.gradle.kts:
// settings.gradle.kts
pluginManagement {
repositories {
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
}
gradlePluginPortal()
mavenCentral()
}
}
You also need the snapshots repository in your project's repositories block so the plugin can resolve HKJ library dependencies:
// build.gradle.kts
repositories {
mavenCentral()
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
}
}
Prefer Not to Use the Plugin?
If your project cannot apply the HKJ plugin, see Manual Gradle and Maven Setup for the full build.gradle.kts configuration.
Maven Setup
With HKJ Maven Plugin (Recommended)
<build>
<plugins>
<plugin>
<groupId>io.github.higher-kinded-j</groupId>
<artifactId>hkj-maven-plugin</artifactId>
<version>LATEST_VERSION</version>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
The plugin automatically adds hkj-core, annotation processors, compile-time checks, and --enable-preview flags. See the Build Plugins documentation for the full configuration reference.
Prefer Not to Use the Plugin?
If your project cannot apply the HKJ Maven plugin, see Manual Gradle and Maven Setup for the full pom.xml configuration.
Simplify Imports with Module Import
Java 23+ supports module import declarations (JEP 511), which let you import all exported types from a module in a single line. Instead of importing individual packages:
import org.higherkindedj.hkt.effect.Path;
import org.higherkindedj.hkt.effect.EitherPath;
import org.higherkindedj.hkt.effect.MaybePath;
import org.higherkindedj.hkt.effect.VTaskPath;
import org.higherkindedj.hkt.validated.Validated;
import org.higherkindedj.optics.focus.FocusPath;
// ... and more
You can write:
import module org.higherkindedj.core;
This gives you access to Path, MaybePath, EitherPath, ValidationPath, VTaskPath, FocusPath, AffinePath, TraversalPath, and all other exported types from the core module.
Module imports require --enable-preview on Java 23–24. On Java 25+, the feature is standard and no flag is needed for module imports themselves (though HKJ still requires --enable-preview for other features).
Handle Absence
When a value might not exist, use MaybePath:
import org.higherkindedj.hkt.effect.Path;
var user = Path.maybe(repository.findById(id)); // Just(user) or Nothing
var name = user.map(User::name); // transforms only if present
var result = name.run().orElse("Anonymous"); // extract to standard Java
Handle Errors
When an operation can fail with a typed error, use EitherPath:
import org.higherkindedj.hkt.effect.Path;
var user = Path.maybe(repository.findById(userId))
.toEitherPath(new AppError.NotFound(userId)); // Nothing becomes Left(error)
var order = user
.via(u -> Path.either(orderService.create(u))) // chain another operation
.map(Order::confirm); // transform the success value
var result = order.run(); // Either<AppError, Order>
Validate Input
When you need all errors, not just the first, use ValidationPath:
import org.higherkindedj.hkt.effect.Path;
import org.higherkindedj.hkt.Semigroup;
import org.higherkindedj.hkt.Semigroups;
Semigroup<List<String>> sg = Semigroups.list();
var name = validateName(input.name()); // ValidationPath<List<String>, String>
var email = validateEmail(input.email()); // ValidationPath<List<String>, String>
var age = validateAge(input.age()); // ValidationPath<List<String>, Integer>
var user = name.zipWith3Accum(email, age, User::new); // accumulates ALL errors
var result = user.run(); // Validated<List<String>, User>
Chain It Together
Combine absence, errors, and transformation in a single pipeline:
import org.higherkindedj.hkt.effect.Path;
public EitherPath<AppError, Receipt> processPayment(String userId, BigDecimal amount) {
return Path.maybe(userRepository.findById(userId))
.<AppError>toEitherPath(new AppError.UserNotFound(userId))
.via(user -> Path.either(validateAmount(user, amount)))
.via(validated -> Path.tryOf(() -> gateway.charge(validated))
.toEitherPath(AppError.PaymentFailed::new))
.map(charge -> new Receipt(charge.id(), amount));
}
toEitherPath infers its error type from the argument it is given. At the end of a chain the assignment target supplies it, but in the middle of one there is nothing downstream to infer from, so a new AppError.UserNotFound(...) would pin the whole railway to UserNotFound and the method's declared EitherPath<AppError, Receipt> would not match. Naming the type once at the point the error channel opens widens it for every step after.
The deferred overload needs the witness for the same reason and in the same place: .<AppError>toEitherPath(() -> new AppError.UserNotFound(userId)). Supplier<? extends E> looks as though its wildcard would widen the error on its own, but E is settled where the conversion is written, before anything downstream is read. See Effect compiler errors for the rest of this family.
Getting Back to Standard Java
Every Path type unwraps to a standard Java value. You are never locked in:
Maybe<User> maybe = maybePath.run(); // → Maybe
Either<AppError, User> either = eitherPath.run(); // → Either
Optional<User> opt = maybePath.run().toOptional(); // → java.util.Optional
User user = eitherPath.getOrElse(User.anonymous()); // → raw value
String msg = eitherPath.run().fold(
error -> "Failed: " + error, // handle error
value -> "Success: " + value // handle success
);
- Effect Path Overview - The railway model explained in depth
- Path Types - Choosing the right path for your problem
- Cheat Sheet - One-page operator reference
- Focus-Effect Integration - Combining effects with optics
Next: Cheat Sheet