VOOZH about

URL: https://dev.to/damian_lattenero/stop-wiring-async-calls-manually-kotlin-coroutines-dai

⇱ Stop wiring async calls manually - DEV Community


If you’ve written non-trivial coroutine code, you’ve probably built something like this:

  • multiple async calls
  • 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


If this resonates, I’d love feedback 🙌