A sample code to explore resilience-4j, io.vavr.Try and rxJava3
- You can just use
mvn clean test verifyfor local testing - Run javadoc, and sonar:sonar and any other validations to ensure everything is in order
- Do
git add,git commitandgit push origin main. scripts/release.shwill directly upload the artifact
Elegant, composable error handling and retries powered by Resilience4j, Vavr, and Lombok’s
@ExtensionMethod.
exception-retry transforms Java’s traditional exception handling into declarative, functional pipelines.
It integrates:
- ✅ Resilience4j for retries, circuit breakers, bulkheads, and rate limiters
- ✅ Vavr for
TryandEithersemantics - ✅ Lombok @ExtensionMethod for natural, OO-style fluent APIs
Instead of procedural try/catch spaghetti, write intention-revealing resilience like this:
function
.errorMappedFunction(...)
.retryFunction(retry)
.tryWrap()
.toEither();| Category | Description |
|---|---|
| Retry | Declarative .retryFunction(), .retrySupplier(), .retryBiFunction() |
| Error Mapping | Transform exceptions via .errorMappedFunction() |
| Error Consumption | React to exceptions without throwing via .errorConsumedFunction() |
| Circuit Breaker | .circuitBreakFunction(cb) and .circuitBreakCheckedFunction(cb) |
| Bulkhead | .bulkheadFunction(bh) and .bulkheadBiFunction(bh) |
| Try / Either Integration | Safe wrapping via .tryWrap() and .toEither() |
| Checked Exceptions | Seamless handling of IOException, SQLException, etc. |
| Composable | Every transformation returns a function — pure and chainable |
| CommonRuntimeException and ExceptionCode | Make it easier to construct error |
Add the following to your Maven pom.xml:
<dependency>
<groupId>io.github.venkateshamurthy</groupId>
<artifactId>exception-retry</artifactId>
<version>1.5</version> <!-- replace with latest -->
</dependency>Enable Lombok's Extension Methods such as follows (in case if you are willing tomake sue of Lombok's experimental feature)
import lombok.experimental.ExtensionMethod;
import io.github.venkateshamurthy.exceptional.*;
@ExtensionMethod({RxFunction.class,RxSupplier.class,RxTry.class})
public class MyService { }You can call now as
| With Extension Method | Without Extension Method |
|---|---|
| function.retryFunction(retry); | RxFunction.retryFunction(function, retry); |
| supplier.errorConsumedSupplier(...) | RxSupplier.errorConsumedSupplier(supplier,...) |
| checkedFn.retryCheckedFunction(...) | RxFunction.retryCheckedFunction(checkedFn,retry); |
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import static io.github.venkateshamurthy.exceptional.Delayer.FIBONACCI;
Retry retry = Retry.of("example", RetryConfig.custom()
.intervalFunction(FIBONACCI.millis(1, 300))
.retryExceptions(IOException.class, SQLException.class)
.maxAttempts(10)
.build());- FIBONACCI — gentle growth
- EXPONENTIAL — aggressive recovery
- LINEAR — predictable retry rate
- LOGARITHMIC — slow ramp-up
Function<String, Integer> fn = toFunction(s -> {
if (s == null) throw new NullPointerException();
if (s.equals("bad")) throw new UnsupportedOperationException();
return s.length();
});
Either<Throwable, Integer> result = fn
.errorMappedFunction(
UnsupportedOperationException.class, IllegalStateException::new,
NullPointerException.class, IllegalArgumentException::new
)
.retryFunction(retry)
.tryWrap()
.toEither()
.apply("bad");➡ Readable, composable resilience with minimal boilerplate.
Supplier<Integer> s = toSupplier(() -> {
String input = "hi";
if (input.length() < 3) throw new IllegalStateException("too short");
return input.length();
});
Supplier<Integer> safe = s
.errorConsumedSupplier(IllegalStateException.class, ex -> log.warn("Handled: {}", ex.getMessage()))
.retrySupplier(retry);
Try<Integer> t = safe.tryWrap();
Either<Throwable, Integer> e = t.toEither();✅ Perfect for metrics or observability layers.
CheckedBiFunction<String, String, Integer> f = toCheckedBiFunction((a, b) -> {
if (a.equals(b)) throw new IOException("equal!");
if (a.isEmpty()) throw new SQLException("empty");
return (a + b).length();
});
CheckedBiFunction<String, String, Integer> resilient = f
.errorConsumedCheckedBiFunction(SQLException.class, ex -> log.warn("SQL error"))
.retryCheckedBiFunction(retry);
Either<Throwable, Integer> out = resilient.tryWrap("a", "b").toEither();✅ Handles checked exceptions ✅ Applies retry semantics ✅ Returns Vavr Either
1️⃣ Declarative Resilience
Resilience becomes expressive, not procedural:
fn.errorMappedFunction(...)
.retryFunction(retry)
.tryWrap()
.toEither();No try-catch pollution — the chain reads like a narrative.
2️⃣ Checked Exceptions, Elevated
Java checked exceptions (IOException, SQLException) are supported directly in CheckedFunction and CheckedBiFunction. 3️⃣ Composability by Design
Each method returns a new transformed function, not a side-effecting wrapper. This enables seamless chaining:
fn.errorMappedFunction(...)
.bulkheadFunction(bh)
.circuitBreakFunction(cb)
.retryFunction(retry)
.tryWrap();4️⃣ Observability-First
Because wrappers are built on Resilience4j, metrics like retry attempts, success/failure counts, and circuit state are directly accessible.
5️⃣ Building Exception made easier
Often, we may find building exception in a context can be very mundane, tedious, repetitive and verbose leading to most of the real estate in the coding occupied by exception building drudge. Most of the time the finite set of errors are codified to enums with a short description of the error and for web applications it is prudent to accompany by with HTTPStatus. Here is an example following a logger style of convenience.
CREDENTIAL_MISSING.toCommonRTE("CDN Access Creds are wrong",
"Missing or invalid credential ID: {}", request.getCredentialId())
.logInfo();A variant to the above is to provide a key based marker in the message template such as
CREDENTIAL_MISSING.toCommonRTE("CDN Access Creds are wrong",
"Missing or invalid credential ID: {credId}", Map.of("credId", request.getCredentialId()))
.logInfo();The common runtime exception class offers override setters in fluent way to reset detail message or status etc
var primarySafe = primarySupplier.retrySupplier(primaryRetry);
var fallbackSafe = fallbackSupplier.retrySupplier(fallbackRetry);
var result = primarySafe.tryWrap()
.recoverWith(err -> fallbackSafe.tryWrap())
.get();var circuitSafe = fn
.circuitBreakFunction(circuitBreaker)
.retryFunction(retry)
.tryWrap();import lombok.experimental.ExtensionMethod;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.core.functions.CheckedBiFunction;
import io.vavr.control.Either;
import static io.github.venkateshamurthy.exceptional.Delayer.FIBONACCI;
@ExtensionMethod({
io.github.venkateshamurthy.exceptional.RxFunction.class,
io.github.venkateshamurthy.exceptional.RxTry.class
})
public class FullExample {
public static void main(String[] args) {
Retry retry = Retry.of("demo", RetryConfig.custom()
.intervalFunction(FIBONACCI.millis(10, 500))
.maxAttempts(5)
.retryExceptions(java.io.IOException.class)
.build());
CheckedBiFunction<String, String, Integer> f = toCheckedBiFunction((a, b) -> {
if (a == null || b == null) throw new java.sql.SQLException("null");
if (a.equals(b)) throw new java.io.IOException("io");
return (a + b).length();
});
var resilient = f
.errorConsumedCheckedBiFunction(java.sql.SQLException.class, ex -> System.out.println("SQL consumed"))
.retryCheckedBiFunction(retry);
Either<Throwable, Integer> out = resilient.tryWrap("hello", "world").toEither();
out.peek(System.out::println).peekLeft(e -> e.printStackTrace());
}
}The following diagram illustrates the complete build, signing, and publishing flow
for projects inheriting from modern-java-parent.
It highlights how GitHub Actions, Maven Central, GitHub Packages, and SonarCloud
interact through securely managed secrets and tokens.
Diagram overview:
- 🔹 GitHub Actions Runner executes the
publish.ymlworkflow defined in each repository. - 🔹 Environment secrets (e.g.,
GH_TOKEN,MAVEN_CENTRAL_TOKEN,GPG_PRIVATE_KEY) are injected securely at runtime. - 🔹 Maven builds, tests, and signs all artifacts using your GPG key.
- 🔹 Signed artifacts are then:
- Published to Maven Central (authenticated via
MAVEN_CENTRAL_USER/MAVEN_CENTRAL_TOKEN) - Deployed to GitHub Packages (using your
GH_TOKENPersonal Access Token)
- Published to Maven Central (authenticated via
- 🔹 SonarCloud analysis is optionally triggered using
SONAR_ORG/SONAR_TOKEN. - 🔹 GitHub Pages automatically hosts generated Javadocs via the built-in
GITHUB_TOKEN.
🧠 Each project maintains its own
publish.ymlworkflow and secrets — ensuring isolated, secure, and independently verifiable release automation.
| Token / Secret | Purpose | Provided By |
|---|---|---|
GH_TOKEN |
Publish artifacts to GitHub Packages | Personal Access Token (repo secret) |
GPG_PRIVATE_KEY |
Sign artifacts before publishing | Exported GPG key (repo secret) |
MAVEN_CENTRAL_USER / MAVEN_CENTRAL_TOKEN |
Authenticate with Sonatype Central | Sonatype account |
MAVEN_GPG_KEYNAME / MAVEN_GPG_PASSPHRASE |
Identify and unlock GPG key | Your keypair |
SONAR_ORG / SONAR_TOKEN |
Enable SonarCloud analysis | SonarCloud |
GITHUB_TOKEN |
Publish docs or releases to this repo | Auto-provided by GitHub |
📘 This CI/CD workflow ensures a clean, repeatable release pipeline for modern Java projects — combining artifact signing, dual registry publishing, and static analysis in one automated process.
Venkatesha Murthy
Collaborative technologist and creator of Exception-Retry, a library designed to unify resilience, readability, and functional purity in Java.
“Functional elegance meets production resilience.” — Exception-Retry Design Philosophy
