If you’ve written non-trivial coroutine code, you’ve probably built something like this:
- multiple
asynccalls - some run in parallel
- some depend on earlier results
- everything gets glued together manually
And at some point… it starts to feel messy.
The real problems (not the obvious ones)
Raw coroutines are great, but orchestration has hidden costs:
- Dependencies are implicit
- Phases are invisible
- You manually wire values between steps
- Swapping values can still compile if types match
Example:
val user = async { fetchUser() }
val cart = async { fetchCart() }
val promos = async { fetchPromos() }
val result = combine(
user.await(),
promos.await(), // swapped accidentally
cart.await()
)
👉 This compiles.
👉 And now you have a bug.
What I wanted instead
I wanted something where:
- parallel work is obvious
- dependencies are explicit
- phases are visible
- and if it compiles, the wiring is correct
Enter KAP
val checkout: CheckoutResult = Async {
kap(::CheckoutResult)
.with { fetchUser() }
.with { fetchCart() }
.with { fetchPromos() }
.with { fetchInventory() }
.then { validateStock() }
.with { calcShipping() }
.with { calcTax() }
.with { calcDiscounts() }
.andThen { partial ->
kap(::FinalCheckout)
.with { reservePayment(partial) }
.with { applyLoyaltyPoints(partial) }
}
.with { generateConfirmation() }
.with { sendEmail() }
}
👉 13 calls
👉 6 phases
👉 no manual wiring
The dependency graph is the code.
The mental model (it’s simpler than it looks)
You only need three things:
| You write | What it means |
|---|---|
.with { } |
runs in parallel with others |
.then { } |
waits for all previous |
.andThen { ctx -> } |
next step using previous result |
That’s it.
Why this matters
1. No more invisible phases
With coroutines:
- phases exist
- but you don’t see them
With KAP:
- phases are structural
2. No more manual wiring
You don’t pass values around manually anymore.
No more:
val ctx = ...
val enriched = ...
3. Compile-time safety
This is the key:
If it compiles, your async wiring is correct.
You literally can’t swap values accidentally.
4. Code reads like execution
Instead of:
“how is this executed?”
You read:
“this runs together → then this → then this depends on that”
Performance?
Same as raw coroutines.
- no reflection
- no runtime code generation
- zero overhead
Benchmarks: ~identical execution time.
When this is useful
This shines when you have:
- multi-service orchestration
- dashboards
- checkout flows
- aggregation pipelines
- anything with parallel + dependent steps
When you don’t need it
If you have:
- 1–2 coroutine calls
- no dependency structure
👉 stick with plain coroutines
Final thought
Coroutines solve concurrency.
KAP solves orchestration.
Links
- GitHub: https://github.com/damian-rafael-lattenero/kap
- Docs: https://damian-rafael-lattenero.github.io/kap/
- Quickstart: https://damian-rafael-lattenero.github.io/kap/guide/quickstart/
If this resonates, I’d love feedback 🙌
Top comments (0)