EffectBoundary: Algebraic Effects Meet Spring Boot
Overview
This release introduces EffectBoundary, a gradual-adoption bridge that brings algebraic effect handlers into Spring Boot applications without rewriting existing code. A Service builds a pure Free<F, A> program; a Controller passes it to boundary.runIO() and returns an IOPath; the existing IOPathReturnValueHandler converts the result to an HTTP response. Effect interpreters become Spring-managed beans via @Interpreter, discovered and combined automatically by @EnableEffectBoundary. Testing uses TestBoundary over the Id monad, producing pure millisecond-speed tests with no mocks and no Spring context required.
The release also ships a complete hkj-spring/effect-example showcase, a suite of six Claude Code skills for in-editor HKJ guidance, @ResponseStatus support across all nine Effect Path return-value handlers, a canonical @WebMvcTest slice-test recipe, Effectful-interface widening for cross-path error recovery, EitherPath.bimap, Try.attempt(CheckedSupplier), and targeted bug fixes.
Key Features
EffectBoundary: Gradual Adoption of Free-Monad Effects
A service builds a pure Free<F, A> program that describes what to do. A controller passes that program to EffectBoundary, which interprets it at a clean boundary and returns an existing Path type. No new infrastructure is introduced — IOPath flows through the handler you already have.
// 1. Describe business logic as a pure Free program
@Service
public class OrderService {
public Free<PaymentEffects.Witness, OrderResult> placeOrder(OrderRequest req) {
return InventoryOps.<PaymentEffects.Witness>check(req.itemId(), req.quantity())
.flatMap(ok -> ok
? OrderOps.<PaymentEffects.Witness>place(req).flatMap(id ->
NotifyOps.confirm(id).map(v -> new OrderResult(id, CONFIRMED)))
: Free.pure(new OrderResult(null, REJECTED)));
}
}
// 2. Interpret at the boundary ? IOPath flows through the existing handler
@RestController
public class OrderController {
private final EffectBoundary<PaymentEffects.Witness> boundary;
private final OrderService service;
@PostMapping("/orders")
public IOPath<OrderResult> placeOrder(@RequestBody OrderRequest req) {
return boundary.runIO(service.placeOrder(req));
}
}Key pieces:
| Component | Role |
|---|---|
EffectBoundary<F> |
Interprets Free<F, A> programs via a production (IO) or custom interpreter, returning IOPath<A> |
TestBoundary<F> |
Id-monad variant for pure, deterministic, millisecond-speed tests with no mocks |
@EnableEffectBoundary |
Auto-discovers @Interpreter-annotated beans and wires a combined EffectBoundary |
@Interpreter |
Spring stereotype marking an interpreter bean for auto-discovery |
@EffectTest |
Test slice annotation for minimal contexts with boundary support |
FreePathReturnValueHandler |
Ninth Effect Path return-value handler, for controllers returning FreePath directly |
ObservableEffectBoundary |
Wraps EffectBoundary with Micrometer success/error/duration metrics via /actuator/hkj |
See the EffectBoundary integration guide for the full six-level adoption ladder.
Effect Boundary Showcase: hkj-spring/effect-example
A complete, runnable Spring Boot order-processing example demonstrating the pattern end-to-end. It composes three effect algebras — OrderOp, InventoryOp, NotifyOp — each annotated with @EffectAlgebra for code generation. Interpreters are Spring beans discovered via @Interpreter. The service builds Free<F, A> programs; the controller invokes boundary.runIO(); the existing IOPathReturnValueHandler produces the HTTP response. Pure OrderServicePureTest runs via TestBoundary over the Id monad in milliseconds with no Spring context and no mocks. EffectExampleIntegrationTest exercises the full MockMvc stack. ObservableEffectBoundary exposes metrics via actuator.
Run it locally:
./gradlew :hkj-spring:effect-example:bootRun
# POST to http://localhost:8081/api/ordersSource: hkj-spring/effect-example.
Claude Code Skills Suite
Six Claude Code skills that provide contextual, in-editor guidance for developers using HKJ. Skills auto-trigger on keyword matching and can also be invoked directly:
| Skill | Coverage |
|---|---|
/hkj-guide |
Navigator: Path-type selection, project setup, migration recipes, compiler error fixes |
/hkj-optics |
Focus DSL, @GenerateLenses, @GenerateFocus, @GenerateTraversals, @ImportOptics, lens/prism/traversal composition |
/hkj-effects |
@EffectAlgebra, Free programs, interpreters, EffectBoundary/TestBoundary, mock-free testing via Id |
/hkj-bridge |
Effects + optics integration via .focus() and toXxxPath() bridge methods |
/hkj-spring |
Spring Boot starter, Either/Validated controller returns, @EnableEffectBoundary, @Interpreter, @EffectTest |
/hkj-arch |
Functional core / imperative shell on Java 25, boundary design, domain modelling |
Each skill ships a SKILL.md under 500 lines plus on-demand reference files (~5,300 lines total). Install via the build plugin:
// Gradle
hkj { skills = true }
// or: ./gradlew hkjInstallSkills# Maven
mvn hkj:install-skillsSee Claude Code Skills for details.
@ResponseStatus Support Across All Effect Path Handlers
All nine Effect Path return-value handlers now honour @ResponseStatus on the handler method via the new SuccessStatusResolver, with controller-class fallback and meta-annotation support. POSTs can return canonical 201, DELETEs can return 204 with body suppressed, etc.:
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED)
public EitherPath<DomainError, Order> create(@RequestBody OrderRequest req) { ... }
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public MaybePath<Void> delete(@PathVariable String id) { ... }Covers EitherPath, MaybePath, TryPath, ValidationPath, IOPath, CompletableFuturePath, VTaskPath, FreePath, and VStreamPath. Error paths continue to use ErrorStatusCodeMapper.
@WebMvcTest Slice-Test Recipe
A canonical slice-test pattern for hkj-spring controllers, covering the auto-configuration imports and @MockitoBean setup that slice tests need:
@WebMvcTest(UserController.class)
@ImportAutoConfiguration({
HkjAutoConfiguration.class,
HkjJacksonAutoConfiguration.class,
HkjWebMvcAutoConfiguration.class
})
class UserControllerWebMvcSliceTest {
@Autowired MockMvc mvc;
@MockitoBean UserService service;
@Test void returns200_whenRight() throws Exception { ... }
@Test void returns404_whenTaggedError() throws Exception { ... }
}See Spring Boot Integration for the full recipe.
Effectful Capability Widening
handleError, handleErrorWith, and guarantee now live on the sealed Effectful capability interface. Polymorphic code against Effectful<A> can recover errors and run finalizers without narrowing to the concrete type. handleErrorWith accepts Function<? super Throwable, ? extends Effectful<A>>, so IOPath can recover into a VTaskPath (or vice versa) while the receiver's concrete type is preserved on return.
See Effectful for the updated interface.
EitherPath.bimap
Transform error and success values in a single call — equivalent to .mapError(errorFn).map(successFn) with laziness on the unused branch:
EitherPath<AppError, User> result = pathFromDb
.bimap(DbError::toAppError, UserDto::from);Try.attempt with CheckedSupplier
New Try.attempt(CheckedSupplier) entry point for Java APIs that throw checked exceptions (Files.readString, Class.forName, JDBC, reflection). CheckedSupplier<T, X extends Exception> in hkj-api declares throws X on get(), so lambda bodies throw checked exceptions directly — avoiding the target-type ambiguity of Try.of(Supplier):
Try<String> contents = Try.attempt(() -> Files.readString(Path.of("data.txt")));
Try<Class<?>> loaded = Try.attempt(() -> Class.forName("com.example.Missing"));Bug Fixes and Improvements
hkj-checkerregistered on test annotation processor classpaths — Fixeserror: plug-in not found: HKJCheckerduring test compilation. The Gradle plugin now iteratesSourceSetContainersohkj-checkerandhkj-processor-pluginsare registered on every source-set annotation-processor classpath (test, integrationTest, etc.); the Maven plugin defensively appends HKJ entries to user-suppliedtestAnnotationProcessorPaths(#485)hkj.web.either.default-error-statusnow binds and takes effect (#490) — The documented property had no binding target, so unmappedLefterror types always fell back to400. Fixed by adding the nestedHkjProperties.Web.Either.defaultErrorStatusbinding; the legacy flat pathhkj.web.default-error-statusis preserved as a backward-compatible alias (#491)- Test coverage uplift —
FocusProcessor,FoldProcessor,ForComprehensionProcessor, the optics processors,EffectAlgebra/ComposeEffects/Path processors, Spec interfaces, andKindFieldAnalysergain significant coverage, plus a new@ExcludeFromJacocoGeneratedReportutility (#483)
Documentation
- New Effect Boundary Integration guide — Six-level adoption ladder, bridge architecture,
@EnableEffectBoundary/@Interpreter/@EffectTestusage,ObservableEffectBoundarymetrics - New Claude Code Skills page
- Updated Effectful capability documentation with cross-implementation recovery semantics
- Updated EitherPath with the Bifunctor Operations section
- Updated Try monad with
Try.attempt/CheckedSupplierguidance - Updated Spring Boot Integration with the
@WebMvcTestslice-test recipe and@ResponseStatushandling - Cheatsheet gains rows for
bimap,handleError,handleErrorWith, andguarantee
Compatibility
No breaking changes. All new features are additive. hkj.web.either.default-error-status is the canonical property key going forward; hkj.web.default-error-status remains supported as a backward-compatible alias.