Single Source of Truth
The Problem Without Repositories
Without a Repository, a ViewModel directly calls the API service and the database. When the same data is fetched from two places, the app can show inconsistent results — the database says one thing, the API says another. The Repository pattern fixes this by providing a single point of access for all data operations.
What is a Repository?
A Repository is a class that mediates between different data sources (network, database, shared preferences) and the rest of the app. It decides which source to read from, when to refresh, and how to cache. The ViewModel asks the Repository for data — it never knows whether that data came from Room, Retrofit, or a local file.
Single Source of Truth
The database is typically the single source of truth. The Repository writes API responses to the database, then reads from the database to emit to the UI. This guarantees the UI always sees consistent, up-to-date data.
class ArticleRepository(
private val api: NewsApi,
private val dao: ArticleDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
fun getArticles(): Flow<List<Article>> {
return dao.getAllArticles()
.flowOn(dispatcher)
}
suspend fun refreshArticles() {
withContext(dispatcher) {
val response = api.fetchTopHeadlines()
val entities = response.articles.map { it.toEntity() }
dao.upsertAll(entities)
}
}
}
The ViewModel calls getArticles() and gets a reactive Flow from the database. refreshArticles() fetches from the network and writes to the database. The Flow automatically updates the UI when new data arrives.
Caching Strategies
Cache-First
Show cached data immediately, then fetch fresh data in the background. Good for content that changes infrequently and where showing stale data is better than showing a loading spinner.
fun getArticles(): Flow<Resource<List<Article>>> = networkBoundResource(
query = { dao.getAllArticles() },
fetch = { api.fetchTopHeadlines() },
saveFetchResult = { response ->
dao.upsertAll(response.articles.map { it.toEntity() })
},
shouldFetch = { cachedArticles ->
cachedArticles.isEmpty() || isStale(cachedArticles)
}
)
Network-First
Always fetch from the network. Use the cache only as a fallback when offline. Good for data that must be current — live prices, chat messages, notifications.
fun getStockPrice(symbol: String): Flow<Resource<StockPrice>> = flow {
emit(Resource.Loading())
try {
val price = api.getStockPrice(symbol)
dao.upsertPrice(price.toEntity())
emit(Resource.Success(price))
} catch (e: Exception) {
val cached = dao.getPrice(symbol)
if (cached != null) {
emit(Resource.Success(cached.toDomain()))
} else {
emit(Resource.Error(e.message ?: "Network error"))
}
}
}
Network-Only
No local caching. Good for data that is never reused — search results, one-time API calls.
Choosing a Strategy
| Strategy | When to Use | Trade-off |
|---|---|---|
| Cache-First | News feeds, profiles, settings | May show stale data briefly |
| Network-First | Live data, prices, chat | Slower initial load |
| Network-Only | Search, one-time calls | No offline support |
| Cache + Refresh | Social feeds, notifications | Best UX, most complex |
Mapping and Error Handling
Data Mapping
API models (DTOs), database models (Entities), and domain models are often different. The Repository is responsible for converting between them.
// API DTO
data class ArticleDto(
@SerializedName("title") val title: String,
@SerializedName("urlToImage") val imageUrl: String?,
@SerializedName("publishedAt") val publishedAt: String
)
// Database Entity
@Entity(tableName = "articles")
data class ArticleEntity(
@PrimaryKey val title: String,
val imageUrl: String?,
val publishedAt: String
)
// Domain Model
data class Article(
val title: String,
val imageUrl: String?,
val publishedAt: LocalDateTime
)
// Mappers
class ArticleMapper {
fun dtoToEntity(dto: ArticleDto) = ArticleEntity(
title = dto.title,
imageUrl = dto.imageUrl,
publishedAt = dto.publishedAt
)
fun entityToDomain(entity: ArticleEntity) = Article(
title = entity.title,
imageUrl = entity.imageUrl,
publishedAt = LocalDateTime.parse(entity.publishedAt)
)
}
Keeping mappers in a dedicated class makes them testable and reusable.
Error Handling
The Repository wraps data operations in a Result type so the ViewModel never catches exceptions directly.
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(data: T? = null) : Resource<T>(data)
}
suspend fun getUser(id: String): Resource<User> {
return try {
val user = api.fetchUser(id)
Resource.Success(user)
} catch (e: IOException) {
Resource.Error("Check your internet connection")
} catch (e: HttpException) {
Resource.Error("User not found")
}
}
The ViewModel renders UI based on Resource.Loading, Resource.Success, or Resource.Error without ever catching exceptions.
Quiz
1. What is the primary purpose of the Repository pattern?
2. In a cache-first strategy, when does the Repository fetch from the network?
3. Why should API DTOs, database Entities, and domain models be kept separate?
4. Which caching strategy is best for live stock prices?
Flashcards
Question
What is a single source of truth in the Repository pattern?
Click to reveal answer
Answer
The database is typically the single source of truth. The Repository writes API responses to the database, then reads from the database to emit to the UI, ensuring consistency.
Question
What is the difference between cache-first and network-first strategies?
Click to reveal answer
Answer
Cache-first shows cached data immediately and refreshes in background. Network-first always fetches from the network and uses cache only as a fallback when offline.
Question
Why should the Repository wrap results in a Resource sealed class?
Click to reveal answer
Answer
It standardizes Loading/Success/Error states so the ViewModel never catches exceptions directly and the UI renders consistently for all data operations.
Question
What three model types does a typical Repository map between?
Click to reveal answer
Answer
API DTOs (network), Database Entities (local), and Domain Models (business logic). Each layer has its own model to maintain separation of concerns.
Revision Notes
Key Takeaways
- 1. The Repository provides a single source of truth for all data access
- 2. The database is the source of truth — API data is written there first
- 3. Choose caching strategy based on data freshness requirements
- 4. Separate DTOs, Entities, and domain models to decouple layers
Interview Tips
- • Explain how a Repository abstracts data sources — the ViewModel doesn't know if data is from API or database
- • Discuss cache-first vs network-first with concrete examples
- • Describe the Resource sealed class pattern for standardized error handling
- • Know why you map between DTOs, Entities, and domain models
Cheat Sheet
Repository Pattern Cheat Sheet
Role: Single source of truth mediating between data sources and the app.
Data Flow:
ViewModel → Repository → API/Database
↓
Database (single source of truth)
↓
UI (observes Flow)
Caching Strategies:
- Cache-First: Show cache, refresh background (news, profiles)
- Network-First: Always fetch, cache fallback (prices, chat)
- Network-Only: No cache (search, one-time calls)
Models:
- API DTO → Database Entity → Domain Model
- Mapper classes handle conversions
Error Handling:
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String) : Resource<T>(message = message)
class Loading<T> : Resource<T>()
}
Key Rule: ViewModel never calls API or Database directly — always through Repository.