Repositories
Repository<T> is the interface that both Karango and Monko implement.
It defines CRUD operations, lifecycle hooks, and entity cache integration.
The Repository<T> interface
Every repository is parameterized by the entity type it stores. The interface provides a standard set of operations that work the same way regardless of the underlying database:
interface Repository<T : Any> {
val name: String // collection/table name
val storedType: TypeRef<T> // type information for deserialization
suspend fun findAll(): Cursor<Stored<T>>
suspend fun findById(id: String?): Stored<T>?
suspend fun <X : T> insert(new: X): Stored<X>
suspend fun <X : T> insert(key: String?, new: X): Stored<X>
suspend fun <X : T> insert(new: New<X>): Stored<X>
suspend fun <X : T> save(stored: Stored<X>): Stored<X>
suspend fun <X : T> save(storable: Storable<X>): Stored<X>
suspend fun <X : T> remove(entity: Stored<X>): RemoveResult
suspend fun remove(idOrKey: String): RemoveResult
suspend fun removeAll(): RemoveResult
} CRUD operations
Insert
@Vault
data class Person(val name: String, val age: Int)
// Insert a value directly — wraps it in New<T> automatically
val stored: Stored<Person> = repo.insert(Person("Alice", 30))
// Insert with an explicit key
val stored2: Stored<Person> = repo.insert("alice-key", Person("Alice", 30))
// Insert with a modification before storing
val stored3: Stored<Person> = repo.insert(Person("Alice", 30)) { person ->
person.copy(age = person.age + 1)
}
// Try inserting — returns null on failure instead of throwing
val maybe: Stored<Person>? = repo.tryInsert(Person("Alice", 30)) Find
// Find by ID — returns null if not found
val person: Stored<Person>? = repo.findById("persons/abc123")
// Find all — returns a Cursor
val all: Cursor<Stored<Person>> = repo.findAll()
// Collect all into a list
val list: List<Stored<Person>> = repo.findAll().toList() Save
Save accepts both Stored<T> and Storable<T>. When given a
New<T>, it delegates to insert. When given a Ref<T>, it resolves
the value first:
// Save a modified entity
val updated = stored.modify { it.copy(name = "Alice Smith") }
val saved: Stored<Person> = repo.save(updated)
// Save with inline modification
val saved2: Stored<Person> = repo.save(stored) { person ->
person.copy(age = 31)
}
// Save only if modified — avoids unnecessary writes
val saved3: Stored<Person> = repo.saveIfModified(stored) { person ->
person.copy(age = 31)
} Remove
// Remove by entity
repo.remove(stored)
// Remove by ID or key
repo.remove("persons/abc123")
// Remove all documents
repo.removeAll() Lifecycle hooks
Repositories support three hook types that run at specific points in the entity lifecycle. Hooks are composable — you can attach multiple hooks, and they run in order.
| Hook | When it runs | Suspend? |
|---|---|---|
OnBeforeSave<T> | Before insert or save. Can modify the entity. | No |
OnAfterSave<T> | After insert or save completes. | Yes |
OnAfterDelete<T> | After a remove completes. | Yes |
OnBeforeSave runs synchronously and returns the (potentially modified) entity.
OnAfterSave and OnAfterDelete run asynchronously in the background
after the database operation completes.
// Built-in hook: automatic timestamps
class TimestampedHook<T : Timestamped> : Repository.Hooks.OnBeforeSave<T> {
override fun <X : T> onBeforeSave(
repo: Repository<T>,
storable: Storable<T>,
): Storable<X> {
// Sets createdAt on first insert, updatedAt on every save
...
}
}
// Composing hooks
val hooks = Repository.Hooks.of(
TimestampedHook<MyEntity>(),
MyCustomHook(),
) EntityCache
The EntityCache deduplicates entity lookups during deserialization.
When a cursor deserializes entities, it can store them in the cache.
Subsequent Ref<T> resolutions check the cache before hitting the database.
interface EntityCache {
fun clear()
fun <T> put(id: String, value: T): T
fun <T> getOrPut(id: String, provider: () -> T?): T?
suspend fun <T> getOrPutAsync(id: String, provider: suspend () -> T?): T?
} Vault ships two implementations:
DefaultEntityCache— thread-safe cache backed byConcurrentHashMap, with synchronized sync access andMutex-based suspend accessNullEntityCache— no-op implementation that always calls through to the provider
In practice, you rarely interact with the EntityCache directly. It is managed by the cursor
and used automatically during Ref resolution.
Repository setup
Repositories can ensure their own infrastructure (collections, indexes) exists:
// Ensure the collection and indexes exist
repo.ensure()
// Or separately
repo.ensureRepository() // create the collection if missing
repo.ensureIndexes() // create defined indexes
// Validate index state
val info: IndexesInfo = repo.validateIndexes()
info.healthyIndexes // indexes that exist as defined
info.missingIndexes // defined but not yet created
info.excessIndexes // exist but not defined
// Recreate all indexes (drop + re-create)
repo.recreateIndexes() Next steps
Ready to connect to a database? Choose your backend: