Kontainer Integration
Karango plugs into Kontainer for automatic repository discovery, dependency injection, and per-request database scoping.
Setting up Karango with Kontainer
One line to register all Karango infrastructure:
import io.peekandpoke.karango.karango
import io.peekandpoke.karango.config.ArangoDbConfig
import io.peekandpoke.ultra.kontainer.kontainer
val blueprint = kontainer {
// Register Karango module — sets up driver, codec, and DB connection
karango(ArangoDbConfig(
host = "localhost",
port = 8529,
database = "my_app",
))
// Register your repositories as singletons
singleton(PersonsRepo::class)
singleton(BlogPostsRepo::class)
singleton(CommentsRepo::class)
} The karango(config) call registers:
ArangoDatabaseAsync— the raw ArangoDB connection (cached per config)KarangoDriver— query executor (dynamic, created per request)KarangoCodec— serialization with Vault + Slumber integration (dynamic)
How the Database discovers repositories
The Database class receives all registered Repository<*> instances via a Lookup<Repository<*>>.
Kontainer automatically collects all services that implement Repository<*> and injects them:
// Kontainer automatically discovers all Repository<*> implementations
// and passes them to the Database class as a Lookup<Repository<*>>
// You DON'T need to register the Database manually — it's wired automatically.
// Just register your repositories:
val blueprint = kontainer {
karango(config)
singleton(PersonsRepo::class)
singleton(ArticlesRepo::class)
}
// When creating a kontainer instance:
val kontainer = blueprint.create()
// The Database now knows about PersonsRepo and ArticlesRepo
val database: Database = kontainer.get(Database::class)
database.getRepositories()
// → [PersonsRepo, ArticlesRepo] Accessing repositories through Database
Define extension properties for convenient typed access:
// Define accessors (typically next to your repository class)
val Database.persons get() = getRepository<PersonsRepo>()
val Database.articles get() = getRepository<ArticlesRepo>()
val Database.comments get() = getRepository<CommentsRepo>()
// Use them anywhere you have a Database
suspend fun getActiveUsers(database: Database): List<Stored<Person>> {
return database.persons.findList {
FOR(database.persons) { person ->
FILTER(person.age GTE 18)
RETURN(person)
}
}
} Repository by type lookup
The Database can also find which repository stores a given entity type:
// Find the repository that stores Person entities
val repo = database.getRepositoryStoring(Person::class)
// → PersonsRepo
// Check if any repository handles a type
database.hasRepositoryStoring(Person::class) // true
database.hasRepositoryStoring(Unknown::class) // false
// Find by collection name
val repo = database.getRepository("persons")
// → PersonsRepo Database lifecycle
On startup, ensure all collections and indexes exist:
val database: Database = kontainer.get(Database::class)
// Create collections if they don't exist
database.ensureRepositories()
// Create indexes defined in buildIndexes()
database.ensureIndexes()
// Or validate without creating
database.validateIndexes() Real-world example: Funktor app setup
Here's how the Funktor demo app wires everything together:
fun createBlueprint(config: AppConfig) = kontainer {
// Mount Funktor modules — each can opt into Karango storage
funktor(
config = config,
logging = { useKarango() }, // Logs stored in ArangoDB
cluster = { useKarango() }, // Job queues, locks in ArangoDB
auth = { useKarango() }, // Auth records in ArangoDB
)
// Mount the ArangoDB connection
karango(config.arangodb)
// Application-specific repositories
singleton(AdminUsersRepo::class)
}
// On application startup:
val app = createBlueprint(config)
val kontainer = app.create()
// Ensure all collections and indexes exist
kontainer.get(Database::class).ensureRepositories()
kontainer.get(Database::class).ensureIndexes() Each Funktor module (logging, cluster, auth) registers its own
repositories when useKarango() is called. The Database automatically discovers all of them.
Dynamic vs singleton repositories
Repositories are typically registered as singletons — they hold no per-request state. The KarangoDriver
and KarangoCodec are registered as dynamic (created per kontainer instance)
because they carry per-request context like the EntityCache:
// This is what karango(config) registers internally:
val KarangoModule = module { config: ArangoDbConfig ->
// Shared connection — created once
instance(ArangoDatabaseAsync::class, config.toArangoDb())
// Per-request driver and codec
dynamic(KarangoDriver::class)
dynamic(KarangoCodec::class) { database: Database, cache: EntityCache ->
KarangoCodec(config = slumberConfig, database = database, entityCache = cache)
}
}