Skip to main content
Version: v0.13

Key-Value Store

The IDK provides a cross-platform key-value store (KvStore) for persisting application data with support for namespacing, TTL (time-to-live), and multiple storage backends.

Overview

The key-value store is designed for storing structured application data with:

  • Namespaced storage with type-safe codecs for serialization
  • TTL support for automatic expiration of entries
  • Scope binding for multi-tenant partitioning
  • Multiple backends including in-memory and persistent storage
  • Result-based error handling via IdkResult

Core Concepts

Namespaces

All KvStore operations require a namespace. A namespace combines a name with a codec that handles serialization:

import com.sphereon.data.store.kv.*
import kotlinx.serialization.Serializable

@Serializable
data class SessionData(
val userId: String,
val token: String,
val createdAt: Long
)

// Create a namespace with JSON codec
val sessionNamespace = KvNamespace(
name = "session.data",
codec = KotlinxSerializationJsonKvCodec(
json = Json { ignoreUnknownKeys = true },
serializer = SessionData.serializer()
)
)

Obtaining the Store

Access the KvStore through the KvStoreService:

// Get the KvStoreService from session
val kvStoreService = session.component.kvStoreService

// Get a store by ID
val store: KvStore = kvStoreService.getStore("my-store")

// List available store IDs
val storeIds: Array<String> = kvStoreService.getStoreIds()

// Get store configuration
val config: KvStoreConfig = kvStoreService.getStoreConfig("my-store")

Basic Operations

Storing Values

import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds

// Store with TTL
val result = store.put(
namespace = sessionNamespace,
key = "user-123",
value = SessionData(
userId = "user-123",
token = "abc123",
createdAt = System.currentTimeMillis()
),
ttl = 1.hours
)

when (result) {
is IdkResult.Success -> {
val metadata = result.value.metadata
println("Stored at: ${metadata.createdAtEpochMillis}")
println("Expires at: ${metadata.expiresAtEpochMillis}")
}
is IdkResult.Failure -> {
println("Failed to store: ${result.error}")
}
}

// Store with infinite TTL (never expires)
store.put(
namespace = settingsNamespace,
key = "app-config",
value = appConfig,
ttl = Duration.INFINITE
)

Retrieving Values

// Get value only
val result = store.get(sessionNamespace, "user-123")

when (result) {
is IdkResult.Success -> {
val sessionData: SessionData? = result.value
if (sessionData != null) {
println("Token: ${sessionData.token}")
} else {
println("Key not found")
}
}
is IdkResult.Failure -> {
println("Error: ${result.error}")
}
}

// Get entry with metadata
val entryResult = store.getEntry(sessionNamespace, "user-123")

when (entryResult) {
is IdkResult.Success -> {
val entry: KvEntry<SessionData>? = entryResult.value
if (entry != null) {
println("Value: ${entry.value}")
println("Created: ${entry.metadata.createdAtEpochMillis}")
println("Expires: ${entry.metadata.expiresAtEpochMillis}")
}
}
is IdkResult.Failure -> {
println("Error: ${entryResult.error}")
}
}

Checking Existence

val existsResult = store.exists(sessionNamespace, "user-123")

when (existsResult) {
is IdkResult.Success -> {
if (existsResult.value) {
println("Key exists")
} else {
println("Key not found")
}
}
is IdkResult.Failure -> {
println("Error: ${existsResult.error}")
}
}

Deleting Values

val deleteResult = store.delete(sessionNamespace, "user-123")

when (deleteResult) {
is IdkResult.Success -> {
if (deleteResult.value) {
println("Deleted successfully")
} else {
println("Key did not exist")
}
}
is IdkResult.Failure -> {
println("Error: ${deleteResult.error}")
}
}

Extending TTL

// Extend TTL without modifying the value
val touchResult = store.touch(
namespace = sessionNamespace,
key = "user-123",
ttl = 2.hours
)

when (touchResult) {
is IdkResult.Success -> {
if (touchResult.value) {
println("TTL extended")
} else {
println("Key not found")
}
}
is IdkResult.Failure -> {
println("Error: ${touchResult.error}")
}
}

Listing Operations

Stores that implement KvStoreListing support key enumeration:

// Check if store supports listing
if (store is KvStoreListing) {
// List all keys in a namespace
val keysResult = store.listKeys(sessionNamespace)

when (keysResult) {
is IdkResult.Success -> {
keysResult.value.forEach { key ->
println("Key: $key")
}
}
is IdkResult.Failure -> {
println("Error: ${keysResult.error}")
}
}

// Get all values in a namespace
val allResult = store.getAll(sessionNamespace)

when (allResult) {
is IdkResult.Success -> {
allResult.value.forEach { (key, value) ->
println("$key: $value")
}
}
is IdkResult.Failure -> {
println("Error: ${allResult.error}")
}
}

// Get all entries with metadata
val entriesResult = store.getAllEntries(sessionNamespace)
}

Cleanup Expired Entries

// Clean up expired entries in a specific namespace
val cleanupResult = store.cleanupExpired(sessionNamespace)

when (cleanupResult) {
is IdkResult.Success -> {
println("Cleaned up ${cleanupResult.value} expired entries")
}
is IdkResult.Failure -> {
println("Error: ${cleanupResult.error}")
}
}

// Clean up all namespaces
val fullCleanupResult = store.cleanupExpired(namespace = null)

Store Configuration

Stores are configured with KvStoreConfig:

val config = KvStoreConfig(
id = "my-store",
scopeBinding = KvStoreScopeBinding.TENANT, // Data partitioned by tenant
backendId = "memory", // In-memory backend
enabled = true,
order = 0,
defaultConfigValues = mapOf(
"custom.setting" to "value"
)
)

Scope Bindings

Scope BindingDescription
APPSingle partition for entire application
TENANTPartitioned by tenant ID
PRINCIPAL_TENANTPartitioned by principal and tenant
SESSIONPartitioned by session ID

Storage Backends

In-Memory Backend

The in-memory backend stores data in memory. Data is lost when the application terminates.

val config = KvStoreConfig(
id = "cache-store",
backendId = "memory",
scopeBinding = KvStoreScopeBinding.SESSION
)

Kottage Backend

The Kottage backend provides persistent storage using the Kottage multiplatform library:

val config = KvStoreConfig(
id = "persistent-store",
backendId = "kottage",
scopeBinding = KvStoreScopeBinding.TENANT
)

Configuration via Properties

Configure stores using properties:

# Define a store
kv.stores.sessions.type=memory
kv.stores.sessions.scopebinding=SESSION
kv.stores.sessions.enabled=true
kv.stores.sessions.order=0

# Persistent store
kv.stores.credentials.type=kottage
kv.stores.credentials.scopebinding=TENANT
kv.stores.credentials.enabled=true

Data Models

KvEntry

data class KvEntry<V>(
val value: V,
val metadata: KvEntryMetadata
)

KvEntryMetadata

data class KvEntryMetadata(
val createdAtEpochMillis: Long,
val expiresAtEpochMillis: Long? // null for infinite TTL
)

KvPutResult

data class KvPutResult(
val metadata: KvEntryMetadata
)

Best Practices

Use meaningful namespace names to organize data logically and prevent key collisions across features.

Set appropriate TTLs for temporary data like session tokens and cached responses to prevent stale data accumulation.

Choose the right scope binding based on data isolation requirements: APP for shared data, TENANT for tenant-specific data, SESSION for session-scoped data.

Handle errors with IdkResult - all operations return result types that must be checked for success or failure.

Implement cleanup routines by periodically calling cleanupExpired() if your backend doesn't automatically purge expired entries.