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.