Skip to content

Quickstart

Get KAP running in 5 minutes.

Use the starter template

Or add the dependency manually:

1. Add the dependency

plugins {
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("io.github.damian-rafael-lattenero:kap-core:2.7.0")
    implementation("io.github.damian-rafael-lattenero:kap-ksp-annotations:2.7.0")
    ksp("io.github.damian-rafael-lattenero:kap-ksp:2.7.0")
}
plugins {
    id 'com.google.devtools.ksp'
}

dependencies {
    implementation 'io.github.damian-rafael-lattenero:kap-core:2.7.0'
    implementation 'io.github.damian-rafael-lattenero:kap-ksp-annotations:2.7.0'
    ksp 'io.github.damian-rafael-lattenero:kap-ksp:2.7.0'
}
<dependency>
    <groupId>io.github.damian-rafael-lattenero</groupId>
    <artifactId>kap-core-jvm</artifactId>
    <version>2.7.0</version>
</dependency>
<!-- See kap-ksp docs for Maven KSP setup -->

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?