home..

Context Dependent Declarations

Context receivers enable context-dependent declarations in Kotlin.

It enables composing contexts without using inheritance, which can simplify the overall design and make your API contextual.

Consider an interfaces IDecider and EventRepository that represents some context(s) and another interface C / Command that we want to define action on, so that this action is available only in a context that provides an instance of an IDecider and an instance of an EventRepository, without having to explicitly pass them around.

context (IDecider<C, S, E>, EventRepository<C, E>)
fun <C, S, E> C.handle(): Flow<E> = fetchEvents().computeNewEvents(this).save()

context (IDecider<C, S, E>)
fun <C, S, E> Flow<E>.computeNewEvents(command: C): Flow<E> = flow {
    val currentState = fold(initialState) { s, e -> evolve(s, e) }
    val newEvents = decide(command, currentState)
    emitAll(newEvents)
}

The logic inside the context (the implementation of the handle method) is completely separated from the context implementation (IDecider, EventRepository) and could be written in different parts of the program or even in a different module.

IDecider represents the pure computation by handling Commands/C and making further decisions (publishing new events/state). EventRepository defines fetching and storing operations / side effects. Refer to the previous blog post for more.

One example of usage would be to handle the dispatched command in the context of the event-sourced1 information system:

test("Event-sourced - add number command") {
   with(decider) {
       with(numberEventRepository) {
           AddNumberCommand(Description("desc"), NumberValue(6)).handle().toList() shouldContainExactly listOf(
               NumberAddedEvent(Description("desc"), NumberValue(6))
           )
       }
   }

Notice how we have specified the contexts by using Kotlin’s scope function with.

Second example of usage would be to handle the dispatched command in the context of the state-stored2 information system:

context (IDecider<C, S, E>, StateRepository<C, S>)
suspend fun <C, S, E> C.handle(): S = fetchState().computeNewState(this).save()

context (IDecider<C, S, E>)
suspend fun <C, S, E> S?.computeNewState(command: C): S {
    val currentState = this ?: initialState
    val newEvents = decide(command, currentState)
    val newState = newEvents.fold(currentState) { s, e -> evolve(s, e) }
    return newState
}

This second handle function is available only in the IDecider and StateRepository context, and this is a different method than the one we previously defined for the event-sourced context.

test("State-stored - add number command") {
   with(decider) {
       with(numberStateRepository) {
           AddEvenNumberCommand(
               Description("desc"),
               NumberValue(4)
           ).handle() shouldBe NumberState(Description("desc"), NumberValue(4))
       }
   }
}

We are now able to handle any command/C in state-stored or event-sourced context, per need. It enables the composability of abstractions in a nice way:

The Flow

Additionally, handle functions could extend the flow of commands to support streaming and enable message-driven system patterns.

context (IDecider<C, S, E>, EventRepository<C, E>)
//using `it.handle()` function previously defined
fun <C, S, E> Flow<C>.handle(): Flow<E> = flatMapConcat { it.handle() }
context (IDecider<C, S, E>, StateRepository<C, S>)
//using `it.handle()` function previously defined
fun <C, S, E> Flow<C>.handle(): Flow<S> = map { it.handle() }

The handle method is now acting as an operator on the Flow, and it can handle the upstream flow of commands dispatched via a message broker producing a new flow of successfully stored events or the successfully stored states as a result, depending on the context.

In this example we are adding numbers in a state-stored context:

    test("State-stored - add numbers") {
        with(decider) {
            with(numberStateRepository) {
                flowOf(
                    AddNumberCommand(Description("desc"), NumberValue(6)),
                    AddNumberCommand(Description("desc"), NumberValue(4))
                ).handle().toList() shouldContainExactly listOf(
                    NumberState(Description("desc"), NumberValue(6)),
                    NumberState(Description("desc"), NumberValue(10))
                )
            }
        }
    }

In this example we are adding numbers in an event-sourced context:

     test("Event-sourced - add number") {
        with(decider) {
            with(numberEventRepository) {
                flowOf(
                    AddNumberCommand(Description("desc"), NumberValue(6)),
                    AddNumberCommand(Description("desc"), NumberValue(4))
                ).handle().toList() shouldContainExactly listOf(
                    NumberAddedEvent(Description("desc"), NumberValue(6)),
                    NumberAddedEvent(Description("desc"), NumberValue(4))
                )
            }
        }
    }

Going Forward

FModel library will use this Kotlin language feature to simplify the design. You can track the progress over the pull request on github.

We are using contexts receivers to simplify the composing and injecting of behaviors across the architecture layers. It is focusing on the information flow and how to handle that flow of messages depending on the context (for example, event-sourced or state-stored)

The usage might be much broader in the near future. Stay tuned, and happy codding!


  1. event-sourced information system: fetch events via EventRepository, execute domain components/compute via IDecider, and store new events by appending to the previous events in the storage via EventRepository 

  2. state-stored information system: fetch state via StateRepository, execute domain components/compute via IDecider, and store new state via StateRepository, by overwriting the previous state in the storage 

© 2024 fraktalio d.o.o.