Namespace Isolation
Why namespaces prevent collisions, how they enforce separation, and when to use multiple namespaces.
The Problem: Global Shared State
Without isolation, all flags share a single global registry:
// X Global registry (all flags mixed together)
object GlobalFlags {
val darkMode = flag("dark_mode")
val paymentProcessing = flag("payment_processing")
val analyticsEnabled = flag("analytics_enabled")
}
Issues:
- Name collisions - Two teams pick the same flag name
- Coupled lifecycle - Updating one domain's config affects others
- Blast radius - Configuration error in one domain breaks all domains
- No governance - Can't enforce team boundaries
Konditional's Solution: Namespace Isolation
Each namespace is a first-class isolation boundary: it owns its own registry, configuration lifecycle, and kill-switch.
object Auth : Namespace("auth") {
val socialLogin by boolean<Context>(default = false)
val twoFactorAuth by boolean<Context>(default = true)
}
object Payments : Namespace("payments") {
val applePay by boolean<Context>(default = false)
val stripeIntegration by boolean<Context>(default = true)
}
Guarantees:
- Separate registries -
AuthandPaymentshave independentNamespaceRegistryinstances - Independent lifecycle - Load/rollback/disable operations are scoped to one namespace
- Failure isolation - Parse error in
Authconfig doesn't affectPayments - No name collisions -
Auth.socialLoginandPayments.socialLogincan coexist
Mechanism 1: Per-Namespace Registry (No Global Singleton)
Each Namespace instance owns an internal registry and delegates the NamespaceRegistry API surface:
Auth.load(configuration)only updatesAuthPayments.rollback(steps = 1)only updatesPaymentsAuth.disableAll()only affectsAuthevaluations
This is why namespaces are operationally safe: isolation is enforced by construction, not convention.
Mechanism 2: Stable, Namespaced FeatureId
Each feature has:
Feature.key: String- the logical key (typically the Kotlin property name)Feature.id: FeatureId- the stable, serialized identifier used at the JSON boundary
FeatureId is encoded as:
feature::${namespaceIdentifierSeed}::${featureKey}
Example:
Auth.socialLogin.id->"feature::auth::socialLogin"Payments.socialLogin.id->"feature::payments::socialLogin"
Guarantee: Features with the same key but different namespaces have different ids (no collisions).
Mechanism 3: Type-Bound Features
The namespace type participates in the feature type:
sealed interface Feature<T : Any, C : Context, out M : Namespace>
Example:
object Auth : Namespace("auth") {
val socialLogin: Feature<Boolean, Context, Auth> by boolean<Context>(default = false)
}
object Payments : Namespace("payments") {
val socialLogin: Feature<Boolean, Context, Payments> by boolean<Context>(default = false)
}
Type safety: Auth.socialLogin and Payments.socialLogin are different types, which lets you build strongly typed
APIs that accept features from a specific namespace.
Independent Lifecycle Operations
Load
val _ = Auth
val _ = Payments
val authConfig = ConfigurationSnapshotCodec.decode(authJson).getOrThrow()
val paymentConfig = ConfigurationSnapshotCodec.decode(paymentJson).getOrThrow()
Auth.load(authConfig) // Only affects Auth registry
Payments.load(paymentConfig) // Only affects Payments registry
Rollback
Auth.rollback(steps = 1) // Only affects Auth
Payments.rollback(steps = 1) // Only affects Payments
Kill-Switch
Auth.disableAll() // Only Auth evaluations return defaults
// Payments evaluations continue normally
Failure Isolation
Parse Error in One Namespace
val authJson = """{ "invalid": true }"""
val paymentJson = """{ "valid": "config" }"""
when (val result = NamespaceSnapshotLoader(Auth).load(authJson)) {
is ParseResult.Failure -> {
// Auth parse failed
logger.error("Auth config parse failed: ${result.error}")
// Auth uses last-known-good config
}
}
when (val result = NamespaceSnapshotLoader(Payments).load(paymentJson)) {
is ParseResult.Success -> {
// Payments config loaded successfully
// Payments is unaffected by Auth parse failure
}
}
Guarantee: Parse failures in one namespace don't affect other namespaces.
When to Use Multiple Namespaces
Use Case 1: Team Ownership
sealed class TeamDomain(id: String) : Namespace(id) {
data object Recommendations : TeamDomain("recommendations") {
val COLLABORATIVE_FILTERING by boolean<Context>(default = true)
val CONTENT_BASED by boolean<Context>(default = false)
}
data object Search : TeamDomain("search") {
val FUZZY_MATCHING by boolean<Context>(default = true)
val AUTOCOMPLETE by boolean<Context>(default = true)
}
}
Benefits:
- Recommendations team owns
recommendationsnamespace - Search team owns
searchnamespace - No coordination required for config updates
Use Case 2: Different Update Frequencies
object ExperimentFlags : Namespace("experiments") {
// Updated frequently (daily experiments)
}
object InfrastructureFlags : Namespace("infrastructure") {
// Updated rarely (circuit breakers, kill switches)
}
Benefits:
- Experiment config changes don't risk infrastructure stability
- Infrastructure config has higher review standards
Use Case 3: Failure Isolation
object CriticalPath : Namespace("critical") {
val PAYMENT_ENABLED by boolean<Context>(default = true)
}
object Analytics : Namespace("analytics") {
val TRACKING_ENABLED by boolean<Context>(default = false)
}
Benefits:
- Analytics config error doesn't affect payment processing
- Critical path config has higher SLA
Anti-Pattern: Over-Segmentation
Don't:
object AuthSocialLogin : Namespace("auth-social-login")
object AuthTwoFactor : Namespace("auth-two-factor")
object AuthPasswordReset : Namespace("auth-password-reset")
Issues:
- Too many namespaces increase complexity
- No real benefit to isolation (all owned by Auth team)
Better:
object Auth : Namespace("auth") {
val socialLogin by boolean<Context>(default = false)
val twoFactorAuth by boolean<Context>(default = true)
val passwordReset by boolean<Context>(default = true)
}
Namespace Governance Patterns
Pattern 1: Sealed Hierarchy
sealed class AppDomain(id: String) : Namespace(id) {
data object Auth : AppDomain("auth")
data object Payments : AppDomain("payments")
data object Analytics : AppDomain("analytics")
}
Benefits:
- All namespaces are discoverable (sealed = exhaustive)
- Compiler prevents unknown namespaces
Pattern 2: Team Ownership via Package Structure
com.example.teams.auth.AuthFeatures : Namespace("auth")
com.example.teams.payments.PaymentFeatures : Namespace("payments")
com.example.teams.analytics.AnalyticsFeatures : Namespace("analytics")
Benefits:
- Package structure mirrors team structure
- Code ownership via CODEOWNERS file
Formal Properties
| Property | Mechanism | Guarantee |
|---|---|---|
| No identifier collisions | FeatureId includes namespace seed | Auth.socialLogin.id != Payments.socialLogin.id |
| Separate state | Different NamespaceRegistry instances | Updating Auth doesn't affect Payments |
| Independent lifecycle | Operations scoped to namespace | Auth.load(...) only affects Auth |
| Failure isolation | Parse errors scoped to namespace | Auth parse failure doesn't break Payments |
| Type binding | Feature<*, *, M> is namespace-bound | Lets you build APIs constrained to M |
Next Steps
- Fundamentals: Core Primitives - Namespace primitive
- Runtime: Operations - Lifecycle API
- Serialization Reference - JSON boundaries