Skip to content

Implementing a Provider

This guide walks through creating a new provider that implements a capability contract.

Step 1: Choose a Capability

Identify which capability your provider implements. See the Capabilities Overview for the full list.

Step 2: Implement the Contract

Every provider must implement BaseProviderContract plus the specific capability contract:

package io.patternops.providers.mydb;

import io.patternops.capabilities.state.StateManagementContract;
import io.patternops.core.contract.BaseProviderContract;
import io.patternops.core.model.MetricsEnvelope;

import java.util.List;
import java.util.Map;

public class MyDatabaseStateProvider implements StateManagementContract {

    private ConnectionPool pool;

    // === Base Provider Contract ===

    @Override
    public ValidationResult validateConfig(Map<String, Object> config) {
        List<String> violations = new ArrayList<>();

        if (!config.containsKey("connectionUrl")) {
            violations.add("connectionUrl is required");
        }
        if (!config.containsKey("poolSize")) {
            violations.add("poolSize is required");
        }

        return violations.isEmpty() 
            ? ValidationResult.success() 
            : ValidationResult.failure(violations);
    }

    @Override
    public void initialise(Map<String, Object> config) {
        String url = (String) config.get("connectionUrl");
        int poolSize = (int) config.get("poolSize");
        this.pool = ConnectionPool.create(url, poolSize);
    }

    @Override
    public HealthStatus healthCheck() {
        if (pool.isHealthy()) {
            return HealthStatus.healthy();
        }
        return HealthStatus.degraded("Connection pool degraded");
    }

    @Override
    public List<MetricsEnvelope> metrics() {
        // Return current provider metrics
        return List.of(/* pool size, active connections, etc. */);
    }

    @Override
    public void cleanup() {
        pool.close();
    }

    // === State Management Contract ===

    @Override
    public void write(String key, Object state) {
        pool.execute("INSERT INTO state (key, value) VALUES (?, ?) ON CONFLICT UPDATE", 
                     key, serialize(state));
    }

    @Override
    public Object read(String key) {
        return pool.queryOne("SELECT value FROM state WHERE key = ?", key);
    }

    @Override
    public List<Object> query(Map<String, Object> filter) {
        // Build query from filter map
        return pool.query(buildQuery(filter));
    }

    @Override
    public void delete(String key) {
        pool.execute("DELETE FROM state WHERE key = ?", key);
    }

    @Override
    public void checkpoint(String executionId) {
        pool.execute("INSERT INTO checkpoints (execution_id, timestamp) VALUES (?, NOW())", 
                     executionId);
    }

    @Override
    public void restore(String executionId, String checkpointId) {
        // Restore state from checkpoint
    }
}

Step 3: Register the Provider

ProviderDescriptor descriptor = new ProviderDescriptor(
    "mydb-state-v1",                    // Unique ID
    "MyDatabase State Provider",         // Display name
    "state-management",                  // Capability
    null,                                // Sub-capability (null for top-level)
    "1.0.0",                             // Version
    List.of("write", "read", "query", "delete", "checkpoint", "restore"),
    Map.of(
        "connectionUrl", "jdbc:mydb://localhost:5432/state",
        "poolSize", 10
    ),
    List.of(ComputePlatformProfile.AWS_NATIVE, ComputePlatformProfile.AZURE_NATIVE)
);

registry.register(descriptor);

Step 4: Write Tests

Use the standard test patterns:

class MyDatabaseStateProviderTest {

    private MyDatabaseStateProvider provider;

    @BeforeEach
    void setUp() {
        provider = new MyDatabaseStateProvider();
        provider.initialise(Map.of(
            "connectionUrl", "jdbc:h2:mem:test",
            "poolSize", 2
        ));
    }

    @AfterEach
    void tearDown() {
        provider.cleanup();
    }

    @Test
    void writeAndRead_roundTrip() {
        provider.write("key-1", Map.of("status", "RUNNING"));
        Object state = provider.read("key-1");
        assertThat(state).isEqualTo(Map.of("status", "RUNNING"));
    }

    @Test
    void healthCheck_whenHealthy_returnsHealthy() {
        HealthStatus status = provider.healthCheck();
        assertThat(status.status()).isEqualTo(HealthStatus.Status.HEALTHY);
    }

    @Test
    void validateConfig_missingUrl_returnsFailure() {
        ValidationResult result = provider.validateConfig(Map.of("poolSize", 5));
        assertThat(result.valid()).isFalse();
        assertThat(result.violations()).contains("connectionUrl is required");
    }
}

Provider Lifecycle

stateDiagram-v2
    [*] --> Registered: register()
    Registered --> Validated: validateConfig()
    Validated --> Initialised: initialise()
    Initialised --> Active: healthCheck() = HEALTHY
    Active --> Active: operational methods
    Active --> Degraded: healthCheck() = DEGRADED
    Degraded --> Active: recovery
    Active --> CleanedUp: cleanup()
    Registered --> Deactivated: deactivate()

Best Practices

Validate all config upfront

Catch configuration errors in validateConfig() before initialise() is called.

Emit metrics from every operation

The metrics() method should return current operational metrics (latency, throughput, error rates).

Handle cleanup gracefully

Release all resources in cleanup(). After cleanup, the provider must not be used without re-initialisation.

Use health checks for circuit breaking

Return DEGRADED or UNHEALTHY to signal the registry that this provider needs attention.