Skip to content

SPI Implementation Guide

Ratchet is designed around a set of Service Provider Interfaces (SPIs) that decouple the core engine from specific implementations. Major extension points -- configuration, invocation resolution, result persistence, resilience, metrics, logging, storage, security, and cluster coordination -- are expressed as SPI interfaces that you can replace with your own implementation.

This guide covers the CDI wiring pattern, the complete SPI inventory, and Ratchet conformance tiers. The compatibility tiers use ratchet-tck-store (store SPI), ratchet-tck-api (public API, container-neutral), and ratchet-tck-jakarta (Jakarta EE conformance via Arquillian), with shared JUnit support in ratchet-tck-util. See Adopting the TCK for the public extension seams, Maven setup, report generation, and the integration work each tier requires.

The CDI @Alternative Pattern

All SPI interfaces in Ratchet have default implementations provided by the reference implementation (RI). To replace a default with your own implementation, use CDI's @Alternative mechanism with @Priority:

java
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.interceptor.Interceptor;

@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class MyCustomSpi implements SomeRatchetSpi {
    // Your implementation
}

How It Works

  1. The RI provides a default bean for each SPI (annotated @ApplicationScoped or produced via @Produces in RatchetProducer).
  2. Your @Alternative bean is discovered by CDI during deployment.
  3. The @Priority(Interceptor.Priority.APPLICATION) (value 2000) ensures your bean takes precedence over the RI default.
  4. CDI injects your implementation everywhere the SPI type is used.

No XML, no configuration files, no service loader entries. Just annotate your class and put it on the classpath.

Priority Ordering

If multiple alternatives exist for the same SPI, the one with the highest @Priority value wins:

Priority ConstantValueTypical Use
Interceptor.Priority.LIBRARY_BEFORE0Library defaults
Interceptor.Priority.APPLICATION2000Application overrides
Interceptor.Priority.APPLICATION + 1002100Override another alternative

Verifying Your Override

After deployment, verify your bean is active by injecting the SPI and checking the concrete type:

java
@Inject
JobInvocationResolver resolver;

// In a startup observer or health check:
log.info("Active JobInvocationResolver: " + resolver.getClass().getName());
// Should print your class, not DefaultJobInvocationResolver

Complete SPI Reference

Ratchet defines SPI interfaces across the API, RI, and store modules. Each entry below shows the interface, its default implementation, and a skeleton for a custom override.

1. JobInvocationResolver

Module: ratchet-apiPackage: run.ratchet.spiDefault: ASM-based callback analysis

Resolves submitted callbacks into persisted job invocations.

java
public interface JobInvocationResolver {
    JobInvocation resolve(Serializable callback);
    JobInvocation resolve(Serializable callback, List<Object> runtimeArguments);
}

Override:

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class AppInvocationResolver implements JobInvocationResolver {

    @Override
    public JobInvocation resolve(Serializable callback) {
        return resolve(callback, List.of());
    }

    @Override
    public JobInvocation resolve(Serializable callback, List<Object> runtimeArguments) {
        return new JobInvocation("com.example.JobTargets", "run", "()V", false, runtimeArguments);
    }
}

See Payload and Result Customization for detailed guidance.


2. RetryPolicy

Module: ratchet-apiPackage: run.ratchet.spiDefault: DefaultRetryPolicy (passthrough -- defers to job-level maxRetries and backoffPolicy)

Controls global retry behavior for failed jobs.

java
public interface RetryPolicy {
    boolean shouldRetry(int attempt, Throwable cause);
    Duration getDelay(int attempt);
}

Override:

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class SmartRetryPolicy implements RetryPolicy {

    @Override
    public boolean shouldRetry(int attempt, Throwable cause) {
        return attempt <= 5 && isTransient(cause);
    }

    @Override
    public Duration getDelay(int attempt) {
        return Duration.ofSeconds(2L * (1L << Math.min(attempt - 1, 8)));
    }

    private boolean isTransient(Throwable t) {
        return t instanceof IOException
            || t instanceof TimeoutException;
    }
}

See Custom Retry Policies for detailed guidance.


3. ResilienceStrategy

Module: ratchet-apiPackage: run.ratchet.spiDefault: DefaultResilienceStrategy (built-in circuit breaker via CircuitBreakerRegistry) Annotation: @Incubating

Wraps job execution with resilience patterns (circuit breakers, bulkheads).

