Skip to content

EffectBoundary: Algebraic Effects Meet Spring Boot

Choose a tag to compare

@MagnusSmith MagnusSmith released this 18 Apr 17:47
a6ed81c

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/orders

Source: 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-skills

See 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-checker registered on test annotation processor classpaths — Fixes error: plug-in not found: HKJChecker during test compilation. The Gradle plugin now iterates SourceSetContainer so hkj-checker and hkj-processor-plugins are registered on every source-set annotation-processor classpath (test, integrationTest, etc.); the Maven plugin defensively appends HKJ entries to user-supplied testAnnotationProcessorPaths (#485)
  • hkj.web.either.default-error-status now binds and takes effect (#490) — The documented property had no binding target, so unmapped Left error types always fell back to 400. Fixed by adding the nested HkjProperties.Web.Either.defaultErrorStatus binding; the legacy flat path hkj.web.default-error-status is preserved as a backward-compatible alias (#491)
  • Test coverage upliftFocusProcessor, FoldProcessor, ForComprehensionProcessor, the optics processors, EffectAlgebra/ComposeEffects/Path processors, Spec interfaces, and KindFieldAnalyser gain significant coverage, plus a new @ExcludeFromJacocoGeneratedReport utility (#483)

Documentation

  • New Effect Boundary Integration guide — Six-level adoption ladder, bridge architecture, @EnableEffectBoundary/@Interpreter/@EffectTest usage, ObservableEffectBoundary metrics
  • 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 / CheckedSupplier guidance
  • Updated Spring Boot Integration with the @WebMvcTest slice-test recipe and @ResponseStatus handling
  • Cheatsheet gains rows for bimap, handleError, handleErrorWith, and guarantee

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.