DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on • Originally published at mvpfactory.io

Kotlin Context Parameters in Practice

---
title: "Kotlin Context Parameters: Less Boilerplate, Clearer Code"
published: true
description: "Kotlin 2.2 context parameters eliminate dependency threading and cut DI boilerplate in KMP. Three production patterns and essential migration gotchas."
tags: kotlin, android, architecture, mobile
canonical_url: https://blog.mvp-factory.com/kotlin-context-parameters-less-boilerplate-clearer-code
---

## What We're Building

If you've threaded a transaction or auth token through six layers of function parameters, you already know the pain. Today I'll walk you through three production patterns using Kotlin 2.2's context parameters that eliminate manual dependency threading, scope database transactions without `ThreadLocal` hacks, and propagate auth context through clean architecture layers. In one of our production KMP modules with ~80 use-case functions, migrating to context parameters eliminated roughly a third of the dependency-forwarding boilerplate.

Let me show you a pattern I use in every project.

## Prerequisites

Context parameters require **Kotlin 2.2.0+** with an opt-in compiler flag:

Enter fullscreen mode Exit fullscreen mode


kotlin
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
}


If migrating from context receivers, remove the `-Xcontext-receivers` flag first.

## Step 1: Understand Why Context Receivers Failed

Context receivers, experimental since Kotlin 1.6.20, brought the receiver's members directly into scope. This caused ambiguity when multiple contexts shared member names — the compiler couldn't resolve which `log()` you meant when both `Logger` and `AuditTrail` were in context.

Context parameters fix this by being named and explicit. They don't pollute the member scope:

Enter fullscreen mode Exit fullscreen mode


kotlin
// Old context receivers (deprecated)
context(Logger)
fun processOrder(order: Order) {
info("Processing ${order.id}") // Whose info()? Ambiguous.
}

// Kotlin 2.2 context parameters
context(logger: Logger)
fun processOrder(order: Order) {
logger.info("Processing ${order.id}") // Explicit. Clear. Done.
}


## Step 2: Replace Service Locators in KMP

Most teams reach for Koin or manual service locators in `commonMain`, then fight platform-specific initialization order. Here is the minimal setup to get this working — context parameters let you thread dependencies at the call-site level without a framework:

Enter fullscreen mode Exit fullscreen mode


kotlin
context(repo: OrderRepository, auth: AuthContext)
fun placeOrder(items: List): Result {
val user = auth.currentUser
return repo.save(Order(userId = user.id, items = items))
}

// Contexts propagate automatically through the call chain
context(repo: OrderRepository, auth: AuthContext)
fun handleCheckout(cart: Cart): Result {
validate(cart)
return placeOrder(cart.items) // No need to forward repo or auth
}


At the outermost boundary — your HTTP handler, ViewModel, or test — you provide the concrete instances once. Every function inward receives them implicitly through context propagation. Context parameters are the only option that combines compile-time safety with minimal boilerplate across all KMP targets.

## Step 3: Scope Database Transactions

`ThreadLocal` works on JVM but breaks in coroutines and doesn't exist on iOS/JS targets. Context parameters handle this cleanly:

Enter fullscreen mode Exit fullscreen mode


kotlin
context(tx: Transaction)
fun transferFunds(from: Account, to: Account, amount: Money) {
tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from.id)
tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to.id)
}

inline fun Database.transactional(
block: context(Transaction) () -> T
): T {
val tx = beginTransaction()
try { return block(tx).also { tx.commit() } }
catch (e: Exception) { tx.rollback(); throw e }
}

// Transaction scope enforced by the compiler
database.transactional {
transferFunds(checking, savings, Money(500))
}


No `ThreadLocal`. No coroutine context element hacks. The transaction scope is visible in the type signature and the compiler enforces it.

## Step 4: Propagate Auth and Tenant Context

In multi-tenant SaaS backends, propagating tenant context through clean architecture layers typically means adding a parameter to every use case, repository, and service call. Context parameters collapse this entirely:

Enter fullscreen mode Exit fullscreen mode


kotlin
context(tenant: TenantContext, auth: AuthContext)
fun executeUseCase(): Result {
return generateReport() // Automatically forwards both contexts
}


This is where context parameters earn their keep. The boilerplate reduction is real, and the intent reads clearly: this function operates within a tenant and auth scope. Period.

## Gotchas

Here is the gotcha that will save you hours:

**Overload resolution** can trip you up. Two functions differing only by context parameter types can confuse the compiler. Keep context parameter lists distinct or use named invocations.

**Type inference with generics** gets shaky when context parameters interact with generic return types. The compiler occasionally needs explicit type arguments. This improves with each 2.2.x patch, but expect to add a few type annotations you wish you didn't need.

**Coroutine interaction** is the subtlest issue. Context parameters are lexically scoped, so they remain available after suspension points within the same function body. But `launch { }` and `async { }` create new scopes that don't automatically inherit context parameters from the parent. You must explicitly provide them in the new scope. Use `CoroutineContext` for coroutine-specific concerns and context parameters for domain-level dependencies.

## Wrapping Up

Introduce context parameters at your use-case entry points first — the HTTP handler or ViewModel layer — and let them propagate inward. Don't refactor everything at once. Auth, tenant, and transaction scoping are ideal candidates. Loggers and metrics are reasonable. Coroutine dispatchers and platform-specific services belong in `CoroutineContext` or platform DI.

The docs don't mention this, but if you're migrating from context receivers, the process is mechanical: add names to every context declaration, replace member-style calls with named references, and let the compiler guide you. Budget one sprint for a medium-sized KMP module. In our experience, the reduction in DI ceremony paid for itself quickly.

For more details, check the [Kotlin context parameters KEEP](https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md) and the [Kotlin 2.2 release notes](https://kotlinlang.org/docs/whatsnew22.html).
Enter fullscreen mode Exit fullscreen mode

Top comments (0)