Skip to content

Conversation

@donald-pinckney
Copy link

@donald-pinckney donald-pinckney commented Jan 14, 2026

Plugin System for Java Temporal SDK

What was changed

Added a plugin system for the Java Temporal SDK, modeled after the Python SDK's plugin architecture but adapted to Java idioms.

New Files (temporal-sdk/src/main/java/io/temporal/common/plugin/)

  • ClientPlugin.java - Client-side plugin interface
  • WorkerPlugin.java - Worker-side plugin interface
  • PluginBase.java - Convenience base class implementing both
  • SimplePluginBuilder.java - Builder for declarative plugin creation

Modified Files

  • WorkflowServiceStubs.java - Added newServiceStubs(options, plugins) and ClientPluginCallback interface
  • WorkflowClientOptions.java - Added plugins field with builder methods
  • WorkflowClientInternalImpl.java - Applies ClientPlugin.configureClient() during creation
  • WorkerFactory.java - Full plugin lifecycle (configuration, execution, shutdown)

Test Files (temporal-sdk/src/test/java/io/temporal/common/plugin/)

  • PluginTest.java - Core plugin interface tests
  • SimplePluginBuilderTest.java - Builder API tests
  • WorkflowClientOptionsPluginTest.java - Options integration tests

Why?

The plugin system provides a higher-level abstraction over the existing interceptor infrastructure, enabling users to:

  • Modify configuration during client/worker creation
  • Wrap execution lifecycles with setup/teardown logic
  • Auto-propagate plugins from client to worker
  • Bundle multiple customizations (interceptors, context propagators, etc.) into reusable units

Checklist

  1. Closes Plugin support #2626, and tracked in Plugins to support controlling multiple configuration points at once features#652.

  2. How was this tested:

    • Unit tests for all new plugin classes (PluginTest.java, SimplePluginBuilderTest.java, WorkflowClientOptionsPluginTest.java)
    • All 21 plugin tests pass
    • Full build passes
  3. Any docs updates needed?

    • Documentation for the new plugin API should be added to the Java docs

Design Decisions

1. No Base Plugin Interface

Decision: ClientPlugin and WorkerPlugin each define their own getName() method independently, rather than sharing a base Plugin interface.

Rationale: This matches the Python SDK's design. Python has separate ClientPlugin and WorkerPlugin with name() on each. I initially had a base Plugin interface but removed it to simplify.

Alternative considered: I could add a shared Plugin interface with just getName(). This would allow List<Plugin> instead of List<?> for storage. However, this adds an interface that serves no purpose other than type convenience, and Python doesn't have it.

2. ClientPluginCallback Interface (Module Boundary)

Decision: A ClientPluginCallback interface exists in temporal-serviceclient, which ClientPlugin (in temporal-sdk) extends.

Rationale: This is required due to Java's module architecture:

  • temporal-serviceclient contains WorkflowServiceStubs
  • temporal-sdk depends on temporal-serviceclient (not vice versa)
  • WorkflowServiceStubs.newServiceStubs(options, plugins) needs to call plugin methods
  • Since serviceclient cannot import from sdk, I define a minimal callback interface in serviceclient

This is the one structural difference from Python, which uses a single-package architecture where everything can import everything else.

3. PluginBase Convenience Class

Decision: I provide an abstract PluginBase class that implements both ClientPlugin and WorkerPlugin.

Rationale: This is a common Java pattern (like AbstractList for List).

4. SimplePluginBuilder with Private SimplePlugin

Decision: I provide a builder for creating plugins declaratively, with the implementation class kept private.

Rationale:

  • Builder pattern is more natural in Java than Python's constructor with many parameters
  • Private SimplePlugin is an implementation detail - users interact with the builder
  • Allows changing implementation without breaking API
PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin")
    .addWorkerInterceptors(new TracingInterceptor())
    .customizeClient(b -> b.setIdentity("custom"))
    .build();

5. No ServiceLoader Discovery

Decision: I do not include ServiceLoader-based plugin discovery.

Rationale:

  • Python doesn't have this - just uses explicit plugins=[]
  • ServiceLoader requires classes with no-arg constructors, which doesn't integrate with SimplePluginBuilder
  • "Magic" discovery is harder to debug than explicit configuration
  • Explicit plugin configuration is clearer and sufficient

We could consider adding it in though if there is interest.


Example Usage

Custom Plugin

public class TracingPlugin extends PluginBase {
    private final Tracer tracer;

