Skip to content

Performance: Repeated ServiceLoader scanning without caching #1611

Description

@jouzi5

Problem

ServiceLoader instances are being loaded and scanned repeatedly during WorkflowApplication.build() and other initialization paths without caching the results. ServiceLoader scanning is expensive because it:

  1. Iterates through all entries on the classpath
  2. Instantiates service provider classes
  3. Maintains internal state that's recreated each scan

This becomes a bottleneck when:

  • Multiple WorkflowApplication instances are created
  • Services are looked up frequently during workflow execution
  • Large numbers of plugins/implementations exist on the classpath

Current Issues

WorkflowApplication.java (lines 472, 482)

ServiceLoader.load(ExpressionFactory.class).forEach(exprFactories::add);
// ... later ...
ServiceLoader.load(EventPublisher.class).forEach(e -> eventPublishers.add(e));

Called during every build() invocation.

RunTaskExecutor.java (line 36)

private static final ServiceLoader<RunnableTaskBuilder> runnables =
    ServiceLoader.load(RunnableTaskBuilder.class);

Then rescanned via runnables.stream() with .sorted() on every task execution.

EmitExecutor.java (line 48)

private static final Collection<EmittedEventDecorator> emittedDecorators =
    ServiceLoader.load(EmittedEventDecorator.class).stream()
        .map(ServiceLoader.Provider::get)
        .sorted()
        .toList();

RunScriptExecutorBuilder.java (line 57)

ServiceLoader lookup happens per task without caching results by language.

DefaultTaskExecutorFactory.java (line 50)

private Collection<CallableTaskBuilder> callTasks =
    ServiceLoader.load(CallableTaskBuilder.class)
        .stream()
        .map(Provider::get)
        .sorted()
        .toList();

Good example of caching (at initialization), but other locations don't follow this pattern.

Impact

  • Startup time: Multiple milliseconds added per WorkflowApplication creation
  • Throughput: Each task execution incurs sorting/filtering overhead
  • Resource usage: Unnecessary object allocations and classpath scanning

Suggested Fix

  1. Cache ServiceLoader results at WorkflowApplication level
  2. Make cached providers available to task executors
  3. Index results by type/language for O(1) lookups instead of O(n) filtering
  4. Document caching pattern for future service implementations

Example Solution (Conceptual)

// In WorkflowApplication
private final Map<Class<?>, Collection<?>> serviceLoaderCache = new ConcurrentHashMap<>();

<T> Collection<T> loadServices(Class<T> serviceClass) {
    return (Collection<T>) serviceLoaderCache.computeIfAbsent(
        serviceClass,
        k -> ServiceLoader.load(serviceClass).stream()
            .map(ServiceLoader.Provider::get)
            .sorted()
            .toList()
    );
}

Then pass this through to executors that need service lookups.

Activity

  1. fjtirado commented on Aug 11, 2026

    @fjtirado
    Collaborator

    There should not be multiple WorkflowApplication objects within the same JVM
    If you follow that policy (which must be followed), not caching the service loader in WorkflowApplication should not be an issue (so there is no performance improvement in caching services that are only processed during WorkflowApplicaiton.build(), but there is an impact in memory because those collection will remain associated to the application, so, on summary, do NOT create several WorkflowApplication object in your JVM and do not create cached collection for service loaders used in Workflowapplication)
    For other usages of uncached service loaders outside workflow application (which I think are only Run and Emit cases) please feel free to open PR.

  2. fjtirado commented on Aug 11, 2026

    @fjtirado
    Collaborator

    I did further analysis,
    WorkflowApplication, as already discussed, should not be cached (one of the only valid reasons to load different Workflowapplication withint the same JVM is actually to rescan application service loaders)
    EmitExecutor is already "cached", so it only loaded once, no cache necessary (yes this actually creates an assimetry with WorkflwApplication ones, but it can be justified by the nature of the decorator)
    RunScriptExecutor, RunTaskExecutor and HttpExecutor ones are only loaded once per workflow definition (not per execution, thats a critical point) containing a script, but they are good candidates for cachig (caching makes a lot of sense)
    Im opening a PR for those three.

  3. added 2 commits that reference this issue on Aug 11, 2026
    2a4b372
    15e8e87
  4. changed the issue type fromtoon Aug 11, 2026
  5. added 2 commits that reference this issue on Aug 11, 2026
    57cfa43
    31e2bb2
  6. added a commit that references this issue on Aug 12, 2026
    abffe00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

javaPull requests that update java code

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions