Hot take : When Encapsulation Becomes Ceremony

Yves KalumeYves Kalume
2 min read

Encapsulation is one of the first concepts we learn in programming, but sometimes it turns into ritual instead of necessity. Why confusing what’s technically possible with what’s realistically a threat ?

Let’s take the common claim that you must always call .asStateFlow() to protect your state in your app.

private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

.asStateFlow() returns a read only StateFlow by wrapping the value in a ReadonlyStateFlow so consumers of the flow cannot cast back to MutableStateFlow and change the values.

I agree, it is simpler than explicitly typing a property as StateFlow<T>. No problem there.

You can also do :

private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState

But some insist you still need to call .asStateFlow() even when the property is already typed as StateFlow<T>, because “someone could cast it back to MutableStateFlow and modify it.”

That’s technically true, but it doesn’t make much sense. Casting back is a deliberate, extra step. If someone on your team goes that far, either fire them or teach them how to code.

It’s like writing a setter that just assigns a value and a getter that just returns it no logic, no validation, just boilerplate for the sake of ceremony.

class A {
    private var b: B

    fun setB(b: B) {
        this.b = b
    }

    fun getB(): B {
        return this.b
    }
}

That’s not engineering, that’s dogma. (Looking at you, Java-style OOP.)

Encapsulation is useful when it adds value: validation, invariants, side effects, meaningful contracts. But adding layers just to stop someone from intentionally breaking your design is like putting a lock on a drawer that everyone already has the key. We can use reflection to bypass your encapsulation.

Like Python devs say, we’re all adults here.

At the end of the day, .asStateFlow() isn’t the enemy, nor is it a silver bullet. It’s a tool. In a public SDK, it makes perfect sense to lock things down because you can’t control the consumers. In an internal codebase, the threat model is different.

When we start adding layers “just in case” someone takes deliberate steps to bypass our design, we’re no longer writing protective code, we’re writing ceremonial code.

Write clear contracts for your team, and reserve extra locks for when they solve real problems. Everything else is noise.

10
Subscribe to my newsletter

Read articles from Yves Kalume directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yves Kalume
Yves Kalume

Android Engineer • Google Developer Expert