Interface ExecutorProvider


@Experimental public interface ExecutorProvider
SPI for supplying the Executor that LangChain4j uses to offload blocking work and run its asynchronous continuations — concurrent tool execution, blocking RAG / retrieval and embedding-store calls, moderation, in-process embedding models, retry backoff scheduling, and similar.

Why implement this

LangChain4j hops threads at these offload points and does not propagate thread-local context (tracing spans, SLF4J MDC, security, CDI / request scope, …) across them by default. Registering an ExecutorProvider that returns a context-propagating executor makes that context follow the work. Typical wrappers:
  • Quarkus / MicroProfile — a ManagedExecutor
  • Spring — an Executor wrapped with a TaskDecorator, or a ContextExecutorService
  • OpenTelemetry — Context.taskWrapping(executor)
  • Micrometer — ContextSnapshot.wrap(executor)

Executor, not ExecutorService

The return type is the minimal Executor: LangChain4j only needs to run tasks and never owns the executor's lifecycle (it will not call shutdown()). This accepts the widest range of host executors — every managed / context-propagating pool (Quarkus ManagedExecutor, Jakarta ManagedExecutorService, Guava ListeningExecutorService, a Spring TaskExecutor, …) is an Executor. Every offload point is driven through this single Executor; components that need a Future handle (to cancel or time-bound a task) obtain it via CompletableFuture.supplyAsync(..., executor) rather than requiring an ExecutorService.

Discovery and precedence

A provider is registered either by implementing this interface and declaring it for ServiceLoader (the standard way; frameworks typically do this for you), or programmatically via set(ExecutorProvider) (a convenience for tests and non-DI applications). The effective executor is resolved as:
  1. a provider set programmatically via set(ExecutorProvider);
  2. the first ExecutorProvider found on the classpath via ServiceLoader;
  3. a built-in default: a virtual-thread-per-task executor on Java 21 or later, falling back to an unbounded platform-thread pool on Java 17-20.
Component-level executors (a retriever's, an AI Service's concurrent-tool executor, a transport's own executor(...) builder option, …) take precedence over this global default when explicitly set.

Contract

executor() is called at each offload, so it must return a shared, long-lived executor rather than create a new one per call.

This SPI intentionally exposes a single, global executor. Should per-purpose executors (e.g. separate pools for CPU-bound vs. blocking work) ever be needed, they can be added here as default methods that fall back to executor(), without breaking existing implementations.

Since:
1.20.0
  • Method Summary

    Modifier and Type
    Method
    Description
     
    get()
     
    static void
    Registers a process-wide ExecutorProvider programmatically, taking precedence over any ServiceLoader-discovered provider and the built-in default.
  • Method Details

    • executor

      Executor executor()
      Returns:
      the shared Executor LangChain4j should offload blocking work and asynchronous continuations onto. Must not be null.
    • set

      static void set(ExecutorProvider provider)
      Registers a process-wide ExecutorProvider programmatically, taking precedence over any ServiceLoader-discovered provider and the built-in default. This is a convenience for tests and non-DI applications; framework integrations typically register via ServiceLoader instead.

      Example — make OpenTelemetry spans and SLF4J MDC follow every LangChain4j offload:

      ExecutorService base = Executors.newVirtualThreadPerTaskExecutor();
      Executor contextAware = Context.taskWrapping(base); // OpenTelemetry
      ExecutorProvider.set(() -> contextAware);
      
      Parameters:
      provider - the provider to register, or null to clear a previously-set one (falling back to the ServiceLoader provider, then the built-in default).
      Since:
      1.20.0
    • get

      static ExecutorProvider get()
      Returns:
      the ExecutorProvider previously registered via set(ExecutorProvider), or null if none was set (in which case a ServiceLoader provider or the built-in default is in effect). Does not return the ServiceLoader-discovered provider.
      Since:
      1.20.0