jev-resilience
September 17, 2026 ยท View on GitHub
A Spring Boot starter that brings semantic failure detection to Spring WebFlux services. Standard circuit breakers only see transport-level failures (5xx, timeouts, connection errors) and miss silent failures: HTTP 200 responses whose body encodes an error, a stack trace, or a "system under maintenance" notice. This starter evaluates each response payload with TypeSafe Jev and converts suspected silent failures into ordinary exceptions that your existing error handling can process.
Key Features
- Reactive AOP. An AspectJ aspect composes the Jev evaluation into the returned
Mono/FluxviaflatMap. No reactive thread is ever blocked. - Fail-open by design. Any TypeSafe API error, timeout, or malformed response is
treated as "not a failure" (score
0.0). A Jev outage cannot trip your circuit breaker or interrupt request processing. - Zero-overhead execution. Evaluation is a single non-blocking
WebClientcall with a configurable timeout (750 ms default), added to the existing reactive pipeline. No extra threads, no blocking. - Standard Spring error handling. Failures surface as
SemanticFailureException, a regularRuntimeException: handle it withonErrorResume,@ExceptionHandler, or register it in a Resilience4jCircuitBreaker'srecordExceptions.
Installation
The starter is published via JitPack. Add the JitPack repository
and the dependency to your pom.xml:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.vicente-md</groupId>
<artifactId>jev-resilience</artifactId>
<version>0.1.1</version>
</dependency>
The consuming application is expected to provide spring-boot-starter-webflux.
Configuration
typesafe:
jev:
api-key: ${TYPESAFE_API_KEY}
base-url: https://api.typesafe.ai # optional; this is the default
model: jev-latest # optional; this is the default
export TYPESAFE_API_KEY=sk-... # issued at https://console.typesafe.ai/settings/keys
Auto-configuration activates once typesafe.jev.api-key is set.
Usage
Annotate any WebFlux service method that returns Mono<T> or Flux<T> with
@SemanticCircuitBreaker:
@Service
public class PaymentService {
private final WebClient webClient;
public PaymentService(WebClient.Builder builder) {
this.webClient = builder.baseUrl("https://provider.example.com").build();
}
@SemanticCircuitBreaker(confidenceThreshold = 0.85)
public Mono<PaymentStatusResponse> fetchStatus(String paymentId) {
return webClient.get()
.uri("/payments/{id}/status", paymentId)
.retrieve()
.bodyToMono(PaymentStatusResponse.class);
}
}
When Jev's confidence that a response is a disguised failure exceeds
confidenceThreshold, the pipeline emits a SemanticFailureException instead of the
original item. Handle it like any other exception:
@RestController
@RequestMapping("/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@GetMapping("/{id}/status")
public Mono<ResponseEntity<?>> getPaymentStatus(@PathVariable String id) {
return paymentService.fetchStatus(id)
.map(ResponseEntity::ok)
.onErrorResume(SemanticFailureException.class, ex ->
Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("Provider returned a disguised failure (confidence=" + ex.getConfidenceScore() + ")")));
}
}
SemanticFailureException also integrates with Resilience4j: add it to a
CircuitBreaker's recordExceptions and it counts toward the failure rate like any
transport error.
Testing
Two bundled tests verify the exact request that JevEvaluationService sends.
JevEvaluationServiceMockWebServerTest runs offline against a local MockWebServer and
asserts the path, Authorization header, and JSON body of the POST /v1/systemone Noul
request, plus the fail-open behavior on a 5xx response:
mvn test -Dtest=JevEvaluationServiceMockWebServerTest
Expected result: Tests run: 2, Failures: 0, Errors: 0.
JevEvaluationServiceRealApiIT is an opt-in check against the real TypeSafe API. It runs
only when TYPESAFE_API_KEY is set, sends one healthy payload and one disguised
maintenance notice, asserts that the returned scores are low and high respectively, and
prints both noul scores:
export TYPESAFE_API_KEY=sk-...
mvn test -Dtest=JevEvaluationServiceRealApiIT
Without the environment variable, this test is skipped and does not run in a normal build.
The same request shape with curl:
curl -s -X POST https://api.typesafe.ai/v1/systemone \
-H "Authorization: Bearer $TYPESAFE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"state": "{\"status\":\"ok\",\"note\":\"system under maintenance, please retry later\"}",
"model": "jev-latest",
"questions": {
"is_silent_failure": {
"type": "noul",
"instructions": "Does this payload represent a silent failure, a maintenance window, or an error state disguised as a success?"
}
}
}'
A response with answers.is_silent_failure.noul close to 1.0 confirms that both the
API key and the request shape are correct.
Note:
JevEvaluationServicesends the configured key asAuthorization: Bearer <api-key>. Configure only the raw key value intypesafe.jev.api-key; theBearerprefix is added by the starter.
How It Works
ReactiveSemanticCircuitBreakerAspectintercepts@SemanticCircuitBreakermethods and transforms the returnedMono<T>/Flux<T>withflatMap.- Each emitted item is serialized to a string and passed to
JevEvaluationService, which submits a singleNoulquestion toPOST /v1/systemoneon the TypeSafe API through a non-blockingWebClient. - If the returned
noulconfidence score exceedsconfidenceThreshold, the item is replaced withMono.error(new SemanticFailureException(score, payload)); otherwise it passes through unchanged. - If the Jev call fails or times out, the evaluation returns
0.0(fail-open), so a TypeSafe outage never trips the circuit breaker.