Quickstart¶
Get KAP running in 5 minutes.
Or add the dependency manually:
1. Add the dependency¶
KSP enables @KapTypeSafe
The KSP plugin and kap-ksp processor generate named builder methods (.withUser {}, .thenStock {}, etc.) from your @KapTypeSafe-annotated data classes. The core .with {} / .then {} operators are used internally by the named builders. For advanced composition, you can also use Kap.of { } with manual currying.
2. Write your first parallel call¶
Copy, paste, run:
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() {
// Named builders generated by @KapTypeSafe
val result: Dashboard = kap(::Dashboard)
.withUser { fetchUser() } // ┐ all three start at t=0
.withCart { fetchCart() } // │ total time = max(50, 40, 30) = 50ms
.withPromos { fetchPromos() } // ┘ not 120ms sequential
.evalGraph()
println(result)
// Dashboard(user=Alice, cart=3 items, promos=SAVE20)
}
That's it. @KapTypeSafe generates .withUser {}, .withCart {}, .withPromos {} from your data class properties. Three calls run in parallel, results are type-checked into your data class, and each step is self-documenting.
Advanced composition
The core .with {} / .then {} operators are used internally by the named builders. For advanced composition, you can also use Kap.of { } with manual currying.
3. Works on functions too¶
@KapTypeSafe works on functions, not just classes. KSP generates a marker object from the function name:
import kap.*
@KapTypeSafe
fun createUser(name: String, email: String, age: Int): String =
"User($name, $email, $age)"
suspend fun main() {
// CreateUser is a generated marker object
val result = kap(CreateUser)
.withName { "Alice" }
.withEmail { "alice@example.com" }
.withAge { 30 }
.evalGraph()
println(result)
// User(Alice, alice@example.com, 30)
}
Classes use kap(::ClassName). Functions use kap(GeneratedObject).
4. Add phases¶
Real-world flows have dependencies. Use .then for barriers:
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class CheckoutResult(
val user: String, val cart: String, val stock: Boolean,
val shipping: Double, val tax: Double,
)
suspend fun fetchUser(): String { delay(50); return "Alice" }
suspend fun fetchCart(): String { delay(40); return "3 items" }
suspend fun validateStock(): Boolean { delay(20); return true }
suspend fun calcShipping(): Double { delay(30); return 5.99 }
suspend fun calcTax(): Double { delay(20); return 0.08 }
suspend fun main() {
// @KapTypeSafe on CheckoutResult generates named builders
val checkout: CheckoutResult = kap(::CheckoutResult)
.withUser { fetchUser() } // ┐ phase 1: parallel
.withCart { fetchCart() } // ┘
.thenStock { validateStock() } // ── phase 2: waits for phase 1
.withShipping { calcShipping() } // ┐ phase 3: parallel
.withTax { calcTax() } // ┘
.evalGraph()
println(checkout)
// CheckoutResult(user=Alice, cart=3 items, stock=true, shipping=5.99, tax=0.08)
}
.with = parallel. .then = barrier. The code shape is the execution plan.
5. Use value-dependent phases¶
When phase 2 needs phase 1's result, use .andThen:
import kap.*
import kotlinx.coroutines.delay
@KapTypeSafe
data class UserContext(val profile: String, val prefs: String)
@KapTypeSafe
data class EnrichedDashboard(val recs: String, val promos: String)
suspend fun fetchProfile(userId: String): String { delay(50); return "profile-$userId" }
suspend fun fetchPreferences(userId: String): String { delay(30); return "dark-mode" }
suspend fun fetchRecommendations(profile: String): String { delay(40); return "recs-for-$profile" }
suspend fun fetchPromotions(prefs: String): String { delay(30); return "promos-for-$prefs" }
suspend fun main() {
val userId = "user-42"
val dashboard: EnrichedDashboard = kap(::UserContext)
.withProfile { fetchProfile(userId) } // ┐ phase 1: parallel
.withPrefs { fetchPreferences(userId) } // ┘
.andThen { ctx -> // ── barrier: ctx available
kap(::EnrichedDashboard)
.withRecs { fetchRecommendations(ctx.profile) } // ┐ phase 2: parallel
.withPromos { fetchPromotions(ctx.prefs) } // ┘ uses ctx from phase 1
}
.evalGraph()
println(dashboard)
// EnrichedDashboard(recs=recs-for-profile-user-42, promos=promos-for-dark-mode)
}
6. Run an example¶
git clone https://github.com/damian-rafael-lattenero/kap.git
cd kap
./gradlew :examples:ecommerce-checkout:run
7. The graph is data¶
Nothing runs until .evalGraph(). The graph is a value — you can store it, pass it, branch on it:
@KapTypeSafe
data class Order(val user: String, val cart: String, val shipping: Double)
// Start building — nothing executes yet
val base = kap(::Order)
.withUser { fetchUser() }
// A function completes the graph based on runtime conditions
fun addCart(partial: OrderStep1, type: String): OrderStep2 = when (type) {
"premium" -> partial.withCart { fetchPremiumCart() }
else -> partial.withCart { fetchStandardCart() }
}
// Another function adds shipping based on country
fun addShipping(partial: OrderStep2, country: String): Kap<Order> = when (country) {
"AR" -> partial.withShipping { calcShippingAR() }
"US" -> partial.withShipping { calcShippingUS() }
else -> partial.withShipping { 0.0 }
}
// Assemble dynamically — still nothing has executed
val graph = addShipping(addCart(base, "premium"), "AR")
// NOW everything runs — parallel branches execute concurrently
val order = graph.evalGraph()
This is what raw coroutines can't do: async {} starts immediately. In KAP, the graph is lazy — you build it, shape it, then execute it.
8. Extra type safety with kapTyped¶
kap(::User) step classes enforce parameter order — but if firstName and lastName are both String, you could return the wrong one inside the lambda. kapTyped adds opaque wrapper types so the compiler catches that too:
@KapTypeSafe
data class User(val firstName: String, val lastName: String, val age: Int)
// Named builders — enforces order, raw types
kap(::User)
.withFirstName { fetchFirstName() }
.withLastName { fetchLastName() }
.withAge { fetchAge() }
.evalGraph()
// Opaque types — enforces order AND type identity
kapTyped(::User)
.with { fetchFirstName().firstNameUser } // String → UserFirstName
.with { fetchLastName().lastNameUser } // String → UserLastName
.with { fetchAge().ageUser } // Int → UserAge
.evalGraph()
Extension properties (.firstNameUser, .lastNameUser) are generated by KSP — the IDE autocompletes them. Use kap() for most cases, kapTyped() when same-typed fields need extra safety.
What's next?¶
- kap-core KDocs — Full API with Raw/Arrow/KAP comparisons
- kap-resilience KDocs — Schedule, CircuitBreaker, Resource
- kap-arrow KDocs — Parallel validation with error accumulation
- Cookbook — 12 complete runnable examples
- Comparison — KAP vs Arrow vs raw coroutines