java
@Incubating
public interface ResilienceStrategy {
    <T> T execute(String serviceName, Callable<T> task) throws Exception;
    boolean isServiceAvailable(String serviceName);
    default Duration getRetryDelay(String serviceName) {
        return Duration.ofSeconds(30);
    }
}

Override (Resilience4j):

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class Resilience4jStrategy implements ResilienceStrategy {

    @Inject
    private io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry r4jRegistry;

    @Override
    public <T> T execute(String serviceName, Callable<T> task) throws Exception {
        return r4jRegistry.circuitBreaker(serviceName).executeCallable(task);
    }

    @Override
    public boolean isServiceAvailable(String serviceName) {
        var state = r4jRegistry.circuitBreaker(serviceName).getState();
        return state != io.github.resilience4j.circuitbreaker.CircuitBreaker.State.OPEN;
    }
}

See Circuit Breakers for detailed guidance.


4. MetricsCollector

Module: ratchet-apiPackage: run.ratchet.spiDefault: NoOpMetricsCollector (empty methods) Adapter module: ratchet-micrometer provides MicrometerMetricsCollectorAnnotation: @Incubating

Receives callbacks covering job outcomes, success finalization, claims and submission gates, wakeups and executor routing, callback and signal events, store timing, poller and application circuit breakers, and encryption integrity/version signals. The complete callback and meter catalog is in Metrics Collection.

Partial override:

This abbreviated collector deliberately exports only the three basic job outcomes. Extend NoOpMetricsCollector when a partial view is intentional. A complete replacement should handle or delegate every callback listed in the catalog.

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class DatadogMetricsCollector extends NoOpMetricsCollector {

    @Inject
    private StatsDClient statsd;

    @Override
    public void jobStarted(UUID jobId, JobType type, JobPriority priority) {
        statsd.incrementCounter("ratchet.jobs.started",
            "type:" + type, "priority:" + priority);
    }

    @Override
    public void jobCompleted(UUID jobId, JobType type, long executionTimeMs) {
        statsd.incrementCounter("ratchet.jobs.completed", "type:" + type);
        statsd.recordExecutionTime("ratchet.jobs.duration", executionTimeMs,
            "type:" + type);
    }

    @Override
    public void jobFailed(UUID jobId, JobType type, Throwable cause, int attempt) {
        statsd.incrementCounter("ratchet.jobs.failed",
            "type:" + type, "family:" + ExceptionFamily.classify(cause).name());
    }
}

See Metrics Collection for detailed guidance.


5. JobLogger

Module: ratchet-apiPackage: run.ratchet.spiDefault: Created by DefaultJobLoggerFactory as a per-execution JBossLoggingJobLogger, which bridges to JBoss Logging and publishes JobLogLine events through the internal event publisher. Annotation: @Incubating

Per-job isolated logging.

java
@Incubating
public interface JobLogger {
    void info(String message);
    void debug(String message);
    void warn(String message);
    void error(String message);
    void trace(String message);
}

See Custom Logging for detailed guidance.


6. ClassPolicy

Module: ratchet-apiPackage: run.ratchet.spiDefault: PackagePrefixClassPolicy (empty allowlist by default -- must be configured) Annotation: @Incubating

Controls which classes can be loaded and executed as job targets. It gates which classes the engine will deserialize and run.

java
@Incubating
public interface ClassPolicy {
    boolean isAllowed(String className);
}

Override:

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class AppClassPolicy implements ClassPolicy {

    private static final Set<String> ALLOWED_PREFIXES = Set.of(
        "com.mycompany.app.",
        "com.mycompany.shared."
    );

    @Override
    public boolean isAllowed(String className) {
        if (className == null || className.isEmpty()) {
            return false;
        }
        return ALLOWED_PREFIXES.stream()
            .anyMatch(className::startsWith);
    }
}

7. BeanResolver

Module: ratchet-apiPackage: run.ratchet.spiDefault: CdiBeanResolver (resolves beans via CDI Instance<Object>) Annotation: @Incubating

Resolves bean instances by type, abstracting the DI mechanism.

java
@Incubating
@FunctionalInterface
public interface BeanResolver {
    <T> T resolve(Class<T> type);
}

Override (Spring context):

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class SpringBeanResolver implements BeanResolver {

    private final ApplicationContext springContext;

    @Inject
    public SpringBeanResolver(ApplicationContext springContext) {
        this.springContext = springContext;
    }

    @Override
    public <T> T resolve(Class<T> type) {
        return springContext.getBean(type);
    }
}

8. ExecutorProvider

Module: ratchet-apiPackage: run.ratchet.spiDefault: DefaultExecutorProvider (Jakarta Concurrency managed executors via JNDI) Annotation: @Incubating

Provides thread pools for job execution and scheduling.

java
@Incubating
public interface ExecutorProvider {
    ExecutorService getJobExecutor();
    ScheduledExecutorService getScheduledExecutor();
}

Override (custom virtual thread pool):

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class VirtualThreadExecutorProvider implements ExecutorProvider {

    private final ExecutorService jobExecutor =
        Executors.newVirtualThreadPerTaskExecutor();

    private final ScheduledExecutorService scheduler =
        Executors.newScheduledThreadPool(2, Thread.ofVirtual().factory());

    @Override
    public ExecutorService getJobExecutor() {
        return jobExecutor;
    }

    @Override
    public ScheduledExecutorService getScheduledExecutor() {
        return scheduler;
    }

    @PreDestroy
    void shutdown() {
        jobExecutor.shutdown();
        scheduler.shutdown();
    }
}

9. NodeIdentityProvider

Module: ratchet-apiPackage: run.ratchet.spiDefault: DefaultNodeIdentityProvider (generates a UUID, manages heartbeats) Annotation: @Incubating

Provides the unique node identifier for multi-node deployments.

java
@Incubating
public interface NodeIdentityProvider {
    String getNodeId();
}

Override (hostname-based):

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class HostnameNodeIdentityProvider implements NodeIdentityProvider {

    private final String nodeId;

    public HostnameNodeIdentityProvider() {
        try {
            String hostname = InetAddress.getLocalHost().getHostName();
            String pid = ProcessHandle.current().pid() + "";
            this.nodeId = hostname + "-" + pid;
        } catch (Exception e) {
            this.nodeId = UUID.randomUUID().toString();
        }
    }

    @Override
    public String getNodeId() {
        return nodeId;
    }
}

10. ClusterCoordinator

Module: ratchet-apiPackage: run.ratchet.spiDefault: NoOpClusterCoordinator (single-node no-op) Annotation: @Incubating

Coordinates job scheduling across cluster nodes by broadcasting wakeup signals.

java
@Incubating
public interface ClusterCoordinator extends AutoCloseable {
    void notifyNewWork(JobPriority priority, NodeIdentity source, String executionTarget);
    void registerWakeupListener(Consumer<JobWakeupHint> listener);
    void close();
}

Override (Redis pub/sub):

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class RedisClusterCoordinator implements ClusterCoordinator {

    private static final String CHANNEL = "ratchet:wakeup";

    @Inject
    private RedisClient redis;

    private final List<Consumer<JobWakeupHint>> listeners = new CopyOnWriteArrayList<>();

    @PostConstruct
    void subscribe() {
        redis.subscribe(CHANNEL, hint -> {
            for (Consumer<JobWakeupHint> listener : listeners) {
                listener.accept(hint);
            }
        });
    }

    @Override
    public void notifyNewWork(JobPriority priority, NodeIdentity source, String executionTarget) {
        redis.publish(CHANNEL, priority.name());
    }

    @Override
    public void registerWakeupListener(Consumer<JobWakeupHint> listener) {
        listeners.add(listener);
    }

    @Override
    public void close() {
        redis.close();
    }
}

11. StartupCoordinator

Module: ratchet-apiPackage: run.ratchet.spiDefault: StoreBackedStartupCoordinator (store-backed lease) Annotation: @Incubating

Coordinates destructive startup work using a lease rather than an external leader-election system.

java
@Incubating
public interface StartupCoordinator {
    boolean tryAcquire(String actionName, Duration leaseTtl);
    void release(String actionName);
}

12. JobLoggerFactory

Module: ratchet-apiPackage: run.ratchet.spiDefault: DefaultJobLoggerFactoryAnnotation: @Incubating

Creates the job-scoped logger bound into JobContext.

java
@Incubating
public interface JobLoggerFactory {
    JobLogger create(JobLoggerContext context);
}

13. ErrorSanitizer

Module: ratchet-apiPackage: run.ratchet.spiDefault: DefaultErrorSanitizer (strips JDBC URLs, credentials, emails, truncates to 2000 chars) Annotation: @Incubating

Sanitizes exception messages before they are persisted to the job store or published in events.

java
@Incubating
public interface ErrorSanitizer {
    String sanitize(Throwable ex);
}

Override (custom PII patterns):

java
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class StrictErrorSanitizer implements ErrorSanitizer {

    private static final Pattern SSN =
        Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b");
    private static final Pattern CREDIT_CARD =
        Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b");
    private static final int MAX_LENGTH = 300;

    @Override
    public String sanitize(Throwable ex) {
        if (ex == null) return "null";

        String className = ex.getClass().getName();
        String message = ex.getMessage();
        if (message == null) return className;

        String sanitized = message;
        sanitized = SSN.matcher(sanitized).replaceAll("***SSN***");
        sanitized = CREDIT_CARD.matcher(sanitized).replaceAll("***CC***");

        String result = className + ": " + sanitized;
        if (result.length() > MAX_LENGTH) {
            result = result.substring(0, MAX_LENGTH - 3) + "...";
        }
        return result;
    }
}

The default implementation already handles JDBC URLs with embedded credentials, URLs with userinfo, email patterns, and common credential key-value patterns (password=..., token=..., etc.).


14. LambdaDescriptor

Module: ratchet-apiPackage: run.ratchet.spi

This is a record (not a replaceable SPI) that describes the result of lambda analysis. It is included here for completeness:

java
@Incubating
public record LambdaDescriptor(
    String targetClass,      // Fully qualified class name
    String methodName,       // Method name
    String methodDescriptor, // JVM method descriptor
    boolean isStatic,        // Whether the method is static
    Object[] capturedArgs    // Arguments captured from the lambda closure
) { }

Store SPI: Custom Persistence

The store layer is the largest SPI surface in Ratchet. The mandatory JobStore interface composes only the persistence concerns every conforming store must provide. Capabilities a store may legitimately lack are advertised separately and discovered through a runtime probe, so a minimal backend implements the core and nothing more:

java
public interface JobStore
    extends JobCrudStore,        // Basic CRUD for job entities
            JobClaimStore,       // Atomic job claiming for execution
            JobTerminalStore,    // Terminal success/failure/cancel transitions
            JobRetryStore,       // Retry scheduling
            JobPauseStore,       // Pause/resume transitions
            JobBatchStatusStore, // Non-terminal status and batch/orphan operations
            JobBulkStore,        // Bulk operations (orphan recovery, cleanup)
            NodeStore,           // Node registration, heartbeat, crash recovery
            TagStore             // Job tag writes
{
    // Probe for an optional capability this store may also implement. The default reflects Java
    // type membership: a store advertises a capability simply by implementing its interface.
    default <T> Optional<T> capability(Class<T> type) {
        return type.isInstance(this) ? Optional.of(type.cast(this)) : Optional.empty();
    }
}

A store opts into an optional capability by additionally implementing its interface. Callers never assume a capability is present; they probe for it, and the engine disables the dependent feature when it is absent:

java
jobStore.capability(SignalStore.class)
    .ifPresent(signals -> signals.deliverSignalByKey(key, payload));

Ratchet ships MySQL, PostgreSQL, Oracle, SQL Server, and MongoDB implementations, all of which advertise every capability. To implement a custom store (for example DynamoDB, Redis, or an in-memory test backend), implement the core JobStore, plus any optional capabilities your backend can support, and validate it against the TCK.

Core Store Interfaces (mandatory)

InterfaceResponsibilityKey Methods
JobCrudStoreCreate, read, update, delete jobssave(), findById(), delete()
JobClaimStoreAtomic job claiming for executionclaimNextBatch(), claimNextBatchOptimized()
JobTerminalStoreTerminal success, failure, and cancellation transitionsmarkJobSucceeded(), markJobFailedTerminal(), cancelJob()
JobRetryStoreRetry scheduling and attempt-state updatesscheduleJobRetry(), incrementRetryAttempt()
JobPauseStorePause and resume transitionstransitionToPaused(), transitionFromPausedAtomic()
JobBatchStatusStoreNon-terminal status, pickup, and orphan operationsupdateJobStatus(), compareAndSwapStatus(), resetRunningJobs()
JobBulkStoreBulk operationsbulkInsert(), resetOrphanJobs(), deleteDlqOlderThan()
NodeStoreNode registration, heartbeat, and crash recoveryupsertHeartbeat(), findInactiveNodesSince(), findAllNodes()
TagStoreJob tag writesinsertTags(), deleteTagsByJobId()

