Jev Spring Boot Starter

September 20, 2026 · View on GitHub

Ask typed questions with TypeSafe Jev from a Spring MVC application. Add one dependency, set an API key, and inject JevClient.

Spring Boot 4.0+ · Java 17+ · Spring MVC · RestClient · Jackson 3

This is an independent community project. Version 0.1.0-SNAPSHOT is a local development version and is not published to Maven Central.

Get started

First, build and install the starter from this repository:

./mvnw clean install

Add it to an existing Spring Boot 4 MVC application:

<dependency>
    <groupId>dev.danvega</groupId>
    <artifactId>jev-spring-boot-starter</artifactId>
    <version>0.1.0-SNAPSHOT</version>
</dependency>

Your application supplies spring-boot-starter-webmvc. The Jev starter supplies spring-boot-starter-restclient; it does not start a web server itself.

Set the key:

export TYPESAFE_API_KEY=your-key

Inject the client into a service:

import dev.danvega.jev.JevClient;
import dev.danvega.jev.Question;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class TicketService {
    private final JevClient jev;

    public TicketService(JevClient jev) {
        this.jev = jev;
    }

    public String route(String message) {
        var response = jev.evaluate(message, Map.of(
                "team", Question.choice("Which team should handle this?",
                        "billing", "integrations", "support")));

        return response.choice("team").choice();
    }
}

No enable annotation or component scanning configuration is needed. The starter activates in servlet applications, supplies one JevClient, and backs off when you provide your own. It makes no HTTP calls during startup. A missing API key produces an actionable startup error.

Three question types, one request

var response = jev.evaluate("Stripe is broken and I cannot accept payments!", Map.of(
        "urgent", Question.noul("Does this need urgent attention?"),
        "team", Question.choice("Which team should handle this?", Map.of(
                "billing", "Charges and refunds",
                "integrations", "Connecting third-party services")),
        "severity", Question.score("How severe is the issue?",
                "Cosmetic", "Workaround available", "Blocking")));

double urgency = response.noul("urgent").noul();
String team = response.choice("team").choice();
double confidence = response.choice("team").confidence();
double severity = response.score("severity").score();
FactoryMeaningResult
Question.noul(...)Yes/no judgmentProbability of yes, between 0 and 1
Question.choice(...)Choose a named optionLabel, probabilities, confidence
Question.score(...)Rate against ordered levelsWeighted zero-based score, legend, probabilities, confidence

A three-level score spans 0–2 and can fall between levels. Your application decides what probability or confidence is sufficient to act; the starter does not impose thresholds.

response.answers() exposes the full Map<String, Answer>. The named accessors above check the answer type and give a clear error for a missing name or incorrect accessor. response.model() reports the actual model; response.usage() provides token counts.

State can be text, a record, a map, or a list that Spring's Jackson converter can serialize. Instructions and criterion descriptions also accept structured values. Choice labels can have null descriptions; the varargs factory handles that without a null-containing map. Collections are shallow snapshots: treat any nested caller-owned values as immutable while in use.

To describe both sides of a yes/no question:

Question.noul("Is this urgent?", "Explicitly time sensitive", "Can wait until next week");

To override the model for one call:

import dev.danvega.jev.JevRequest;

var response = jev.evaluate(new JevRequest(message, questions, "jev-1.13.0"));

Configuration

Virtual threads on JDK 21+

Enable Spring Boot's virtual-thread support in your MVC application:

spring:
  threads:
    virtual:
      enabled: true

With Boot's embedded Tomcat, MVC requests then run on virtual threads on JDK 21+. JevClient and RestClient execute synchronously on that same request thread, allowing blocking HTTP I/O to benefit from virtual threads. The client needs no extra executor or asynchronous API. This helps concurrency during I/O waits; it does not make Jev inference faster.

The included example enables this setting. Boot falls back to platform threads on JDK 17; set spring.threads.virtual.enabled=false to opt out on newer JDKs. The starter itself does not change application-wide threading settings. Calls made outside MVC use their caller's thread as usual.

See Spring Boot virtual threads.

Jev properties

PropertyDefaultPurpose
jev.api-keyTYPESAFE_API_KEY fallbackTypeSafe API key
jev.base-urlhttps://api.typesafe.aiAPI root, without /v1/systemone
jev.modeljev-latestDefault model
jev.enabledtrueEnable auto-configuration

These properties have generated IDE completion metadata. Spring's standard property-source precedence applies. You can also set JEV_API_KEY, JEV_BASE_URL, and JEV_MODEL. An explicitly configured jev.api-key wins over TYPESAFE_API_KEY; an explicitly blank key is an error rather than silently selecting another credential.

For YAML-based configuration:

jev:
  api-key: ${TYPESAFE_API_KEY}
  model: jev-latest

HTTP settings

The client uses a clone of Boot's configured RestClient.Builder, retaining its message converters, request factory, interceptors, and observation configuration. The starter does not install a second JSON mapper or replace your HTTP transport.

Set timeouts using Boot 4's standard properties:

spring:
  http:
    clients:
      connect-timeout: 5s
      read-timeout: 10s

These settings apply to Boot-configured HTTP clients application-wide. The starter adds no timeout of its own; without configured values, the selected request factory's defaults apply. Use a Spring RestClientCustomizer for shared interceptors or other HTTP customization.

For Jev-specific customization, supply your own bean:

import dev.danvega.jev.JevClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;

@Bean
JevClient jevClient(RestClient.Builder builder,
        @Value("${jev.api-key:${TYPESAFE_API_KEY:}}") String apiKey) {
    var restClient = builder.clone()
            .baseUrl("https://api.typesafe.ai")
            .defaultHeaders(headers -> headers.setBearerAuth(apiKey))
            .defaultHeader("X-Application", "support")
            .build();
    return new JevClient(restClient, "jev-latest");
}

Place this method in an application configuration class. When a custom JevClient is present, your configuration owns its credentials and HTTP settings.

Errors and retries

HTTP failures use Spring's RestClientResponseException, preserving the status, response headers (including Retry-After), and body. Connection failures use ResourceAccessException. Malformed responses raise RestClientException; missing numeric values never become zero. Unknown JSON fields are ignored, while unknown answer types and mismatched answer names/types fail.

The starter makes one attempt per evaluation and has no automatic retry policy. Unlike the JavaScript SDK, it leaves retry decisions to the application. For 429/529 responses, an application can honor Retry-After and apply bounded backoff. Consider that retrying a POST after an ambiguous timeout can repeat work. Do not configure an error handler that suppresses failed HTTP statuses.

Run the MVC example

The support-triage example exposes POST /triage:

./mvnw install
export TYPESAFE_API_KEY=your-key
./mvnw -f examples/support-triage/pom.xml spring-boot:run

From another terminal:

curl http://localhost:8080/triage \
  -H 'Content-Type: application/json' \
  -d '{"message":"Stripe is broken and I cannot accept payments!"}'

This example calls the live API only when you send a request. Its tests use a local HTTP stub.

Development

./mvnw clean install
./mvnw -f examples/support-triage/pom.xml clean verify

The compatibility matrix covers Boot 4.0.0, 4.0.8, and 4.1.1 on Java 17, 21, and 25. The MVC test verifies the actual outgoing HTTP call runs on a virtual thread on JDK 21+, with separate checks for the platform-thread opt-out and JDK 17 fallback. To select a version for either build, add -Dspring-boot.version=4.1.1. All tests are offline and require no API key.