Repositories, use cases, and single source of truth
Repository owns application-data policy
interface ProductsRepository {
fun observeProduct(id: ProductId): Flow<Product?>
suspend fun refresh(id: ProductId)
suspend fun updateFavorite(id: ProductId, favorite: Boolean)
}
It abstracts sources, centralizes mutation, defines cache/network conflict rules, and exposes domain/application data. It should not expose DAO entities or Retrofit calls as its public contract.
Database-backed source of truth
fun observeProduct(id: ProductId): Flow<Product?> =
dao.observe(id).map { it?.toDomain() }
suspend fun refresh(id: ProductId) {
val remote = api.getProduct(id.value)
dao.upsert(remote.toEntity())
}
UI observes database; refresh writes database. If UI alternates between direct network and database results, two truths race and offline behavior diverges.
Mutation and optimistic UI
For favorite toggle:
- persist local desired state/pending operation transactionally
- UI immediately observes it
- sync attempts network
- success clears pending marker
- failure follows explicit rollback/retry/conflict policy
Do not update only ViewModel state and hope later refresh agrees.
Use cases add policy, reuse, or orchestration
class PlaceOrder(
private val cart: CartRepository,
private val orders: OrderRepository,
private val inventory: InventoryRepository,
) {
suspend operator fun invoke(): OrderId { /* validate + coordinate */ }
}
A pass-through GetProductsUseCase is optional. Add it when it creates a useful capability boundary, shared policy, or simplifies multiple consumers—not to satisfy a diagram.
Immutable UI state
data class ProductUiState(
val product: ProductUi? = null,
val loading: Boolean = true,
val refreshing: Boolean = false,
val failure: ProductFailure? = null,
)
Expose StateFlow<ProductUiState>, not MutableStateFlow. Use atomic update; model mutually exclusive states with sealed types when appropriate. UI state is a presentation projection, not necessarily the application-data source of truth.
Contract details to document
- cold/hot stream and replay behavior
- threading/main safety
- null versus not-found versus error
- freshness and refresh semantics
- cancellation behavior
- ordering/deduplication
- offline mutation/conflict behavior
- fake implementation equivalence
“Repository” is valuable when it owns data policy. “Use case” is valuable when it owns an operation. “SSOT” is valuable only when all writers/readers actually honor it.
Sources
Interview exercises
- Design a repository contract for offline favorites.
- Identify two truths in UI that reads network and Room independently.
- Decide whether three proposed pass-through use cases should exist.