Quickstart

What You'll Learn

  • 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 {
    id("io.github.higher-kinded-j.hkj") version "LATEST_VERSION"
}

This single line configures dependencies, preview features, annotation processors, 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

<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.

Note

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))
        .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
);

See Also


Next: Cheat Sheet