Skip to main content

Structuring Repositories

A common pattern is to define queries as public vals on a repository object with declared types. Fixed queries become Operations (name them for analysis reports), parameterized queries become Templates:

object UserRepo {
data class User(val id: Int, val name: String)

val userCodec: RowCodecNamed<User> =
RowCodec.namedBuilder<User>()
.field("id", PgTypes.int4, User::id)
.field("name", PgTypes.text, User::name)
.build(::User)

val selectAll: Operation<List<User>> =
sql { "SELECT ${userCodec.columnList} FROM users ORDER BY name" }
.query(userCodec.all())
.named("UserRepo.selectAll")

val selectById: Template<Int, User?> =
sql { "SELECT ${userCodec.columnList} FROM users WHERE id = " }
.param(PgTypes.int4)
.query(userCodec.maxOne())

}

Exposing Operation and Template directly — rather than wrapping them in methods — gives callers maximum flexibility. They can compose, batch, name, or analyze these values however they like, without the repository dictating execution strategy.

This also means the repository stays in the database layer: it knows what to query, but not when or how to run it. The service layer owns the transaction boundary by calling .transact(tx):

class UserService(private val tx: Transactor) {
fun listUsers(): List<User> =
UserRepo.selectAll.transact(tx)

fun findUser(id: Int): User? =
UserRepo.selectById.on(id).transact(tx)
}

Both Operation and Template implement Analyzable, so AnalyzableScanner discovers them automatically — no manual list needed. See Query Analysis for details.