skip to content

What are the practical pitfalls and trade-offs of relying on observable/vetoable for state management, and when would you avoid them?

level: principalimportance: nice to knowfreq 18%

answer

  1. Silent veto -> caller blindness
  2. No built-in thread-safety on the backing field
  3. observable fires even when old == new
  4. Reentrancy/loops if onChange reassigns
  5. Prefer StateFlow/explicit setter for complex/async state

basics

~20 s

These delegates are handy for small reactions and validation, but they hide logic in property setters, fail silently on veto, aren't thread-safe by themselves, and don't compose well. For real reactive flows or complex rules, prefer explicit setters or Flow.

solid answer

~40 s

observable/vetoable are great for lightweight, local concerns: log-on-change, mark-dirty, simple range guards. Their trade-offs: (1) **silent veto** — a rejected write gives the caller no feedback, which surprises callers and hides bugs; (2) **no thread-safety** — the shared backing field isn't synchronized, so concurrent writers race; (3) **fires on equal values** (observable does no equality check), risking redundant work or notification loops; (4) **reentrancy/loops** — if onChange reassigns the same or another observable property you can recurse; (5) **logic hidden in a delegate** is less discoverable than an explicit setter and harder to unit-test in isolation. For multi-subscriber, asynchronous, or backpressure-aware state, `StateFlow`/`SharedFlow` or `LiveData` are better; for transformation or cross-field invariants, an explicit `set(value)` with validation is clearer. Use the delegates for what they are: terse, single-purpose hooks.

go deeper

for a junior

Recognizes these are for simple reactions and basic validation.

for a middle

Names a couple of pitfalls (silent veto, fires on equal value) and a sensible alternative.

for a senior

Articulates thread-safety, reentrancy, and discoverability/testability trade-offs and picks alternatives per scenario.

for a principal

Frames the choice as an architectural one — feedback contracts, concurrency model, observer multiplicity, and maintainability — and can justify Flow vs explicit setter vs delegate per context.

## Where they shine - One-liner side effects: logging, analytics, `invalidate()` a view. - Trivial validation gates with `vetoable` (range, non-blank). - Late non-null primitives with `notNull()`. ## Pitfalls ### 1. Silent veto (caller blindness) `vetoable` returning `false` discards the write with **no signal**. `obj.x = bad; print(obj.x)` shows the old value, but most call sites won't re-read and check. This is a classic source of 'why didn't my value stick?' bugs. Mitigation: throw inside the lambda for hard invariants, or use an explicit setter returning a result. ### 2. Not thread-safe ```kotlin var counter: Int by Delegates.observable(0) { _, _, _ -> } ``` The `ObservableProperty` backing field is a plain `var`. Concurrent writers can race on both the field and the callback; `afterChange` may observe interleaved states. You must add your own synchronization (`@Synchronized`, `Mutex`, atomics) if shared across threads. ### 3. Fires on equal values `observable` does not compare old vs new; assigning the same value still runs `onChange`. This can cause redundant recomputation or, worse, notification storms. Add your own `if (old != new)` guard inside the lambda. ### 4. Reentrancy and loops If `onChange`/`beforeChange` writes back to the same property (or a chain of observable properties), you can recurse or deadlock. The stdlib provides no reentrancy guard. ### 5. Discoverability and testing Logic buried in a delegate factory call is easy to overlook in review and awkward to test independently of the owning class. An explicit `var x: T = init; set(value) { ... }` keeps the rule next to the property and is trivially testable. ### 6. No multi-subscriber / async story These hooks are synchronous and single-callback. They don't support multiple observers, coroutines, conflation, or backpressure. ## When to choose alternatives | Need | Prefer | |---|---| | Many subscribers / async / coroutines | `StateFlow` / `SharedFlow` | | Cross-field invariants or value transform | explicit custom `set(value)` | | Caller must know success/failure | method returning a result, or throw | | Android UI lifecycle-aware state | `StateFlow` (or `LiveData`) | | Just log / mark-dirty locally | `observable` is perfect | ## Rule of thumb Reach for `observable`/`vetoable` when the reaction is **local, synchronous, single-purpose, and single-threaded**. Escalate to Flow or explicit setters when you need feedback, concurrency safety, multiple consumers, or value transformation.

  • How do you avoid redundant work when observable fires on an equal value?
    Guard inside the lambda: if (old != new) { ... } — the stdlib does no equality check for you.
  • Why can vetoable's silent failure be dangerous?
    The caller gets no exception or return value, so code assumes the write succeeded; the property silently keeps its old value, masking bugs.
  • When would StateFlow beat observable for state?
    When you need multiple subscribers, coroutine/async consumption, conflation/backpressure, or lifecycle-aware collection — observable is single, synchronous, and callback-only.

They're sticky notes on a single drawer — fine for a quick reminder, but a poor substitute for a shared, audited ledger when many people and threads touch the data.

saying these in an interview costs you the question

  • Assuming the delegates are thread-safe out of the box
  • Using vetoable for invariants that callers must be told about, without throwing
  • Not realizing observable fires on equal assignments
  • Building a whole reactive architecture on these instead of Flow
  • Ignoring reentrancy when onChange writes other properties

context