Releases: CajunSystems/roux
v0.3.0
Roux 0.3.0 — Type-Safe Capability Environments & Virtual-Clock Testing
Roux 0.3.0 introduces compile-time capability tracking via phantom types, a ZIO-style layer system for wiring environments, and a virtual-clock TestRuntime for instant, deterministic sleep tests. The Effect API is unchanged for the common case — these features layer on top as opt-in additions.
✨ Highlights
✅ Compile-time capability tracking with EffectWithEnv
EffectWithEnv<R,E,A> wraps any Effect<E,A> and tracks, at compile time, which capabilities it needs via the phantom type R. The compiler rejects run() calls that don't supply the right environment — no missing-handler surprises at runtime.
// Define a capability
sealed interface StoreOps extends Capability<String> {
record Get(String key) implements StoreOps {}
record Put(String key, String value) implements StoreOps {}
}
// Build a typed environment
Map<String, String> store = new HashMap<>();
HandlerEnv<StoreOps> env = HandlerEnv.of(StoreOps.class, cap -> switch (cap) {
case StoreOps.Get g -> store.getOrDefault(g.key(), "missing");
case StoreOps.Put p -> { store.put(p.key(), p.value()); yield "ok"; }
});
// Wrap an effect — R is inferred as StoreOps
EffectWithEnv<StoreOps, RuntimeException, String> typedEffect =
EffectWithEnv.of(Effect.from(new StoreOps.Get("greeting")));
// Run — compiler verifies env covers StoreOps
String result = typedEffect.run(env, DefaultEffectRuntime.create());✅ Layer system for capability wiring
Layer<RIn,E,ROut> is a functional interface that builds a HandlerEnv<ROut> from a HandlerEnv<RIn>. Layers compose horizontally (and) and vertically (andProvide), producing a single layer that satisfies multiple requirements.
// Leaf layer — no input requirements
Layer<Empty, RuntimeException, StoreOps> storeLayer =
Layer.succeed(StoreOps.class, cap -> switch (cap) { ... });
Layer<Empty, RuntimeException, LogOps> logLayer =
Layer.succeed(LogOps.class, cap -> switch (cap) { ... });
// Horizontal composition — satisfies both StoreOps and LogOps
Layer<Empty, Throwable, With<StoreOps, LogOps>> combined = storeLayer.and(logLayer);
// Build the environment and run
HandlerEnv<With<StoreOps, LogOps>> env =
combined.build(HandlerEnv.empty()).unsafeRun(runtime);✅ Phantom types: Empty and With<A,B>
Empty marks effects that need no capabilities. With<A,B> represents a capability environment union — both are compile-time-only interfaces that are never instantiated.
// Effect that needs nothing
EffectWithEnv<Empty, RuntimeException, Integer> pure =
EffectWithEnv.pure(Effect.succeed(42));
pure.run(HandlerEnv.empty(), runtime); // compiles
// Effect that needs both StoreOps and LogOps
EffectWithEnv<With<StoreOps, LogOps>, IOException, Unit> combined = ...;
combined.run(storeEnv.and(logEnv), runtime); // compiles
// combined.run(storeEnv, runtime); // compile error — missing LogOps✅ First-class Effect.Sleep and TestRuntime
Effect.sleep(Duration) now creates an Effect.Sleep node instead of an opaque Suspend node. This makes sleep interceptable by custom runtimes. TestRuntime uses this to advance a TestClock instead of blocking — sleep-dependent effects run instantly in tests.
TestRuntime runtime = TestRuntime.create();
// Runs instantly — no real sleep
runtime.unsafeRun(Effect.sleep(Duration.ofSeconds(30)));
// Virtual clock advanced by 30 seconds
assertEquals(Duration.ofSeconds(30), runtime.clock().currentTime());
// Chain multiple sleeps
runtime.unsafeRun(
Effect.sleep(Duration.ofSeconds(5))
.flatMap(__ -> Effect.sleep(Duration.ofSeconds(5)))
);
assertEquals(Duration.ofSeconds(40), runtime.clock().currentTime());✅ CapabilityHandler.forType() — new recommended API
CapabilityHandler.forType(Class<F>) anchors the builder to a capability family, giving the compiler enough information to infer lambda parameter types without explicit casts. The old builder() is deprecated and will be removed in a future release.
// Old (deprecated)
var handler = CapabilityHandler.builder()
.on(StoreOps.Get.class, (StoreOps.Get g) -> store.get(g.key()))
.build();
// New (recommended)
var handler = CapabilityHandler.forType(StoreOps.class)
.on(StoreOps.Get.class, g -> store.get(g.key())) // type inferred
.build();✅ parTraverse — parallel map over a collection
Effects.parTraverse collapses the two-step "build list then parAll" pattern into one declarative call. parTraverseEither is the tolerant variant — it collects both successes and failures instead of short-circuiting.
// Before
List<Effect<Throwable, Result>> effs = new ArrayList<>();
for (Item item : items) {
effs.add(checkInventory(item).<Throwable>toEffect());
}
Effect<Throwable, List<Result>> result = parAll(effs);
// After
Effect<Throwable, List<Result>> result =
Effects.parTraverse(items, item -> checkInventory(item).<Throwable>toEffect());
// Collect all results — no short-circuit on failure
Effect<Throwable, List<Either<Throwable, Result>>> mixed =
Effects.parTraverseEither(items, item -> checkInventory(item).<Throwable>toEffect());✅ Schedule<A, B> — composable repeat-on-success scheduling
RetryPolicy handles the failure path (retry on error). Schedule handles the success path — repeat an effect on a cadence, while a predicate holds, or for a fixed number of iterations, and optionally accumulate the outputs. The two compose naturally.
// Poll every 2 s, up to 10 times, until done — collect all status values
Schedule<Status, List<Status>> schedule = Schedule
.<Status>fixed(Duration.ofSeconds(2))
.recurs(10)
.whileOutput(s -> !s.isDone())
.collect();
Effect<Throwable, List<Status>> polling = schedule.repeat(checkStatus);
// Retry transient failures, then repeat on success
schedule.repeat(
unstableCheck.retry(RetryPolicy.exponential(Duration.ofMillis(50)).maxAttempts(3))
);API surface:
- Factories:
fixed(Duration),exponential(Duration),immediate() - Termination:
recurs(n),whileOutput(pred),untilOutput(pred),maxDelay(Duration),jittered(factor) - Accumulation:
collect()— folds all outputs intoList<A> - Execution:
repeat(effect)— stack-safe, integrates with the trampolined runtime
✅ Effect.effect() — no-handler generator entry point
When writing generator-style blocks that only use ctx.yield() and ctx.call() (no capability dispatch), the CapabilityHandler argument to Effect.generate() was dead ceremony. Effect.effect() removes it.
// Before
Effect<IOException, String> pipeline = Effect.generate(ctx -> {
String a = ctx.yield(fetchA());
String b = ctx.yield(fetchB());
return a + b;
}, CapabilityHandler.builder().build()); // dead noise
// After
Effect<IOException, String> pipeline = Effect.effect(ctx -> {
String a = ctx.yield(fetchA());
String b = ctx.yield(fetchB());
return a + b;
});⚠️ Breaking Changes
Effect.Sleep is a new sealed subtype (source-breaking)
Sleep<E> is a new permitted type in the public sealed interface Effect. Any exhaustive switch expression or statement over all Effect variants will fail to compile without a Sleep branch.
Before (compiles in 0.2.x, fails in 0.3.0):
return switch (effect) {
case Effect.Pure<E,A> p -> ...
case Effect.Fail<E,A> f -> ...
case Effect.Suspend<E,A> s -> ...
case Effect.Sleep<E> s -> ... // ← MISSING — compile error in 0.3.0
// ... all other cases
};After (required in 0.3.0):
case Effect.Sleep<E> s -> performSleep(s.duration()); yield result;Custom EffectRuntime implementations must add a Sleep branch and call their sleep implementation there.
CapabilityHandler.Builder is now generic (source-breaking)
The nested class changed from Builder to Builder<F extends Capability<?>>. Explicit raw-type references produce compiler warnings.
Before (raw type, 0.2.x):
CapabilityHandler.Builder builder = CapabilityHandler.builder();After (typed, 0.3.0):
CapabilityHandler.Builder<MyCapability> builder =
CapabilityHandler.forType(MyCapability.class);🔄 Migration Guide
Upgrade from 0.2.x to 0.3.0 with the following checklist:
- Add
Sleepbranch to exhaustiveEffectswitches — addcase Effect.Sleep<?> s -> ...to any switch expression or statement that covers allEffectvariants - Update custom
EffectRuntimeimplementations — interceptEffect.Sleepand call your sleep implementation (e.g.Thread.sleep(sleep.duration())) - Replace raw
CapabilityHandler.Builderreferences — useCapabilityHandler.Builder<?>or switch toCapabilityHandler.forType(MyCapability.class) - Replace
CapabilityHandler.builder()calls — useCapabilityHandler.forType(MyCapability.class)instead; behaviour is identical, type inference is better - No changes needed for
Effect<E,A>code — all existingmap,flatMap,catchAll,fork,retry,timeout, and other combinators are unchanged
⚠️ Known Limitations
CapabilityHandler.Builder.build() — flat sealed hierarchies only
The built handler resolves capabilities by checking the concrete class and then its direct interfaces. It correctly handles the standard pattern where sealed subtypes directly implement the registered interface. However, nested sealed hierarchies — where a concrete capability implements a supertype that is itself a subtype of the registered interface — are not resolved and will throw UnsupportedOperationException at runtime.
🧪 Testing
- ~93 new tests added across Milestones 1 and 2 (effect laws, test utilities, typed effects)
- Effect law tests — 11 algebraic laws v...
v0.2.2
Roux v0.2.2
Patch release with targeted runtime fixups and documentation consistency improvements.
Fixed
Resource.flatMap(...)now correctly composes finalizers without relying on a placeholderf.apply(null)path.- Outer resource release is now guaranteed when inner resource acquisition fails during
flatMapcomposition.
Documentation
- Capability docs and examples now consistently use
Unitfor side-effect-only capabilities instead ofVoid. - Replaced
yield (R) nullpatterns in examples withUnit.unit().
Roux 0.2.1
Roux 0.2.1 - Scoped Fork Context Fixes and Better Diagnostics
Roux 0.2.1 is a bugfix release focused on structured-concurrency correctness and clearer capability error reporting.
✨ Highlights
✅ Scoped fork now preserves capability context
scope.fork(...)now inherits the currentExecutionContext- Forked effects now see the same installed
CapabilityHandleras the parent flow - Behavior is now consistent with
effect.fork()
✅ Missing capability errors are now actionable
- Missing handler failures now include the exact capability type that was attempted
- Added a dedicated
MissingCapabilityHandlerExceptionfor cleaner typed handling - Error guidance points users to
unsafeRunWithHandler(...)or embedded generator handlers
✅ Cleaner failure propagation from fibers
Fiber.join()no longer double-wraps runtime exceptions- Stack traces are easier to read and root causes are easier to identify
🧪 Testing
- Added scoped-fork regression tests for:
- capability-handler inheritance inside scoped forks
- missing-handler diagnostics including capability type
- Existing scope-related tests continue to pass
📦 Installation
Maven
<dependency>
<groupId>com.cajunsystems</groupId>
<artifactId>roux</artifactId>
<version>0.2.1</version>
</dependency>Gradle (Kotlin DSL)
implementation("com.cajunsystems:roux:0.2.1")Gradle (Groovy)
implementation 'com.cajunsystems:roux:0.2.1'🔗 Links
- GitHub: github.com/CajunSystems/roux
- Maven Central: search.maven.org/artifact/com.cajunsystems/roux/0.2.1/jar
- Documentation: github.com/CajunSystems/roux/tree/main/docs
- Changelog: github.com/CajunSystems/roux/blob/main/CHANGELOG.md
Full Changelog: v0.2.0...v0.2.1
Roux 0.2.0
Roux 0.2.0 - Reliability, Resource Safety, and New Core Combinators
We're excited to announce Roux 0.2.0 - a major update focused on runtime reliability, better resource safety, richer effect combinators, and improved ergonomics across the API.
✨ Highlights
✅ New Effect Constructors
Effect.unit()Effect.runnable(Runnable)Effect.sleep(Duration)Effect.when(boolean, Effect)Effect.unless(boolean, Effect)
✅ New Combinators
tap(Consumer<A>)tapError(Consumer<E>)retry(int)retryWithDelay(int, Duration)retry(RetryPolicy)timeout(Duration)
✅ New Concurrency Helpers (Effects)
Effects.race(List)/Effects.race(ea, eb)Effects.sequence(List)Effects.traverse(List, Function)Effects.parAll(List)
✅ Resource Management
- New
Resource<A>type with:Resource.make(acquire, release)Resource.fromCloseable(acquire)resource.use(f)Resource.ensuring(effect, finalizer)
✅ Retry Policies
- New
RetryPolicywith fluent composition:immediate()fixed(Duration)exponential(Duration).maxAttempts(n).maxDelay(Duration).withJitter(factor).retryWhen(Predicate<Throwable>)
✅ Runtime and Capability Improvements
DefaultEffectRuntimenow implementsAutoCloseable- Async and fork execution now use trampolined interpretation for stack safety
- Replaced spin-wait startup synchronization with
CountDownLatch - Improved capability dispatch semantics and handler composition behavior
🛠️ Fixes
- Closed stack-safety gaps in
runAsyncandexecuteFork - Removed CPU-burning spin-wait loops in async/fork startup
- Fixed capability composition fall-through behavior to avoid swallowing internal handler errors
- Fixed interface-resolution edge cases in
CompositeCapabilityHandler - Corrected docs drift where
.retry()and.timeout()were previously documented but missing - Updated Java-idiomatic tuple accessor names (
first(),second(),third())
🧪 Testing
- 205 total tests (up from ~100 in 0.1.0)
- New dedicated test coverage for:
- combinators (
tap, retry variants, timeout, conditional effects) - collection helpers (
sequence,traverse,parAll,race) Eitherenriched API- capability handler builder/composition
- retry policy validation + integration
- full
Resourcelifecycle semantics
- combinators (
📦 Installation
Maven
<dependency>
<groupId>com.cajunsystems</groupId>
<artifactId>roux</artifactId>
<version>0.2.0</version>
</dependency>Gradle (Kotlin DSL)
implementation("com.cajunsystems:roux:0.2.0")Gradle (Groovy)
implementation 'com.cajunsystems:roux:0.2.0'🔗 Links
- GitHub: github.com/CajunSystems/roux
- Maven Central: search.maven.org/artifact/com.cajunsystems/roux/0.2.0/jar
- Documentation: github.com/CajunSystems/roux/tree/main/docs
- Changelog: github.com/CajunSystems/roux/blob/main/CHANGELOG.md
Full Changelog: v0.1.0...v0.2.0
Roux 0.1.0 - Initial Release
Roux 0.1.0 - Initial Release
We're excited to announce the first release of Roux - a modern effect system for Java 21+ that brings composable, type-safe effects to the JVM!
🎉 What is Roux?
Roux is a lightweight, pragmatic effect system built from the ground up for Java 21+ virtual threads. It provides a clean, composable way to handle side effects while staying close to Java's natural behavior.
✨ Key Features
🧵 Virtual Thread Native
Built specifically for JDK 21+ virtual threads with structured concurrency support.
🎯 Core Effect System
- Type-safe effects with explicit error channel:
Effect<E, A> - Pure values -
Effect.succeed()for wrapping values - Failures -
Effect.fail()for explicit error handling - Lazy evaluation -
Effect.suspend()for deferred computations
🔀 Rich Combinators
map- Transform success valuesflatMap- Chain effects sequentially (monadic composition)catchAll- Handle and recover from errorsmapError- Transform error typesorElse- Fallback to alternative effect on failureattempt- Convert effect toEither<E, A>fold- Handle both success and error caseswiden/narrow- Type-safe error type transformations
⚡ Concurrency & Parallelism
- Fork/Fiber - Launch effects concurrently
- join - Wait for fiber completion
- interrupt - Cancel running fibers
- zipPar - Run two effects in parallel
- Effects.par - Run multiple effects in parallel
🏗️ Structured Concurrency
- Effect.scoped - Create structured concurrency scopes
- Automatic cancellation - All forked effects cancelled on scope exit
- Built on Java 21's
StructuredTaskScope(JEP 453)
🎭 Algebraic Effects (Capabilities)
- Capability system - Define custom algebraic effects
- CapabilityHandler - Handle capabilities with custom interpreters
- Type-safe composition - Compose handlers safely
🎨 Generator-Style Effects
- Effect.generate - Build effects using imperative-style generators
- GeneratorContext - Imperative API for effect building
perform- Execute capabilitiesyield- Embed effectscall- Execute throwing operations
🛡️ Stack Safety
- Trampolined execution - Stack-safe by default
- Constant stack depth - Handle millions of
flatMapoperations without stack overflow - Enabled by default - No configuration needed
📦 Installation
Maven
<dependency>
<groupId>com.cajunsystems</groupId>
<artifactId>roux</artifactId>
<version>0.1.0</version>
</dependency>Gradle (Kotlin DSL)
implementation("com.cajunsystems:roux:0.1.0")Gradle (Groovy)
implementation 'com.cajunsystems:roux:0.1.0'Requirements: Java 21 or higher
🚀 Quick Example
import com.cajunsystems.roux.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Effect<IOException, String> readFile = Effect.suspend(() ->
Files.readString(Path.of("config.txt"))
);
Effect<IOException, String> withFallback = readFile
.catchAll(e -> Effect.succeed("default config"))
.map(String::toUpperCase);
EffectRuntime runtime = DefaultEffectRuntime.create();
String result = runtime.unsafeRun(withFallback);📚 Documentation
- Effect API Reference
- Structured Concurrency Guide
- Capabilities Guide
- Capability Recipes
- Custom Capabilities Example
🧪 Testing
This release includes 100+ unit tests covering:
- All effect combinators
- Stack safety (chains up to 1,000,000 operations)
- Concurrency and fork/fiber behavior
- Structured concurrency scopes
- Capability system
- Generator-style effects
🔗 Links
- GitHub: https://github.com/CajunSystems/roux
- Maven Central: https://search.maven.org/artifact/com.cajunsystems/roux/0.1.0/jar
- Documentation: https://github.com/CajunSystems/roux/tree/main/docs
🙏 Acknowledgments
Roux is inspired by modern effect systems like ZIO and Cats Effect, adapted for Java's unique strengths with virtual threads and structured concurrency.
📝 License
MIT License - see LICENSE file for details.
Full Changelog: https://github.com/CajunSystems/roux/blob/main/CHANGELOG.md