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:
- Kotlin
- Java
- Scala
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())
}
public final class UserRepo {
record User(int id, String name) {}
static final RowCodecNamed<User> userCodec =
RowCodec.<User>namedBuilder()
.field("id", PgTypes.int4, User::id)
.field("name", PgTypes.text, User::name)
.build(User::new);
static final Operation<List<User>> selectAll =
Fragment.of("SELECT ")
.append(userCodec.columnList()).append(" FROM users ORDER BY name")
.query(userCodec.all())
.named("UserRepo.selectAll");
static final Template<Integer, Optional<User>> selectById =
Fragment.of("SELECT ")
.append(userCodec.columnList()).append(" FROM users WHERE id = ")
.param(PgTypes.int4)
.query(userCodec.maxOne());
}
object UserRepo:
case class User(id: Int, name: String)
val userCodec: RowCodecNamed[User] = RowCodec.namedBuilder[User]()
.field("id", PgTypes.int4)(_.id)
.field("name", PgTypes.text)(_.name)
.build(User.apply)
val selectAll: Operation[List[User]] =
sql"SELECT ${userCodec.columnList} FROM users ORDER BY name"
.query(userCodec.all())
.named("UserRepo.selectAll")
val selectById: Template[Int, Option[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):
- Kotlin
- Java
- Scala
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)
}
public final class UserService {
private final Transactor tx;
public UserService(Transactor tx) {
this.tx = tx;
}
public List<User> listUsers() {
return UserRepo.selectAll.transact(tx);
}
public Optional<User> findUser(int id) {
return UserRepo.selectById.on(id).transact(tx);
}
}
class UserService(tx: Transactor):
def listUsers(): List[User] =
UserRepo.selectAll.transact(tx)
def findUser(id: Int): Option[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.