Skip to content
intermediate Phase 6 · Architecture Patterns

Repository Pattern

Abstract data sources behind repositories for single source of truth and testability.

40m
2 problems
Topic Progress 0%

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?

Question 1 options

2. In a cache-first strategy, when does the Repository fetch from the network?

Question 2 options

3. Why should API DTOs, database Entities, and domain models be kept separate?

Question 3 options

4. Which caching strategy is best for live stock prices?

Question 4 options

Flashcards

Question

What is a single source of truth in the Repository pattern?

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?

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?

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?

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.