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 requires Java 25 or later with preview features enabled.
The library uses Java preview features including stable values and flexible constructor bodies. Without --enable-preview, your project will not compile.
Gradle Setup
// build.gradle.kts
plugins { java }
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
dependencies {
implementation("io.github.higher-kinded-j:hkj-core:LATEST_VERSION")
// Optional: generates Focus paths and Effect paths for your records
annotationProcessor("io.github.higher-kinded-j:hkj-processor-plugins:LATEST_VERSION")
}
// Required: enable Java preview features
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("--enable-preview")
}
tasks.withType<Test>().configureEach {
jvmArgs("--enable-preview")
}
tasks.withType<JavaExec>().configureEach {
jvmArgs("--enable-preview")
}
For SNAPSHOTS, add the Sonatype snapshots repository:
repositories {
mavenCentral()
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
}
}
Maven Setup
<properties>
<maven.compiler.release>25</maven.compiler.release>
<maven.compiler.enablePreview>true</maven.compiler.enablePreview>
</properties>
<dependencies>
<dependency>
<groupId>io.github.higher-kinded-j</groupId>
<artifactId>hkj-core</artifactId>
<version>LATEST_VERSION</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Optional: generates Focus paths and Effect paths for your records -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.higher-kinded-j</groupId>
<artifactId>hkj-processor-plugins</artifactId>
<version>LATEST_VERSION</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Required: enable preview features for tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
<!-- Required: enable preview features for application execution -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<executable>java</executable>
<arguments>
<argument>--enable-preview</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
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;
Semigroup<List<String>> sg = Semigroup.listSemigroup();
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))
.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));
}
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.run().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