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:
- Iterates through all entries on the classpath
- Instantiates service provider classes
- 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
- Cache ServiceLoader results at
WorkflowApplication level
- Make cached providers available to task executors
- Index results by type/language for O(1) lookups instead of O(n) filtering
- 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.
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:This becomes a bottleneck when:
WorkflowApplicationinstances are createdCurrent Issues
WorkflowApplication.java (lines 472, 482)
Called during every
build()invocation.RunTaskExecutor.java (line 36)
Then rescanned via
runnables.stream()with.sorted()on every task execution.EmitExecutor.java (line 48)
RunScriptExecutorBuilder.java (line 57)
ServiceLoader lookup happens per task without caching results by language.
DefaultTaskExecutorFactory.java (line 50)
Good example of caching (at initialization), but other locations don't follow this pattern.
Impact
WorkflowApplicationcreationSuggested Fix
WorkflowApplicationlevelExample Solution (Conceptual)
Then pass this through to executors that need service lookups.