Skip to main content

Fallback Chains

The otherwise infix operator lets you chain multiple handlers for the same request or notification type. Each candidate is tried in order — the first one that succeeds wins, and the rest are skipped. If every handler throws, the last exception is re-thrown.

PrimaryHandler() → (throws?) → SecondaryHandler() → (throws?) → TertiaryHandler()
↓ re-throws if this also throws

This is useful any time you want resilient dispatch without wrapping your handlers in try/catch:

  • Call a live API, fall back to a cache, fall back to a stub.
  • Try a fast in-memory path, fall back to a database.
  • Route to a feature-flagged implementation, fall back to the stable one.

Requests

class CreateOrderHandler(private val api: OrderApi) : RequestHandler<CreateOrderCommand, Order> {
override suspend fun handle(mediator: Mediator, requestContext: RequestContext, request: CreateOrderCommand): Order =
api.create(request.cartId) // throws if the live API is down
}

class CreateOrderFallbackHandler(private val cache: OrderCache) : RequestHandler<CreateOrderCommand, Order> {
override suspend fun handle(mediator: Mediator, requestContext: RequestContext, request: CreateOrderCommand): Order =
cache.createFromCache(request.cartId)
}

class CreateOrderStubHandler : RequestHandler<CreateOrderCommand, Order> {
override suspend fun handle(mediator: Mediator, requestContext: RequestContext, request: CreateOrderCommand): Order =
Order.stub()
}

Register the chain with otherwise:

class OrderRegistrar(
private val api: OrderApi,
private val cache: OrderCache,
) : MediatorRegistrar {
override fun register(registry: HandlerRegistry) {
registry register (
CreateOrderHandler(api)
otherwise CreateOrderFallbackHandler(cache)
otherwise CreateOrderStubHandler()
)
}
}

Dispatch is unchanged — callers don't know a fallback chain exists:

val order: Order = mediator.send(CreateOrderCommand(cartId = "cart-42"))

Notifications

class PushNotificationHandler(private val push: PushService) : NotificationHandler<OrderShippedNotification> {
override suspend fun handle(notification: OrderShippedNotification) =
push.send(notification.userId, "Your order shipped!") // throws if push service is down
}

class EmailNotificationHandler(private val email: EmailService) : NotificationHandler<OrderShippedNotification> {
override suspend fun handle(notification: OrderShippedNotification) =
email.send(notification.userId, "Your order shipped!")
}
class NotificationRegistrar(
private val push: PushService,
private val email: EmailService,
) : MediatorRegistrar {
override fun register(registry: HandlerRegistry) {
registry registerNotification (
PushNotificationHandler(push) otherwise EmailNotificationHandler(email)
)
}
}

Behaviour at a glance

ScenarioResult
First handler succeedsReturns immediately, rest are skipped
First throws, second succeedsSecond result returned
All handlers throwLast exception re-thrown
Single otherwise callCreates a FallbackRequestHandler with two candidates
Chained otherwise callsAll candidates collected into one handler — no nesting

+ DSL shorthand

otherwise returns a FallbackRequestHandler / FallbackNotificationHandler, both of which implement the standard handler interfaces, so the + shorthand inside a scope { } block works as-is:

registry.scope {
+(CreateOrderHandler(api) otherwise CreateOrderFallbackHandler(cache))
+(PushNotificationHandler(push) otherwise EmailNotificationHandler(email))
}

Next

Exceptions — per-request exception handlers that transform errors into responses