Jiffy is a lightweight library that brings algebraic effects to Java with compile-time effect checking through annotations.
- 🔍 Compile-time effect checking - Catch missing effect declarations during compilation
- 📝 Annotation-based - Use familiar Java annotations to declare effects
- 🎯 Type-safe - Effects are visible in method signatures
- 🔧 Extensible - Easy to add new effects and handlers
- 🏗️ Spring-friendly - Integrates well with Spring Boot applications
- ⚡ Minimal overhead - Efficient runtime with direct handler dispatch
- 🧪 Multiple execution modes - Sync, async, traced, and dry-run
- 🧵 Structured concurrency - Uses Java 25 virtual threads and StructuredTaskScope
- 🛡️ Stack-safe - Handles arbitrarily deep effect chains without stack overflow
This demo shows how to use jiffy in a SpringBoot application. It also has a nice introduction to core concepts used:
https://github.com/thma/jiffy-clean-architecture/
public sealed interface LogEffect extends Effect<Void> {
record Info(String message) implements LogEffect {}
record Error(String message) implements LogEffect {}
}import org.jiffy.annotations.Uses;
import org.jiffy.core.Eff;
public class UserService {
@Uses({LogEffect.class, DatabaseEffect.class})
public Eff<User> findUser(Long id) {
return Eff.perform(new LogEffect.Info("Finding user " + id))
.flatMap(ignored ->
Eff.perform(new DatabaseEffect.Query("SELECT * FROM users WHERE id = " + id))
)
.map(result -> parseUser(result));
}
}public class LogHandler implements EffectHandler<LogEffect> {
@Override
public <T> T handle(LogEffect effect) {
switch (effect) {
case LogEffect.Info(var message) -> System.out.println("[INFO] " + message);
case LogEffect.Error(var message) -> System.err.println("[ERROR] " + message);
}
return null; // LogEffect returns Void
}
}EffectRuntime runtime = EffectRuntime.builder()
.withHandler(LogEffect.class, new LogHandler())
.withHandler(DatabaseEffect.class, new DatabaseHandler())
.build();
// Execute the program
User user = runtime.run(findUser(123L));The annotation processor validates that all effects used in a method are declared:
@Uses({LogEffect.class}) // Missing DatabaseEffect!
public Eff<User> findUser(Long id) {
return Eff.perform(new DatabaseEffect.Query("...")) // Compile error!
.map(this::parseUser);
}<dependency>
<groupId>org.jiffy</groupId>
<artifactId>jiffy</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>implementation 'org.jiffy:jiffy:1.0.0-SNAPSHOT'Effects represent side effects as data. They extend the Effect<T> interface where T is the return type.
Eff<T> is a monadic type that represents a computation that may perform effects and produce a value of type T. It is a pure data structure describing what to do, not how to do it.
Handlers interpret effects. They implement EffectHandler<E> where E is the effect type.
The runtime holds registered handlers and provides methods to execute Eff programs:
EffectRuntime runtime = EffectRuntime.builder()
.withHandler(LogEffect.class, logHandler)
.withHandler(DatabaseEffect.class, dbHandler)
.build();@Uses- Declares which effects a method may use@Pure- Marks methods as effect-free
Jiffy provides multiple ways to execute effect programs:
// Direct execution
User user = runtime.run(findUser(123L));Jiffy uses Java 25's virtual threads and StructuredTaskScope for efficient async execution:
// Returns immediately with a StructuredFuture (runs on virtual thread)
StructuredFuture<User> future = runtime.runAsync(findUser(123L));
// Block and wait for result
User user = future.join();
// Wait with timeout
User user = future.join(5, TimeUnit.SECONDS);
// Compose async operations
StructuredFuture<Report> report = runtime.runAsync(fetchUser(id))
.flatMap(user -> runtime.runAsync(fetchOrders(user.id()))
.map(orders -> generateReport(user, orders)));
// Interop with CompletableFuture when needed
CompletableFuture<User> cf = future.toCompletableFuture();Capture all effects performed during execution for debugging, testing, or auditing:
Traced<User> traced = runtime.runTraced(findUser(123L));
// Access the result
User user = traced.result();
// Inspect the effect log
System.out.println("Effects performed: " + traced.effectCount());
traced.effectLog().forEach(System.out::println);
// Query specific effect types
if (traced.hasEffect(DatabaseEffect.Query.class)) {
List<DatabaseEffect.Query> queries = traced.getEffects(DatabaseEffect.Query.class);
System.out.println("Database queries: " + queries.size());
}Inspect effects without executing them:
List<Effect<?>> effects = runtime.dryRun(findUser(123L));
System.out.println("This program would perform: " + effects);Scala-style for comprehensions for composing multiple effects:
Eff<CustomerReport> report = Eff.For(
perform(new LogEffect.Info("Generating report")),
perform(new DatabaseEffect.FindCustomer(customerId)),
perform(new DatabaseEffect.FindOrders(customerId))
).yield((log, customer, orders) ->
new CustomerReport(customer, orders)
);Run effects concurrently using StructuredTaskScope under the hood:
Eff.parallel(
fetchUserData(userId),
fetchUserOrders(userId)
).map(pair -> new UserProfile(pair.getFirst(), pair.getSecond()));// Simple recovery with a fallback value
fetchData()
.recover(error -> defaultData());
// Recovery with logging (using recoverWith for proper effect handling)
fetchData()
.recoverWith(error ->
Eff.perform(new LogEffect.Error("Failed: " + error.getMessage()))
.map(v -> defaultData())
);Eff.andThen(
validateInput(data),
saveToDatabase(data),
notifyUser(userId)
);Jiffy's interpreter uses an iterative algorithm with an explicit continuation stack, making it safe for arbitrarily deep effect chains:
// This works fine - no StackOverflowError!
Eff<Integer> program = Eff.pure(0);
for (int i = 0; i < 100_000; i++) {
program = program.flatMap(n -> Eff.pure(n + 1));
}
Integer result = runtime.run(program); // Returns 100000This is essential for:
- Stream processing - Processing large collections with
flatMap - Recursive patterns - Paginated API calls, tree traversals
- Long-running workflows - Business processes with many steps
The implementation avoids Java's lack of tail-call optimization by converting recursion to iteration internally.
Jiffy follows a clean separation between effect description and interpretation:
┌─────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Effects │ │ Eff<T> │ │ @Uses │ │
│ │ (Data) │───▶│ (Program) │◀───│ @Pure │ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
│ │ │
└─────────────────────────────│────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ EffectRuntime │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Handlers │ │ Interpreter │ │ Execution │ │
│ │ │◀───│ (stack-safe │───▶│ Modes │ │
│ │ │ │ iterative) │ │ │ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
│ │
│ run() | runAsync() | runTraced() | dryRun() │
└─────────────────────────────────────────────────────────┘
git clone https://github.com/yourusername/jiffy.git
cd jiffy
mvn clean installContributions are welcome! Please open an issue or submit a pull request.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.