Optional Capabilities

A store advertises each of these by implementing the interface; the engine probes with capability() and disables the feature when the capability is absent.

CapabilityResponsibilityKey Methods
RecurringJobStoreRecurring-master persistenceclaimDueRecurring(), advanceNextFire(), cancelRecurringAndArchive()
BatchStoreBatch progress tracking and metricssaveBatch(), incrementCompletedAtomic(), markBatchCompleteIfReady()
WorkflowConditionStoreWorkflow branch conditionssaveCondition(), findConditionsByParentJobId()
SignalStoreSignal-waiting jobsdeliverSignalById(), deliverSignalByKey(), findTimedOutSignalJobs()
ResourcePermitStoreResource permitstryAcquirePermit(), releasePermit()
LockStoreDistributed lockstryLock(), unlock(), renewLock()
ArchiveStoreJob archivingarchiveJob(), findArchivedJobs()
JobQueryStoreRead-only admin/query projections and tag lookupssearchJobs(), countJobs(), findJobIdsByTag()
JobAnalyticsStoreAggregate counts, rates, and percentilescountJobsByStatus(), getQueueWaitTimePercentile()
JobAuditStoreExecution history and per-job logssaveExecution(), findExecutionsByJobId(), appendLog()
JobExtensionStoreIndexed job properties and per-namespace extension stateputProperty(), getPropertiesByPrefix(), initState(), updateState()

LockStore is a best-effort lease

LockStore coordinates cluster work with expiring leases. It is not a strict-exclusive lock and it does not issue fencing tokens. Every implementation and caller must preserve these rules:

  • Acquisition for one lease name is atomic. While a lease is live, at most one racing caller may observe success. An expired lease must be reclaimable without cooperation from the old holder.
  • unlock() must verify ownership and must be a no-op for a stale or different owner.
  • renewLock() must verify ownership atomically with the expiry update. A false result means the caller has lost the lease and must stop the protected work.
  • A previous holder can still commit after its lease expires and another node acquires it. Callers must therefore make protected writes idempotent under overlap with a later holder.
  • Implementations should derive expiry from the store's server-side clock, not a client clock that can drift, and must be thread-safe.

AbstractLockStoreContract verifies acquisition, ownership, renewal, and expiry behavior. It does not turn the lease into a fencing primitive; applications that require stale-writer exclusion need a monotonic token checked atomically by every protected write, which is outside this SPI.

Transaction boundaries are part of the store contract

Persistence methods on the mandatory and optional store interfaces state a transaction attribute in their Javadoc. Do not infer the boundary from the method name:

AttributeImplementor obligation
SUPPORTSA read may join an existing transaction and must not require a new write transaction. A concrete provider may use a stronger boundary when needed for safe connection lifecycle, provided the observable read contract is preserved.
REQUIREDThe mutation and all related statements must be atomic. It joins a caller transaction when the persistence model supports that, or starts the store's equivalent unit of work.

A separate JPA conformance profile strengthens six cluster-liveness methods. A JPA store must have a class-level @Transactional(REQUIRED) default and put @Transactional(REQUIRES_NEW) on:

  • tryLock, unlock, and renewLock;
  • upsertHeartbeat;
  • deleteInactiveNodesSince; and
  • deleteInactiveNodesByIds.

For a JPA-backed store, this concrete profile is the requirement to follow even though the general SPI descriptions classify these mutations as REQUIRED.

Those operations must commit independently so another node sees lease and heartbeat changes as soon as the call returns, and an outer rollback cannot undo liveness state. The conditional AbstractJobStoreTransactionBoundaryContract checks the annotations for JPA stores. A document store that provides the same independent-commit behavior through one atomic operation is exempt from that annotation contract.

Implementing a Custom Store

java
import run.ratchet.store.spi.JobStore;
import run.ratchet.store.entity.JobEntity;
import run.ratchet.store.entity.BatchEntity;
import run.ratchet.store.id.UuidV7Factory;
// ... other entity imports

import java.util.UUID;

import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.interceptor.Interceptor;