    public TracingPlugin(Tracer tracer) {
        super("my-org.tracing");
        this.tracer = tracer;
    }

    @Override
    public WorkflowClientOptions.Builder configureClient(
            WorkflowClientOptions.Builder builder) {
        return builder.setInterceptors(new TracingClientInterceptor(tracer));
    }

    @Override
    public WorkerFactoryOptions.Builder configureWorkerFactory(
            WorkerFactoryOptions.Builder builder) {
        return builder.setWorkerInterceptors(new TracingWorkerInterceptor(tracer));
    }
}

Using SimplePluginBuilder

PluginBase metricsPlugin = SimplePluginBuilder.newBuilder("my-org.metrics")
    .customizeServiceStubs(b -> b.setMetricsScope(myScope))
    .addWorkerInterceptors(new MetricsInterceptor())
    .build();

Client/Worker with Plugins

WorkflowClientOptions clientOptions = WorkflowClientOptions.newBuilder()
    .setNamespace("default")
    .addPlugin(new TracingPlugin(tracer))
    .addPlugin(metricsPlugin)
    .build();

// Plugins that implement WorkerPlugin auto-propagate to workers
WorkerFactory factory = WorkerFactory.newInstance(client);

Open Questions

  1. Mark as @Experimental? - I've marked all public APIs as @Experimental to allow iteration. Is this appropriate?

@CLAassistant
Copy link

CLAassistant commented Jan 14, 2026

CLA assistant check
All committers have signed the CLA.

@donald-pinckney donald-pinckney marked this pull request as ready for review January 14, 2026 15:22
@donald-pinckney donald-pinckney requested a review from a team as a code owner January 14, 2026 15:22
* @see PluginBase
*/
@Experimental
public interface ClientPlugin extends ClientPluginCallback {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this WorkflowClientPlugin or is it meant to work for ScheduleClient too (and the upcoming standalone activity client and Nexus operation client)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it scoped to just workflows. I think it should be clear now, after I moved it to the appropriate package.

Copy link
Member

@cretz cretz Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that schedule clients can't have plugins? Clients doing schedule things can have plugins in every other SDK (same concern for standalone activity clients coming and Nexus operation clients coming)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah i see, i was missing that in Python the big huge Client also includes scheduling stuff, whereass in java they are separate. I'll take a closer look at that.

*/
@Override
@Nonnull
default WorkflowServiceStubsOptions.Builder configureServiceStubs(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default

Even in places where we could provide default implementations (e.g. Python and Ruby), we intentionally chose not to because we want to force implementers to implement these (even if they choose no-op). Granted in the simple plugin it makes sense to have default implementations.

* Callback interface for client plugins to participate in service stubs creation. This interface
* is implemented by {@code ClientPlugin} in the temporal-sdk module.
*/
interface ClientPluginCallback {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just consider making a ServiceStubPlugin separate from a WorkflowClientPlugin and if someone wants to implement both, they can. We can discuss whether a plugin here automatically propagates to clients if it implements the proper interface (I think it should, same way client plugins should automatically propagate to workers if the they implement the right interfaces).

* @return the workflow service stubs
*/
static WorkflowServiceStubs newServiceStubs(
@Nonnull WorkflowServiceStubsOptions options, @Nonnull List<?> plugins) {
Copy link
Member

@cretz cretz Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should accept the plugins on the options and therefore only mutate existing stub construction methods instead of adding a new one. All other SDK implementations of plugins do (acknowledging that plugins should not mutate the plugin list or it's undefined behavior). This makes it easy for users building options separate from where they call this. It also makes it easy to properly apply to the existing overloads like newConnectedServiceStubs. This was done for client, I think it's worth doing here.


/** Functional interface for the connection chain. */
@FunctionalInterface
interface ServiceStubsSupplier {
Copy link
Member

@cretz cretz Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much benefit to this separate interface vs just changing param type to Supplier<WorkflowServiceStubs> (we don't have WorkerPluginRunnable for worker). I also don't think it should throw a checked exception.

}

/** Internal implementation of the simple plugin. */
private static final class SimplePlugin extends PluginBase {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this needs to be a public concept. People need to be able to use simple plugins and still be able to override maybe one method if they still need to. I think therefore PluginBase does not need to exist, it can be replaced by SimplePlugin and Builder can be an inner class of SimplePlugin (as we do elsewhere). Can still have the (public) constructor accept options though, but I think a public no-arg (or name-only arg) constructor should also be allowed for some use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin support

4 participants