Cookbook¶
Complete, self-contained examples. Every block compiles and runs. Verified on every CI push in readme-examples.
# Run all examples yourself:
git clone https://github.com/damian-rafael-lattenero/kap.git && cd kap
./gradlew :examples:readme-examples:run
Parallel Execution — 3 services at once¶
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class Dashboard(val user: String, val cart: String, val promos: String)
suspend fun fetchUser(): String { delay(30); return "Alice" }
suspend fun fetchCart(): String { delay(20); return "3 items" }
suspend fun fetchPromos(): String { delay(10); return "SAVE20" }
suspend fun main() {
val result: Dashboard = kap(::Dashboard)
.with { user from fetchUser() } // ┐ all three start at t=0
.with { cart from fetchCart() } // │ total time = max(30, 20, 10) = 30ms
.with { promos from fetchPromos() } // ┘ not 60ms sequential
.evalGraph()
println(result)
// Dashboard(user=Alice, cart=3 items, promos=SAVE20)
}
Functions — @KapTypeSafe on fun¶
@KapTypeSafe works on functions too. KSP generates a marker object from the function name:
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
fun createUser(name: String, email: String, age: Int): String =
"User($name, $email, age=$age)"
suspend fun fetchName(): String { delay(30); return "Alice" }
suspend fun fetchEmail(): String { delay(20); return "alice@example.com" }
suspend fun fetchAge(): Int { delay(10); return 30 }
suspend fun main() {
// CreateUser is a generated marker object — classes use ::ClassName, functions use ObjectName
val result = kap(CreateUser)
.with { name from fetchName() }
.with { email from fetchEmail() }
.with { age from fetchAge() }
.evalGraph()
println(result)
// User(Alice, alice@example.com, age=30)
}
11-Service Checkout — 5 phases, one flat chain¶
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class CheckoutResult(
val user: String, val cart: Double, val promos: String,
val inventory: Boolean, val stock: Boolean,
val shipping: Double, val tax: Double, val discounts: Double,
val payment: Boolean, val confirmation: String, val email: String,
)
suspend fun fetchUser(): String { delay(50); return "Alice" }
suspend fun fetchCart(): Double { delay(40); return 147.50 }
suspend fun fetchPromos(): String { delay(30); return "SUMMER20" }
suspend fun fetchInventory(): Boolean { delay(50); return true }
suspend fun validateStock(): Boolean { delay(20); return true }
suspend fun calcShipping(): Double { delay(30); return 5.99 }
suspend fun calcTax(): Double { delay(20); return 12.38 }
suspend fun calcDiscounts(): Double { delay(15); return 29.50 }
suspend fun reservePayment(): Boolean { delay(40); return true }
suspend fun generateConfirmation(): String { delay(30); return "order-#90142" }
suspend fun sendEmail(): String { delay(20); return "alice@example.com" }
suspend fun main() {
val checkout: CheckoutResult = kap(::CheckoutResult)
.with { user from fetchUser() } // ┐
.with { cart from fetchCart() } // ├─ phase 1: parallel
.with { promos from fetchPromos() } // │
.with { inventory from fetchInventory() } // ┘
.then { stock from validateStock() } // ── phase 2: barrier
.with { shipping from calcShipping() } // ┐
.with { tax from calcTax() } // ├─ phase 3: parallel
.with { discounts from calcDiscounts() } // ┘
.then { payment from reservePayment() } // ── phase 4: barrier
.with { confirmation from generateConfirmation() } // ┐ phase 5: parallel
.with { email from sendEmail() } // ┘
.evalGraph()
println(checkout)
// CheckoutResult(user=Alice, cart=147.5, promos=SUMMER20, inventory=true, stock=true, shipping=5.99, tax=12.38, discounts=29.5, payment=true, confirmation=order-#90142, email=alice@example.com)
// 130ms total — not 460ms sequential
}
Value-Dependent Phases — .andThen¶
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class UserContext(val profile: String, val prefs: String, val tier: String)
@KapTypeSafe
data class PersonalizedDashboard(val recs: String, val promos: String, val trending: String)
suspend fun fetchProfile(id: String): String { delay(50); return "profile-$id" }
suspend fun fetchPreferences(id: String): String { delay(30); return "prefs-dark" }
suspend fun fetchLoyaltyTier(id: String): String { delay(40); return "gold" }
suspend fun fetchRecommendations(profile: String): String { delay(40); return "recs-for-$profile" }
suspend fun fetchPromotions(tier: String): String { delay(30); return "promos-$tier" }
suspend fun fetchTrending(prefs: String): String { delay(20); return "trending-$prefs" }
suspend fun main() {
val dashboard = kap(::UserContext)
.with { profile from fetchProfile("user-1") } // ┐ phase 1: parallel
.with { prefs from fetchPreferences("user-1") } // │
.with { tier from fetchLoyaltyTier("user-1") } // ┘
.andThen { ctx -> // ── barrier: ctx available
kap(::PersonalizedDashboard)
.with { recs from fetchRecommendations(ctx.profile) } // ┐ phase 2: parallel
.with { promos from fetchPromotions(ctx.tier) } // │ uses ctx from phase 1
.with { trending from fetchTrending(ctx.prefs) } // ┘
}
.evalGraph()
println(dashboard)
// PersonalizedDashboard(recs=recs-for-profile-user-1, promos=promos-gold, trending=trending-prefs-dark)
}
Partial Failure — settled { }¶
import kap.*
import kotlinx.coroutines.delay
// The type changes: user is Result<String> instead of String
@KapTypeSafe
data class PartialDashboard(val user: Result<String>, val cart: String, val config: String)
// Services: one is unreliable, the others always succeed
suspend fun fetchUserMayFail(): String { throw RuntimeException("user service down") }
suspend fun fetchCartAlways(): String { delay(20); return "cart-ok" }
suspend fun fetchConfigAlways(): String { delay(15); return "config-ok" }
suspend fun main() {
val dashboard = kap(::PartialDashboard)
.with(PartialDashboardKap.user from settled { fetchUserMayFail() }) // Result<String> — won't cancel siblings
.with { cart from fetchCartAlways() } // String — runs normally
.with { config from fetchConfigAlways() } // String — runs normally
.evalGraph()
println(dashboard)
// PartialDashboard(user=Result.failure(RuntimeException), cart=cart-ok, config=config-ok)
// Use the result with a fallback:
val userName = dashboard.user.getOrDefault("anonymous")
println("userName = $userName") // "anonymous"
}
Collect ALL Results — traverseSettled¶
import kap.*
suspend fun main() {
val ids = listOf(1, 2, 3, 4, 5)
val results: List<Result<String>> = ids.traverseSettled { id ->
Kap {
if (id % 2 == 0) throw RuntimeException("fail-$id")
"user-$id"
}
}.evalGraph()
val successes = results.filter { it.isSuccess }.map { it.getOrThrow() }
val failures = results.filter { it.isFailure }.map { it.exceptionOrNull()!!.message }
println("successes=$successes, failures=$failures")
// successes=[user-1, user-3, user-5], failures=[fail-2, fail-4]
}
Racing — Fastest region wins¶
import kap.*
import kotlinx.coroutines.delay
suspend fun fetchFromRegionUS(): String { delay(100); return "US-data" }
suspend fun fetchFromRegionEU(): String { delay(30); return "EU-data" }
suspend fun fetchFromRegionAP(): String { delay(60); return "AP-data" }
suspend fun main() {
val fastest = raceN(
Kap { fetchFromRegionUS() }, // 100ms
Kap { fetchFromRegionEU() }, // 30ms — wins
Kap { fetchFromRegionAP() }, // 60ms
).evalGraph()
println(fastest)
// EU-data (at ~30ms, US and AP cancelled automatically)
}
Retry with Schedule¶
import kap.*
import kotlin.time.Duration.Companion.milliseconds
suspend fun main() {
var attempts = 0
val policy = Schedule.times<Throwable>(5) and
Schedule.exponential(10.milliseconds) and
Schedule.doWhile<Throwable> { it is RuntimeException }
val result = Kap {
attempts++
if (attempts <= 2) throw RuntimeException("flake #$attempts")
"success on attempt $attempts"
}.retry(policy).evalGraph()
println(result)
// success on attempt 3
}
TimeoutRace — Parallel fallback¶
import kap.*
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.milliseconds
suspend fun fetchFromPrimary(): String { delay(200); return "primary-data" }
suspend fun fetchFromFallback(): String { delay(30); return "fallback-data" }
suspend fun main() {
val start = System.currentTimeMillis()
val result = Kap { fetchFromPrimary() }
.timeoutRace(100.milliseconds, Kap { fetchFromFallback() })
.evalGraph()
val elapsed = System.currentTimeMillis() - start
println("$result (${elapsed}ms — fallback won, both started at t=0)")
// fallback-data (30ms — 2.6x faster than sequential timeout)
}
Resource Safety — bracket¶
import kap.*
import kotlinx.coroutines.delay
class MockConnection(val name: String) {
var closed = false
suspend fun query(q: String): String { delay(20); return "$name:result-of-$q" }
suspend fun get(key: String): String { delay(15); return "$name:$key" }
fun close() { closed = true; println(" closed $name") }
}
suspend fun openDb(): MockConnection { delay(10); return MockConnection("db") }
suspend fun openCache(): MockConnection { delay(10); return MockConnection("cache") }
suspend fun openHttp(): MockConnection { delay(10); return MockConnection("http") }
suspend fun main() {
val result = Kap.of { db: String -> { cache: String -> { api: String -> "$db|$cache|$api" } } }
.with(bracket(
acquire = { openDb() },
use = { conn -> Kap { conn.query("SELECT 1") } },
release = { conn -> conn.close() }, // guaranteed, even on failure
))
.with(bracket(
acquire = { openCache() },
use = { conn -> Kap { conn.get("key") } },
release = { conn -> conn.close() },
))
.with(bracket(
acquire = { openHttp() },
use = { client -> Kap { client.get("/api") } },
release = { client -> client.close() },
))
.evalGraph()
println(result)
// closed db
// closed cache
// closed http
// db:result-of-SELECT 1|cache:key|http:/api
}
Parallel Validation — Collect every error¶
import kap.*
import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.nonEmptyListOf
import kotlinx.coroutines.delay
sealed class RegError(val message: String) {
class NameTooShort(msg: String) : RegError(msg)
class InvalidEmail(msg: String) : RegError(msg)
class AgeTooLow(msg: String) : RegError(msg)
class UsernameTaken(msg: String) : RegError(msg)
}
data class ValidName(val value: String)
data class ValidEmail(val value: String)
data class ValidAge(val value: Int)
data class ValidUsername(val value: String)
data class User(val name: ValidName, val email: ValidEmail, val age: ValidAge, val username: ValidUsername)
suspend fun validateName(name: String): Either<NonEmptyList<RegError>, ValidName> {
delay(20)
return if (name.length >= 2) Either.Right(ValidName(name))
else Either.Left(nonEmptyListOf(RegError.NameTooShort("Name must be >= 2 chars")))
}
suspend fun validateEmail(email: String): Either<NonEmptyList<RegError>, ValidEmail> {
delay(15)
return if ("@" in email) Either.Right(ValidEmail(email))
else Either.Left(nonEmptyListOf(RegError.InvalidEmail("Invalid email: $email")))
}
suspend fun validateAge(age: Int): Either<NonEmptyList<RegError>, ValidAge> {
delay(10)
return if (age >= 18) Either.Right(ValidAge(age))
else Either.Left(nonEmptyListOf(RegError.AgeTooLow("Must be >= 18, got $age")))
}
suspend fun checkUsername(username: String): Either<NonEmptyList<RegError>, ValidUsername> {
delay(25)
return if (username.length >= 3) Either.Right(ValidUsername(username))
else Either.Left(nonEmptyListOf(RegError.UsernameTaken("Username too short")))
}
suspend fun main() {
// All fail — every error collected in one response:
val result: Either<NonEmptyList<RegError>, User> = zipV(
{ validateName("A") }, // too short
{ validateEmail("bad") }, // no @
{ validateAge(10) }, // under 18
{ checkUsername("al") }, // too short
) { name, email, age, username -> User(name, email, age, username) }
.evalGraph()
when (result) {
is Either.Right -> println("Valid: ${result.value}")
is Either.Left -> println("${result.value.size} errors: ${result.value.map { it.message }}")
}
// 4 errors: [Name must be >= 2 chars, Invalid email: bad, Must be >= 18 got 10, Username too short]
// ALL 4 validators ran in parallel. All errors in one response.
}
Timed — Measure execution duration¶
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class Dashboard(val user: String, val cart: String, val promos: String)
suspend fun fetchUser(): String { delay(50); return "Alice" }
suspend fun fetchCart(): String { delay(40); return "3 items" }
suspend fun fetchPromos(): String { delay(30); return "SAVE20" }
suspend fun main() {
// timed().evalGraph() returns TimedResult(value, duration)
val result = kap(::Dashboard)
.with { user from fetchUser() }
.with { cart from fetchCart() }
.with { promos from fetchPromos() }
.timed()
.evalGraph()
println(result.value)
println("Built in ${result.duration.inWholeMilliseconds}ms")
// Dashboard(user=Alice, cart=3 items, promos=SAVE20)
// Built in 50ms (parallel, not 120ms sequential)
}
Memoization — Cache only successes¶
import kap.*
import kotlinx.coroutines.delay
suspend fun main() {
var callCount = 0
val fetchOnce = Kap { callCount++; delay(30); "expensive-result" }.memoizeOnSuccess()
val a = fetchOnce.evalGraph() // runs the actual call, callCount=1
val b = fetchOnce.evalGraph() // cached, instant, callCount still 1
println("a=$a, b=$b, callCount=$callCount")
// a=expensive-result, b=expensive-result, callCount=1
// If first call HAD failed? Not cached. Next call would retry.
}
Graph as Data — reuse and branch¶
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class Order(val user: String, val cart: String, val total: Double)
suspend fun fetchUser(): String { delay(50); return "Alice" }
suspend fun fetchStandardCart(): String { delay(40); return "3 items" }
suspend fun fetchPremiumCart(): String { delay(30); return "3 items + priority" }
suspend fun main() {
// The graph is data — nothing runs until .evalGraph()
val base = kap(::Order).with { user from fetchUser() }
// Complete it differently based on runtime conditions.
// Type inference handles the wrapper shape — no need to spell out OrderKap<...>
// unless you want to (it's the curried function type after `cart` is filled).
fun addCart(
partial: OrderKap<(OrderCart) -> (OrderTotal) -> Order>,
premium: Boolean,
): OrderKap<(OrderTotal) -> Order> =
if (premium) partial.with { cart from fetchPremiumCart() }
else partial.with { cart from fetchStandardCart() }
// Build two different graphs from the same base
val standard = addCart(base, premium = false).with { total from 99.0 }.evalGraph()
val premium = addCart(base, premium = true).with { total from 149.0 }.evalGraph()
println("Standard: $standard")
println("Premium: $premium")
// Standard: Order(user=Alice, cart=3 items, total=99.0)
// Premium: Order(user=Alice, cart=3 items + priority, total=149.0)
}
Real HTTP — GitHub API¶
From examples/real-world-http:
import kap.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.json.Json
@Serializable
data class GithubUser(
val login: String,
val name: String? = null,
@SerialName("public_repos") val publicRepos: Int = 0,
val followers: Int = 0,
)
@Serializable
data class GithubRepo(
val name: String,
@SerialName("stargazers_count") val stars: Int = 0,
val language: String? = null,
)
@Serializable
data class CatFact(val fact: String, val length: Int = 0)
@KapTypeSafe
data class DeveloperProfile(val user: GithubUser, val topRepos: List<GithubRepo>, val funFact: String)
val client = HttpClient(CIO) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
suspend fun fetchGithubUser(username: String): GithubUser =
client.get("https://api.github.com/users/$username").body()
suspend fun fetchGithubRepos(username: String): List<GithubRepo> =
client.get("https://api.github.com/users/$username/repos?sort=stars&per_page=5").body()
suspend fun fetchCatFact(): CatFact =
client.get("https://catfact.ninja/fact").body()
suspend fun main() {
val profile = kap(::DeveloperProfile)
.with { user from fetchGithubUser("JetBrains") } // ┐
.with { topRepos from fetchGithubRepos("JetBrains") } // ├─ all three in parallel
.with { funFact from fetchCatFact().fact } // ┘
.evalGraph()
println("User: ${profile.user.login} (${profile.user.name})")
println("Repos: ${profile.user.publicRepos} public")
profile.topRepos.forEach { println(" - ${it.name} (${it.stars} stars)") }
println("Fun fact: ${profile.funFact}")
// User: JetBrains (JetBrains)
// Repos: 805 public
// - kotlin (50423 stars)
// - ...
// Fun fact: Cats sleep 70% of their lives.
client.close()
}
Ready to try? Get Started GitHub