Version 5.3 Released!

We’re pleased to announce the release of Java Operator SDK v5.3.0! This minor version brings two headline features — read-cache-after-write consistency and a new metrics implementation — along with a configuration adapter system, MDC improvements, and a number of smaller improvements and cleanups.

Key Features

Read-cache-after-write Consistency and Event Filtering

This is the headline feature of 5.3. Informer caches are inherently eventually consistent: after your reconciler updates a resource, there is a window of time before the change is visible in the cache. This can cause subtle bugs, particularly when storing allocated values in the status sub-resource and reading them back in the next reconciliation.

From 5.3.0, the framework provides two guarantees when you use ResourceOperations (accessible from Context):

  1. Read-after-write: Reading from the cache after your update — even within the same reconciliation — returns at least the version of the resource from your update response.
  2. Event filtering: Events produced by your own writes no longer trigger a redundant reconciliation.

UpdateControl and ErrorStatusUpdateControl use this automatically. Secondary resources benefit via context.resourceOperations():

public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {

    ConfigMap managedConfigMap = prepareConfigMap(webPage);
    // update is cached and will suppress the resulting event
    context.resourceOperations().serverSideApply(managedConfigMap);

    // fresh resource instantly available from the cache
    var upToDateResource = context.getSecondaryResource(ConfigMap.class);

    makeStatusChanges(webPage);
    // UpdateControl also uses this by default
    return UpdateControl.patchStatus(webPage);
}

If your reconciler relied on being re-triggered by its own writes, a new reschedule() method on UpdateControl lets you explicitly request an immediate re-queue.

Note: InformerEventSource.list(..) bypasses the additional caches and will not reflect in-flight updates. Use context.getSecondaryResources(..) or InformerEventSource.get(ResourceID) instead.

See the related blog post and reconciler docs for details.

MicrometerMetricsV2

A new micrometer-based Metrics implementation designed with low cardinality in mind. All meters are scoped to the controller, not to individual resources, avoiding unbounded cardinality growth as resources come and go.

MeterRegistry registry; // initialize your registry
Metrics metrics = MicrometerMetricsV2.newBuilder(registry).build();
Operator operator = new Operator(client, o -> o.withMetrics(metrics));

Optionally attach a namespace tag to per-reconciliation counters (disabled by default):

Metrics metrics = MicrometerMetricsV2.newBuilder(registry)
        .withNamespaceAsTag()
        .build();

The full list of meters:

MeterTypeDescription
reconciliations.activegaugeReconciler executions currently running
reconciliations.queuegaugeResources queued for reconciliation
custom_resourcesgaugeResources tracked by the controller
reconciliations.execution.durationtimerExecution duration with explicit histogram buckets
reconciliations.started.totalcounterReconciliations started
reconciliations.success.totalcounterSuccessful reconciliations
reconciliations.failure.totalcounterFailed reconciliations
reconciliations.retries.totalcounterRetry attempts
events.receivedcounterKubernetes events received

The execution timer uses explicit bucket boundaries (10ms–30s) to ensure compatibility with histogram_quantile() in both PrometheusMeterRegistry and OtlpMeterRegistry.

A ready-to-use Grafana dashboard is included at observability/josdk-operator-metrics-dashboard.json.

The metrics-processing sample operator provides a complete end-to-end setup with Prometheus, Grafana, and an OpenTelemetry Collector, installable via observability/install-observability.sh. This is a good starting point for verifying metrics in a real cluster.

Deprecated: The original MicrometerMetrics (V1) is deprecated as of 5.3.0. It attaches resource-specific metadata as tags to every meter, causing unbounded cardinality. Migrate to MicrometerMetricsV2.

See the observability docs for the full reference.

Configuration Adapters

A new ConfigLoader bridges any key-value configuration source to the JOSDK operator and controller configuration APIs. This lets you drive operator behaviour from environment variables, system properties, YAML files, or any config library without writing glue code by hand.

The default instance stacks environment variables over system properties out of the box:

Operator operator = new Operator(ConfigLoader.getDefault().applyConfigs());

Built-in providers: EnvVarConfigProvider, PropertiesConfigProvider, YamlConfigProvider, and AggregatePriorityListConfigProvider for explicit priority ordering.

ConfigProvider is a single-method interface, so adapting any config library (MicroProfile Config, SmallRye Config, etc.) takes only a few lines:

public class SmallRyeConfigProvider implements ConfigProvider {
    private final SmallRyeConfig config;

    @Override
    public <T> Optional<T> getValue(String key, Class<T> type) {
        return config.getOptionalValue(key, type);
    }
}

Pass the results when constructing the operator and registering reconcilers:

var configLoader = new ConfigLoader(new SmallRyeConfigProvider(smallRyeConfig));

Operator operator = new Operator(configLoader.applyConfigs());
operator.register(new MyReconciler(), configLoader.applyControllerConfigs(MyReconciler.NAME));

See the configuration docs for the full list of supported keys.

Note: This new configuration mechanism is useful when using the SDK by itself. Framework (Spring Boot, Quarkus, …) integrations usually provide their own configuration mechanisms that should be used instead of this new mechanism.

MDC Improvements

MDC in workflow execution: MDC context is now propagated through workflow (dependent resource graph) execution threads, not just the top-level reconciler thread. Logging from dependent resources now carries the same contextual fields as the primary reconciliation.

NO_NAMESPACE for cluster-scoped resources: Instead of omitting the resource.namespace MDC key for cluster-scoped resources, the framework now emits MDCUtils.NO_NAMESPACE. This makes log queries for cluster-scoped resources reliable.

De-duplicated Secondary Resources from Context

When multiple event sources manage the same resource type, context.getSecondaryResources(..) now returns a de-duplicated stream. When the same resource appears from more than one source, only the copy with the highest resource version is returned.

Record Desired State in Context

Dependent resources now record their desired state in the Context during reconciliation. This allows reconcilers and downstream dependents in a workflow to inspect what a dependent resource computed as its desired state and guarantees that the desired state is computed only once per reconciliation.

Informer Health Checks

Informer health checks no longer rely on isWatching. For readiness and startup probes, you should primarily use hasSynced. Once an informer has started, isWatching is not suitable for liveness checks.

Additional Improvements

  • Annotation removal using locking: Finalizer and annotation management no longer uses createOrReplace; a locking-based createOrUpdate avoids conflicts under concurrent updates.
  • KubernetesDependentResource uses ResourceOperations directly, removing an indirection layer and automatically benefiting from the read-after-write guarantees.
  • Skip namespace deletion in JUnit extension: The JUnit extension now supports a flag to skip namespace deletion after a test run, useful for debugging CI failures.
  • ManagedInformerEventSource.getCachedValue() deprecated: Use context.getSecondaryResource(..) instead.
  • Improved event filtering for multiple parallel updates: The filtering algorithm now handles cases where multiple parallel updates are in flight for the same resource.
  • exitOnStopLeading is being prepared for removal from the public API.

Migration Notes

JUnit module rename

<!-- before -->
<artifactId>operator-framework-junit-5</artifactId>
<!-- after -->
<artifactId>operator-framework-junit</artifactId>

Metrics interface renames

v5.2v5.3
reconcileCustomResourcereconciliationSubmitted
reconciliationExecutionStartedreconciliationStarted
reconciliationExecutionFinishedreconciliationSucceeded
failedReconciliationreconciliationFailed
finishedReconciliationreconciliationFinished
cleanupDoneForcleanupDone
receivedEventeventReceived

reconciliationFinished(..) is extended with RetryInfo. monitorSizeOf(..) is removed.

See the full migration guide for details.

Getting Started

<dependency>
    <groupId>io.javaoperatorsdk</groupId>
    <artifactId>operator-framework</artifactId>
    <version>5.3.0</version>
</dependency>

All Changes

See the comparison view for the full list of changes.

Feedback

Please report issues or suggest improvements on our GitHub repository.

Happy operator building! 🚀