Skip to main content

A/B Testing & Feature Flags

MediatorK makes A/B testing straightforward: define one command per variant, register a handler for each, then let the caller pick which command to send based on a feature flag. The mediator and handlers stay completely unaware of each other.


How it works​

Each variant is its own command that shares the same response type. The flag decision happens at the call site, before send() is called.

// Two variants — same response type, different implementations
data class StripePaymentCommand(val orderId: String, val amount: Double) : Request<PaymentResult>
data class NetworkPaymentCommand(val orderId: String, val amount: Double) : Request<PaymentResult>

Register both handlers at startup:

class PaymentRegistrar : MediatorRegistrar {
override fun register(registry: HandlerRegistry) {
registry.scope {
+StripePaymentHandler()
+NetworkPaymentHandler()
}
}
}

At the call site, fetch the flag and send the matching command:

val useStripe = featureFlags.isEnabled("stripe_checkout")

val result = mediator.send(
if (useStripe) StripePaymentCommand(orderId, amount)
else NetworkPaymentCommand(orderId, amount)
)

Android ViewModel example​

class CheckoutViewModel(
private val mediator: Mediator,
private val featureFlags: FeatureFlagService,
) : ViewModel() {

val paymentResult = MutableStateFlow<PaymentResult?>(null)

fun pay(orderId: String, amount: Double) {
viewModelScope.launch {
val useStripe = featureFlags.isEnabled("stripe_checkout")

paymentResult.value = mediator.send(
if (useStripe) StripePaymentCommand(orderId, amount)
else NetworkPaymentCommand(orderId, amount)
)
}
}
}

Why this works well​

Detail
No couplingHandlers know nothing about flags or each other
Easy to clean upWhen the experiment ends, delete one command + one handler
TestableTest each variant independently by sending its command directly
Pipeline applies to bothLogging, auth, and other behaviors wrap both variants automatically

Next​

→ Request Context