@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class CustomDocumentJobStore implements JobStore {

    @Inject
    private MongoDatabase database;

    // --- JobCrudStore ---

    @Override
    public JobEntity save(JobEntity job) {
        MongoCollection<Document> collection = database.getCollection("ratchet_jobs");
        if (job.getId() == null) {
            job.setId(UuidV7Factory.create());
            Document doc = toDocument(job);
            collection.insertOne(doc);
        } else {
            Document doc = toDocument(job);
            collection.replaceOne(eq("_id", job.getId()), doc);
        }
        return job;
    }

    @Override
    public Optional<JobEntity> findById(UUID id) {
        Document doc = database.getCollection("ratchet_jobs")
            .find(eq("_id", id))
            .first();
        return Optional.ofNullable(doc).map(this::toJobEntity);
    }

    // --- JobClaimStore ---

    @Override
    public List<JobEntity> claimNextBatch(int limit, String nodeId) {
        // Use MongoDB findOneAndUpdate with atomic status transition
        // PENDING → RUNNING, set ownedBy = nodeId
        List<JobEntity> claimed = new ArrayList<>();
        for (int i = 0; i < limit; i++) {
            Document doc = database.getCollection("ratchet_jobs")
                .findOneAndUpdate(
                    and(eq("status", "PENDING"),
                        lte("scheduledAt", Instant.now())),
                    combine(
                        set("status", "RUNNING"),
                        set("ownedBy", nodeId),
                        set("startedAt", Instant.now())),
                    new FindOneAndUpdateOptions()
                        .sort(ascending("priority", "scheduledAt"))
                        .returnDocument(ReturnDocument.AFTER));
            if (doc == null) break;
            claimed.add(toJobEntity(doc));
        }
        return claimed;
    }

    // ... implement the remaining core JobStore methods. Add an optional capability only when your
    // backend supports it — e.g. `implements JobStore, SignalStore` — and the engine will probe
    // for it through capability(); leave it off and the dependent feature stays disabled.
}

Validating with the TCK

The published store SPI Technology Compatibility Kit (TCK) provides abstract test contracts for the core surface and for each optional capability. A capability contract is conditional: it runs against a store that advertises the capability and is reported N/A against one that does not, so a core-only store stays conformant. Each contract base implements JobStoreContractFixture, so to validate your custom store you extend the contract and supply the fixture methods (store(), newPendingJob(), newBatchParentJob(), cleanupStore()):

java
import run.ratchet.tck.store.JobStoreContractFixture;
import run.ratchet.tck.store.AbstractJobCrudStoreContract;
import run.ratchet.store.entity.JobEntity;
import run.ratchet.store.spi.JobStore;

// 1. Implement the fixture
public class MongoStoreFixture implements JobStoreContractFixture {

    private final CustomDocumentJobStore store;

    public MongoStoreFixture(MongoDatabase database) {
        this.store = new CustomDocumentJobStore(database);
    }

    @Override
    public JobStore store() {
        return store;
    }

    @Override
    public JobEntity newPendingJob() {
        JobEntity job = new JobEntity();
        job.setTags(List.of("test-" + UUID.randomUUID()));
        job.setStatus(JobStatus.PENDING);
        // ... set required fields
        return job;
    }

    @Override
    public JobEntity newBatchParentJob() {
        JobEntity job = newPendingJob();
        job.setJobType(JobExecutionType.BATCH_PARENT);
        return job;
    }

    @Override
    public void cleanupStore() {
        // Drop test collections or delete test data
    }
}

// 2. Extend TCK contracts. AbstractJobCrudStoreContract implements
// JobStoreContractFixture, so the test class supplies the fixture methods directly,
// delegating to the helper above.
class MongoJobCrudStoreTest extends AbstractJobCrudStoreContract {

    private final MongoStoreFixture fixture = new MongoStoreFixture(testDatabase);

    @AfterEach
    void cleanup() {
        fixture.cleanupStore();
    }

    @Override
    public JobStore store() {
        return fixture.store();
    }

    @Override
    public JobEntity newPendingJob() {
        return fixture.newPendingJob();
    }

    @Override
    public JobEntity newBatchParentJob() {
        return fixture.newBatchParentJob();
    }

    @Override
    public void cleanupStore() {
        fixture.cleanupStore();
    }
}

The TCK includes abstract contracts for each store sub-interface:

TCK ContractTests
AbstractJobCrudStoreContractsave, find, update, delete operations
AbstractJobClaimStoreContractAtomic claiming, concurrent claim safety
AbstractJobTerminalStoreContractTerminal success, failure, and cancellation transitions
AbstractJobRetryStoreContractRetry scheduling
AbstractJobPauseStoreContractPause and resume transitions
AbstractJobBatchStatusStoreContractNon-terminal status and batch/orphan operations
AbstractJobBulkStoreContractBulk recovery, stale job detection
AbstractBatchStoreContractBatch progress tracking
AbstractLockStoreContractLock acquire, release, expiry
AbstractNodeStoreContractNode registration, heartbeat, dead node detection
AbstractArchiveStoreContractJob archiving and retrieval
AbstractJobAuditStoreContractExecution history + per-job log persistence
AbstractJobAnalyticsStoreContractAnalytics/aggregation queries
AbstractTagStoreContractTag-based job queries
AbstractWorkflowConditionStoreContractWorkflow condition evaluation
AbstractResourcePermitStoreContractPermit acquire and release
AbstractDualWriteInvariantContractCross-store invariants for dual hot/cold write paths

Run every required contract and every conditional contract that applies to the store. The generated report must have no failed or missing required contract before the store earns the "Ratchet Store Compatible" label. API and Jakarta-runtime compatibility are separate tiers; follow Adopting the TCK for their runtime bridge and deployment seams.

Adding the TCK Dependency

xml
<dependency>
    <groupId>run.ratchet</groupId>
    <artifactId>ratchet-tck-store</artifactId>
    <version>0.3.1</version>
    <scope>test</scope>
</dependency>

Implementation Checklist

When implementing any SPI:

  • [ ] Thread safety -- All SPI implementations are called from multiple threads concurrently. Use @ApplicationScoped (one instance, must be thread-safe) or ensure your instance handles concurrent access.

  • [ ] CDI proxy compatibility -- If your implementation is @ApplicationScoped, include a protected no-arg constructor for the CDI proxy:

java
@ApplicationScoped
public class MySpi implements SomeRatchetSpi {

    // Required by CDI proxy
    protected MySpi() {
        this.dependency = null;
    }

    @Inject
    public MySpi(SomeDependency dependency) {
        this.dependency = dependency;
    }
}
  • [ ] Null safety -- Check the Javadoc for null contracts. Most SPI methods have non-null parameters, but exceptions may have null messages.

  • [ ] Exception handling -- SPI methods should throw the documented exception types. Unexpected exceptions may cause the engine to fail jobs rather than retrying.

  • [ ] Lifecycle -- Use @PostConstruct for initialization and @PreDestroy for cleanup (closing connections, shutting down thread pools).

  • [ ] Testing -- Unit test your implementation in isolation, then integration test it within a CDI container to verify wiring.

Quick Reference: SPI to Default Mapping

SPI InterfaceDefault ImplementationCDI ScopeModule
RatchetConfigSourceOverlay for RatchetOptionsFactory.fromEnvironment(...) when the application's own config platform fronts env vars / MP ConfigOptional @ApplicationScoped application beanapplication
JobInvocationResolverDefaultJobInvocationResolver@ApplicationScopedratchet
ResultPersistenceStrategyDefaultResultPersistenceStrategy@ApplicationScopedratchet
ExecutionTuningProviderDefaultExecutionTuningProvider@ApplicationScopedratchet
PollingStrategyProviderDefaultPollingStrategyProvider@ApplicationScopedratchet
CircuitBreakerConfigProviderDefaultCircuitBreakerConfigProvider@ApplicationScopedratchet
SchedulerLifecycleHookNo default hookOptional @ApplicationScoped alternativeapplication
RetryPolicyDefaultRetryPolicy@ApplicationScopedratchet
ResilienceStrategyDefaultResilienceStrategyProduced by RatchetProducerratchet
MetricsCollectorNoOpMetricsCollector@ApplicationScopedratchet-api
JobLoggerFactoryDefaultJobLoggerFactory@ApplicationScopedratchet
StartupCoordinatorStoreBackedStartupCoordinator@ApplicationScopedratchet
ClassPolicyPackagePrefixClassPolicyProduced by RatchetProducerratchet
BeanResolverCdiBeanResolver@ApplicationScopedratchet
ExecutorProviderDefaultExecutorProvider@ApplicationScopedratchet
NodeIdentityProviderDefaultNodeIdentityProviderProduced by RatchetProducerratchet
ClusterCoordinatorNoOpClusterCoordinator@ApplicationScopedratchet
ErrorSanitizerDefaultErrorSanitizerProduced by RatchetProducerratchet
JobStoreMySQL / PostgreSQL / Oracle / SQL Server / MongoDB@ApplicationScopedratchet-store-*