Service Provider Interface

The IBM watsonx.ai Java SDK exposes several Service Provider Interfaces that allow framework integrators to replace or customize its core infrastructure — HTTP transport, thread management, and JSON serialization — without changing any application code. All SPIs are resolved at runtime via the Java ServiceLoader mechanism.


REST Client SPI

Every service delegates HTTP communication to an abstract RestClient. The concrete implementation is discovered at startup via ServiceLoader; if none is registered, the SDK falls back to its built-in DefaultRestClient (based on the Java HttpClient).

Service to REST client mapping

Service REST client
ChatService ChatRestClient
EmbeddingService EmbeddingRestClient
RerankService RerankRestClient
TokenizationService TokenizationRestClient
DetectionService DetectionRestClient
TextClassificationService TextClassificationRestClient
TextExtractionService TextExtractionRestClient
TimeSeriesService TimeSeriesRestClient
FoundationModelService FoundationModelRestClient
ToolService ToolRestClient
DeploymentService DeploymentRestClient

Each RestClient performs the lookup this way:

static ChatRestClient.Builder builder() {
    return ServiceLoader.load(ChatRestClientBuilderFactory.class).findFirst()
        .map(Supplier::get)
        .orElse(DefaultRestClient.builder());
}

Providing a custom REST client

The recommended pattern is to keep the factory and builder as static nested classes inside the custom RestClient. The example below shows a full implementation for ChatRestClient using Quarkus and the RESTEasy Reactive client.

Step 1 — Define a JAX-RS interface for the API

Declare the API endpoints using JAX-RS annotations. The SDK’s request/response types can be used directly:

@Path("/ml/v1")
public interface ChatRestApi {

    @POST
    @Path("text/chat")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    ChatResponse chat(
        @HeaderParam("X-Request-Id") String requestId,
        @HeaderParam("X-Global-Transaction-Id") String transactionId,
        @QueryParam("version") String version,
        TextChatRequest request);

    @POST
    @Path("text/chat_stream")
    @Consumes(MediaType.APPLICATION_JSON)
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    Multi<String> chatStreaming(
        @HeaderParam("X-Request-Id") String requestId,
        @HeaderParam("X-Global-Transaction-Id") String transactionId,
        @QueryParam("version") String version,
        TextChatRequest request);
}

Step 2 — Implement the RestClient

Extend ChatRestClient, build the framework-native client in the constructor using the inherited fields, and implement the abstract methods:

public final class QuarkusChatRestClient extends ChatRestClient {

    private final ChatRestApi client;

    QuarkusChatRestClient(Builder builder) {
        super(builder);
        try {
            client = QuarkusRestClientBuilder.newBuilder()
                .baseUrl(URI.create(baseUrl).toURL())
                .clientHeadersFactory(new BearerTokenHeaderFactory(authenticator))
                .connectTimeout(timeout.toSeconds(), TimeUnit.SECONDS)
                .readTimeout(timeout.toSeconds(), TimeUnit.SECONDS)
                .build(ChatRestApi.class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public ChatResponse chat(String transactionId, TextChatRequest textChatRequest) {
        // Quarkus implementation
        // ...
    }

    @Override
    public CompletableFuture<ChatResponse> chatStreaming(
            String transactionId,
            TextChatRequest textChatRequest,
            ChatClientContext context,
            ChatHandler handler) {
        // Quarkus implementation
        // ...
    }

    // Factory and Builder as static nested classes
    public static final class QuarkusChatRestClientBuilderFactory
            implements ChatRestClientBuilderFactory {
        @Override
        public Builder get() {
            return new QuarkusChatRestClient.Builder();
        }
    }

    static final class Builder extends ChatRestClient.Builder {
        @Override
        public ChatRestClient build() {
            return new QuarkusChatRestClient(this);
        }
    }
}

Step 3 — Register via ServiceLoader

Create the file:

META-INF/services/com.ibm.watsonx.ai.chat.ChatRestClient$ChatRestClientBuilderFactory

Note the $ separator — this is the JVM convention for nested class names in ServiceLoader registration files. The file content is the fully qualified name of the factory:

io.quarkiverse.langchain4j.watsonx.runtime.client.impl.QuarkusChatRestClient$QuarkusChatRestClientBuilderFactory

Once registered, any ChatService built in that runtime will automatically use your implementation.

Real-world example: Quarkus integration

The quarkus-langChain4j-watsonx integration uses this SPI to replace the default Java HttpClient with Quarkus’s reactive RESTEasy client. This allows the SDK to participate in Quarkus’s managed thread model, reactive pipelines (Mutiny), and GraalVM native compilation. A *BuilderFactory is registered for each service via CDI — the consuming application uses the same ChatService, EmbeddingService, etc. API without any modification.

See quarkus-langchain4j watsonx for the complete reference implementation.


Executor SPI

The SDK uses three distinct executors internally, each replaceable independently via ServiceLoader.

SPI interface Default behavior Used for
CpuExecutorProvider ForkJoinPool.commonPool() CPU-bound tasks: JSON parsing, data transformation
IOExecutorProvider Single-threaded (configurable via WATSONX_IO_EXECUTOR_THREADS) HTTP response processing, SSE stream parsing
CallbackExecutorProvider Virtual threads (Java 21+), cached thread pool (Java 17–20) User callbacks in ChatHandler and TextGenerationHandler

The three executors are intentionally separate to prevent user callback code from blocking the SSE parsing thread, and to keep CPU-bound work off the I/O thread.

The same pattern applies to CpuExecutorProvider and IOExecutorProvider. The executor is loaded once at startup and cached for the lifetime of the JVM.


JSON SPI

By default, the SDK uses Jackson for JSON serialization and deserialization, configured with snake_case property naming and NON_NULL inclusion. The JsonProvider SPI allows replacing this with any other JSON library.

public interface JsonProvider {
    <T> T fromJson(String json, Class<T> clazz);
    <T> T fromJson(String json, TypeToken<T> typeToken);
    String toJson(Object object);
    String prettyPrint(Object object);
    boolean isValidObject(String json);
}

TypeToken<T> is a utility class provided by the SDK to capture generic type information at runtime, used for deserializing parameterized types like List<ChatMessage> or DetectionResponse<DetectionTextResponse>.


Back to top

Copyright 2025 IBM Corporation. Licensed under the Apache License 2.0.

This site uses Just the Docs, a documentation theme for Jekyll.