Feature flags let you ship code behind a switch: turn a feature on or off without a new deploy, run A/B tests, and roll back in seconds when something goes wrong. What started as a “dev tool” has become a platform capability: product teams use flags to ship faster and experiment; platform and SRE teams use them to control risk and rollout across many services; compliance and audit need a clear record of who changed what and when. The choice of how you run and integrate flags — especially when your backend is Kotlin and Ktor — affects the whole stack: type safety, request lifecycle, and whether you treat flags as first-class or as an afterthought.

If your stack is Kotlin and Ktor but the flag service is built for another ecosystem, you get polyglot SDKs, stringly-typed keys, blocking or callback-based APIs, and no native place in your request pipeline. This post explains why a Kotlin-native approach pays off at the level of a single service and at the level of a platform, and how to use Flagent in a Ktor app with real code and real trade-offs.

Why feature flags matter (beyond a single release)

Without flags, every release is all-or-nothing: you merge, deploy, and hope. With flags you can:

  • Ship safely — Deploy code with the feature off, then enable it for 1%, 10%, or 100% of users from the admin UI. No redeploy to roll back: flip the flag off. At scale this becomes a release valve: one bad deploy doesn’t force a full revert; you disable the offending feature and fix forward.
  • Run experiments — Assign users to variants (e.g. control vs treatment) deterministically. Same user always gets the same variant; you measure impact instead of guessing. Over time, experiments become a habit: every big change can be validated with data before full rollout.
  • Target by context — Show a feature only to certain regions, tiers, or segments using rules (e.g. country == "US", tier in ["premium"]). Useful for canary by region, beta for internal users, or compliance (e.g. don’t show feature X in EU until legal signs off).

Once multiple teams and services depend on flags, the system becomes part of your platform: you need stable evaluation semantics, clear ownership of flag keys, and a way to avoid “flag sprawl” (hundreds of stale flags). A Kotlin-native platform gives you one language and one mental model from backend to admin UI to (with KMP) mobile — and keeps evaluation in the same process and coroutine world as your routes. The catch with a non-native service: stringly-typed keys, blocking or callback-based APIs, and no first-class integration with your request lifecycle. That’s where Kotlin-native pays off.

Why Kotlin-native matters

When the platform and SDKs are Kotlin-first, you get concrete benefits:

  • Type safety — Flag keys and evaluation context can be typed (e.g. generated FlagKeys or enums). The IDE autocompletes and the compiler catches typos before runtime. No more isEnabled("new_paymnet_flow") shipping to production.
  • Coroutines — Evaluation and config refresh fit naturally into suspend functions and structured concurrency. No callback soup, no blocking calls in request handlers. The Ktor plugin uses the same coroutine context as your routes.
  • Ktor ecosystem — A Ktor plugin means evaluation runs inside your request pipeline. You get getFlagentClient() from the application or call and call evaluate(flagKey, entityId, context) without managing a separate client or connection pool. Same process, same lifecycle.
  • One language — Backend (Ktor), admin UI (Compose for Web), and mobile (Kotlin Multiplatform) speak the same language. No “feature-flag DSL” or second-class SDK: it’s just Kotlin and your existing patterns.

How Flagent fits

Flagent is the first Kotlin-native feature-flag platform: the backend is Ktor, the admin UI is Compose for Web, and the SDKs are Kotlin-first (including KMP for Android and multiplatform). You get flags, A/B experiments, segments, targeting, and evaluation counts in one deploy. For server-side evaluation you add the Ktor plugin and use it in a route.

1. Add the plugin (Gradle)

Include the Flagent Ktor plugin and point it at your Flagent server. From a typical Ktor project:

// build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-server-core-jvm:2.x.x")
    implementation("io.maxluxs.flagent:ktor-flagent:0.x.x")  // or local project
}

2. Install and configure in Application

// Application.kt
fun Application.module() {
    install(ContentNegotiation) { json() }
    installFlagent {
        flagentBaseUrl = environment.config.property("flagent.baseUrl").getString()
        enableEvaluation = true
        enableCache = true
        cacheTtlMs = 60_000
    }
    routing {
        // your routes
    }
}

3. Evaluate in a route

Inside any route, get the client and branch on the result. Example: payment flow with two variants (control = legacy, treatment = new).

get("/pay") {
    val entityID = call.request.queryParameters["entityID"] ?: "guest-${UUID.randomUUID()}"
    val client = call.application.getFlagentClient()
    if (client == null) {
        call.respond(HttpStatusCode.ServiceUnavailable, mapOf("error" to "Flagent not available"))
        return@get
    }
    val request = EvaluationRequest(
        flagKey = "new_payment_flow",
        entityID = entityID,
        entityType = "user"
    )
    val result = client.evaluate(request)
    when (result.variantKey) {
        "treatment" -> call.respond(mapOf("flow" to "new", "message" to "Using new payment"))
        else -> call.respond(mapOf("flow" to "legacy", "message" to "Using legacy payment"))
    }
}

Evaluation is deterministic (MurmurHash3 bucketing): same entityID and rules always yield the same variant. You control rollout percentage and targeting in the Flagent UI; no code change to move from 10% to 100%.

Server-side vs client-side

In Ktor you typically use server-side evaluation: each request calls Flagent (or the in-app cache). Latency is low because the plugin caches flag state with a configurable TTL; when the cache is hot, evaluation is in-memory. For mobile or client-heavy apps, the Kotlin SDK supports client-side evaluation: fetch a snapshot once, evaluate locally on every isEnabled() with no network round-trip. Real-time updates over SSE keep the snapshot in sync when you change flags in the admin. Same API concepts — flags, segments, variants — whether you evaluate on the server or on the device. Choosing server vs client is a product and architecture decision: server gives you a single source of truth and no flag state on the device; client gives you offline resilience and zero latency after the first fetch.

When evaluation fails or Flagent is down

In production you want a clear policy when the flag service is unreachable. With the Ktor plugin you can check getFlagentClient() == null (e.g. misconfiguration or startup failure) and respond with 503 or a safe default. When the client exists but the network call fails (timeout, 5xx), the plugin and SDK can be configured to fall back to cached state or to a default variant. Design your routes so that “Flagent unavailable” doesn’t break the whole request: e.g. fall back to control or a known-safe path and log for alerting. That way a brief Flagent outage doesn’t become a full user-facing outage.

Build-time verification (optional)

To avoid typos in flag keys, use the Flagent Gradle plugin to generate FlagKeys from a YAML or from annotations. The build fails if you reference a key that doesn’t exist. See the docs on build-time verification for strict mode and generated keys.

From one service to a platform

Starting with one Ktor service and a handful of flags is enough to get value. As you add more services (or more teams), keep flag keys and semantics consistent: use a naming convention (e.g. area_feature_name), document who owns which flags, and periodically clean up flags that are 100% rolled out and no longer needed. Flagent’s OpenFeature-compatible API and optional build-time verification help keep references in sync across repos. One Kotlin-native platform means one place to train people, one audit log, and one way to reason about “what’s on for whom.”

Try it

  • Getting Started — 5-minute setup with Docker, create a flag, and call the API or SDK.
  • Tutorial: Gradual rollout on Ktor — New payment flow in 30 minutes: 10% → 50% → 100% and a kill switch.
  • Why Flagent — Value props and comparison with LaunchDarkly, Unleash, and OpenFeature.
  • Run Flagent and the Ktor sample in one command: ./scripts/run-golden-path.sh from the repo root (see Getting Started “Golden path”).

You get one platform, one language, and no second-class SDK. It’s just Kotlin — from a single app to many.