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:
- Android/Kotlin
- iOS/Swift
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()
)
)
import SphereonDataStore
struct SessionData: Codable {
let userId: String
let token: String
let createdAt: Int64
}
// Create a namespace with JSON codec
let sessionNamespace = KvNamespace(
name: "session.data",
codec: KotlinxSerializationJsonKvCodec(
json: Json.Default,
serializer: SessionData.serializer()
)
)
Obtaining the Store
Access the KvStore through the KvStoreService:
- Android/Kotlin
- iOS/Swift
// 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")
// Get the KvStoreService from session
let kvStoreService = session.component.kvStoreService
// Get a store by ID
let store: KvStore = kvStoreService.getStore(storeId: "my-store")
// List available store IDs
let storeIds: [String] = kvStoreService.getStoreIds()
// Get store configuration
let config: KvStoreConfig = kvStoreService.getStoreConfig(storeId: "my-store")
Basic Operations
Storing Values
- Android/Kotlin
- iOS/Swift
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
)
// Store with TTL
let result = try await store.put(
namespace: sessionNamespace,
key: "user-123",
value: SessionData(
userId: "user-123",
token: "abc123",
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
),
ttl: Duration.hours(1)
)
switch result {
case .success(let putResult):
let metadata = putResult.metadata
print("Stored at: \(metadata.createdAtEpochMillis)")
print("Expires at: \(metadata.expiresAtEpochMillis)")
case .failure(let error):
print("Failed to store: \(error)")
}
// Store with infinite TTL (never expires)
try await store.put(
namespace: settingsNamespace,
key: "app-config",
value: appConfig,
ttl: Duration.INFINITE
)
Retrieving Values
- Android/Kotlin
- iOS/Swift
// 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}")
}
}
// Get value only
let result = try await store.get(namespace: sessionNamespace, key: "user-123")
switch result {
case .success(let sessionData):
if let sessionData = sessionData {
print("Token: \(sessionData.token)")
} else {
print("Key not found")
}
case .failure(let error):
print("Error: \(error)")
}
// Get entry with metadata
let entryResult = try await store.getEntry(namespace: sessionNamespace, key: "user-123")
switch entryResult {
case .success(let entry):
if let entry = entry {
print("Value: \(entry.value)")
print("Created: \(entry.metadata.createdAtEpochMillis)")
print("Expires: \(entry.metadata.expiresAtEpochMillis)")
}
case .failure(let error):
print("Error: \(error)")
}
Checking Existence
- Android/Kotlin
- iOS/Swift
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}")
}
}
let existsResult = try await store.exists(namespace: sessionNamespace, key: "user-123")
switch existsResult {
case .success(let exists):
if exists {
print("Key exists")
} else {
print("Key not found")
}
case .failure(let error):
print("Error: \(error)")
}
Deleting Values
- Android/Kotlin
- iOS/Swift
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}")
}
}
let deleteResult = try await store.delete(namespace: sessionNamespace, key: "user-123")
switch deleteResult {
case .success(let deleted):
if deleted {
print("Deleted successfully")
} else {
print("Key did not exist")
}
case .failure(let error):
print("Error: \(error)")
}
Extending TTL
- Android/Kotlin
- iOS/Swift
// 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}")
}
}
// Extend TTL without modifying the value
let touchResult = try await store.touch(
namespace: sessionNamespace,
key: "user-123",
ttl: Duration.hours(2)
)
switch touchResult {
case .success(let touched):
if touched {
print("TTL extended")
} else {
print("Key not found")
}
case .failure(let error):
print("Error: \(error)")
}
Listing Operations
Stores that implement KvStoreListing support key enumeration:
- Android/Kotlin
- iOS/Swift
// 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)
}
// Check if store supports listing
if let listingStore = store as? KvStoreListing {
// List all keys in a namespace
let keysResult = try await listingStore.listKeys(namespace: sessionNamespace)
switch keysResult {
case .success(let keys):
for key in keys {
print("Key: \(key)")
}
case .failure(let error):
print("Error: \(error)")
}
// Get all values in a namespace
let allResult = try await listingStore.getAll(namespace: sessionNamespace)
}
Cleanup Expired Entries
- Android/Kotlin
- iOS/Swift
// 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)
// Clean up expired entries in a specific namespace
let cleanupResult = try await store.cleanupExpired(namespace: sessionNamespace)
switch cleanupResult {
case .success(let count):
print("Cleaned up \(count) expired entries")
case .failure(let error):
print("Error: \(error)")
}
// Clean up all namespaces
let fullCleanupResult = try await store.cleanupExpired(namespace: nil)
Store Configuration
Stores are configured with KvStoreConfig:
- Android/Kotlin
- iOS/Swift
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"
)
)
let config = KvStoreConfig(
id: "my-store",
scopeBinding: .tenant, // Data partitioned by tenant
backendId: "memory", // In-memory backend
enabled: true,
order: 0,
defaultConfigValues: [
"custom.setting": "value"
]
)
Scope Bindings
| Scope Binding | Description |
|---|---|
APP | Single partition for entire application |
TENANT | Partitioned by tenant ID |
PRINCIPAL_TENANT | Partitioned by principal and tenant |
SESSION | Partitioned 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.