Hi everyone, I have a state management challenge ...
# announcements
t
Hi everyone, I have a state management challenge in our system and I'd love to hear how others might be solving this today, or to hear recommendations on how we could approach it. Our architecture is micro services based and we use mongodb for persistence. For examples sake, lets say there is an
AccountState
object in one micro service. Now for the various use cases that act on this state all have narrowed attributes of the state they're interested in. For example, Login only cares about Username and Password, whereas Change Profile Details cares about the Name and Address fields. When I retrieve data from mongo, I'd like to use a projection to only return the fields needed in the running use case and then I'd like to de-serialise that projection into something where my use cases can access those values without being cluttered with every filed that on the complete state object. ie. i don't want to create a version of the state where every value is nullable / wrapped in an Option. In typescript I would use something along these lines:
Copy code
type State {
 const id: string
 const username: string
 const password: string
 const name: string
 const address: string[]
}

type NarrowViewOfState = Pick<State, "name", "address">
But as kotlin doesn't offer the same type flexibility; has anyone had success here?
Two bits I forgot to add. Currently we're experimenting with two approaches: • Delegating a classes' properties to map • Representing the complete view of the state as an interface and then providing a
interface NarrowedView<T>
type that any narrow viewed class should extend. Obviously this is a human/process control whereas I would prefer something that still offered type saftety and additionally it leads to a lot more boilerplate which would all need to be updated any time the interface change ... and the compiler won't catch errors 😞
n
The way I'd expect this to work in Kotlin is that you'd have interfaces for the bits of state
interface NarrowViewOfState { val name: String, val address: List<String>
and then
class State(override val name: String, override val address: List<String>)
etc
compared to the typescript example; in TS you aren't repeating the type, just listing the fields you want for each narrow view, and the dependency relationship between the type and the view is inverted
it's a little more boilerplate due to having to repeat types as well as names, but not that bad. The dependency inversion, may or may not be important, I'm not sure. In the kotlin case the compiler will be pretty aggressive about enforcing things; if a mismatch is introduced there will be errors immediately.
t
If i understand it, the issue would be that
interface NarrowViewOfState
has no linkage to
interface CompleteState
... and so if a value were modified/added/removed from
CompleteState
its unlikely to be caught until runtime which I'd like to avoid? My colleague has put together a poc using reflection that allows us to do something like this:
Copy code
interface State { 
  val a: String
  val b: Int
}

interface Narrow<T>

class CompilesNarrowedState( 
  val a: String
) : Narrow<State>

class FailsToCompileBecauseUnknownPropertyNarrowState(
  val bad: String
) : Narrow<State>
If we can get this working more completely I think it'll achieve the effect we're after 🤞 I guess the bit that isn't great with this solution is we won't get feedback until compile time ... something about cake and eating it 🥮 🤔
n
complete state isn't an interface though
it's the actual type
At least, I don't understand why you'd want to do that
Either way though, CompleteState will inherit from NarrowedViewOfState
If CompleteState is the actual type, and you remove a member variable from it that is mentioned in the interface Narrowed, then yes, you will get a compilation error
because CompleteState no longer meets the interface that it claims to
I'm probably missing something but reflection and generics actually seem pretty heavy weight relative to what's being done
If you remove a property from Complete in this example you'll see it immediately fails to compile
t
ahh thanks for sharing that. sorry it hadn't quiet clicked in my brain what you were suggesting
n
yeah it's always better to have code 🙂
t
I'll have to experiment with this approach 🤔 I think one blocker might be that a service may define its own narrow view of State and that definition would resides solely inside that services namespace and git repo ... and so wouldn't be available for the State to implement. For example:
Copy code
// Repo 1 shared code between all services
package example.common.kernel

interface State {
  val a: String
  val b: String
}

interface Narrow<T>

// Repo 2 for a specified service that operates on one part of state
package example.a

import example.commons.kernel.State
import example.commons.kernel.NarrowView

data class(val a: String): NarrowView<State>

// Repo 3 for another service that operates on a different part of state
package example.b

import example.commons.kernel.State
import example.commons.kernel.NarrowView

data class(val b: String): NarrowView<State>
Within Service A and Service B each would then have its own projection to only include the fields from that state object when touching the database. I should point out this is for reads no writes and I'm conscious that we've got some issues with shared state between services 🙂
But with the approach you're suggesting, those interfaces could just reside in our common package and then their implementing data classes could reside in the services which might work nicely
Thanks for your help ... going to give your suggestion a try 🤞