Repository Pattern — a pattern that adds an abstraction layer between business logic and data sources. Instead of directly calling the API, database, or cache, the Repository provides a unified interface for obtaining and storing data. This simplifies testing and switching between sources. Read more in the Android Data Layer documentation.
Key Takeaways
Repository Pattern is a structural pattern that isolates business logic from direct access to data sources. Instead of having an Activity, UIViewController, or ViewModel directly call Retrofit, URLSession, Room, or CoreData, they communicate with the Repository. The Repository decides where to get the data — from the network, database, or cache — and returns the result in a unified format. This implements the single responsibility principle — the UI does not know how or from where the data was obtained.
Repository components include an interface (protocol), an implementation, and one or more DataSources. A DataSource is a class that works with a single source: RemoteDataSource calls the API via an HTTP client, LocalDataSource reads and writes to the database. The Repository receives DataSources through the constructor (Dependency Injection) and decides which source to use. For example, when requesting a list of users, the Repository first checks the cache, then the database, then the network.
Benefits of the Repository Pattern: isolation of data source changes (API changes, database migrations) does not affect the UI layer; unit testing by mocking Repository or DataSource; caching is transparent to the UI; switching between online and offline modes without changing screen logic. The Android community recommends Repository as a mandatory layer in Clean Architecture.
iOS implementation of Repository is built on Swift protocols. The Repository protocol declares methods for obtaining and storing data. The actual implementation is injected through the initializer — this allows replacing the implementation in tests and SwiftUI previews. DataSources are also declared as protocols: Protocol RemoteDataSource, Protocol LocalDataSource. The ViewModel or Interactor does not know about a specific implementation — only about the Repository protocol.
protocol UserRepository {
func getUsers() async throws -> [User]
}
protocol UserRemoteDataSource {
func fetchUsers() async throws -> [User]
}
protocol UserLocalDataSource {
func getCachedUsers() throws -> [User]
func saveUsers(_: [User]) throws
}
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
init(remote: UserRemoteDataSource, local: UserLocalDataSource) {
self.remote = remote
self.local = local
}
func getUsers() async throws -> [User] {
if let cached = try? local.getCachedUsers() {
return cached
}
let users = try await remote.fetchUsers()
try local.saveUsers(users)
return users
}
}
Dependency Injection in iOS for Repository is typically configured through a factory or DI container (Swinject, Factory). In tests, the UserRepository protocol is replaced with a mock implementation that returns predefined data. Async-await makes the code synchronous and readable without closures and delegates. For Combine reactivity, Repository methods return AnyPublisher instead of async throws.
Android implementation of Repository extensively uses Kotlin Coroutines and Flow for asynchronous operations. Google recommends Repository in the official Android architecture guide (Android Architecture Components). The Repository accepts RemoteDataSource (Retrofit) and LocalDataSource (Room) through the constructor, and the ViewModel subscribes to a Flow from the Repository. The Repository manages the data strategy: cache-first, network-first, or always network with cache write.
interface UserRepository {
fun getUsers(): Flow<Result<List<User>>>
}
interface UserRemoteDataSource {
suspend fun fetchUsers(): List<User>
}
interface UserLocalDataSource {
fun getCachedUsers(): Flow<List<User>>
suspend fun saveUsers(users: List<User>)
}
class UserRepositoryImpl(
private val remote: UserRemoteDataSource,
private val local: UserLocalDataSource
) : UserRepository {
override fun getUsers(): Flow<Result<List<User>>> = flow {
emit(Result.Loading)
local.getCachedUsers().collect { cached ->
if (cached.isNotEmpty()) {
emit(Result.Success(cached))
}
}
try {
val users = remote.fetchUsers()
local.saveUsers(users)
emit(Result.Success(users))
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}
Result wrapper in the example above is standard for Android: a sealed class Result informs the ViewModel about the loading state (Loading, Success, Error). The ViewModel subscribes via collect and updates StateFlow or LiveData. Repository with Flow automatically notifies the UI of database changes — this is a key difference from one-shot requests where the UI does not know about changes without manual refresh.
DataSource — classes responsible for working with a specific data source. RemoteDataSource uses an HTTP client (URLSession, Retrofit, Ktor) to fetch data from the API. LocalDataSource works with local storage (CoreData, Realm, Room, UserDefaults, DataStore). Each DataSource has a narrow responsibility: RemoteDataSource only knows about the API request format, LocalDataSource — about the database schema. The Repository combines them, implementing a caching strategy.
| DataSource | iOS Platform | Android Platform | Source |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | REST / GraphQL API |
| Local (DB) | CoreData, SwiftData | Room, SQLDelight | SQLite on device |
| Local (cache) | NSCache, UserDefaults | DataStore, EncryptedSP | In-memory / disk |
| Preference | UserDefaults, Keychain | SharedPreferences, EncryptedSP | Settings, tokens |
Caching strategies in Repository: Cache-First (cache first, then background fetch), Network-Only (network only, for payment screens), Network-First-With-Cache-Backup (network first, fallback to cache on error). The choice of strategy depends on the scenario: a country list can be cached for a long time, currency rates — for 15 minutes, wallet balance — network only. The Repository implements the strategy and changes it without modifying the ViewModel or UI.
Repository and Service are different patterns with overlapping functions. Repository is responsible for data access and caching, returning data models. Service (or Interactor, Use Case) contains business logic: validation, data transformation, orchestrating multiple Repository calls. Service can combine UserRepository, OrderRepository, and NotificationRepository to process an order. Repository does not contain business logic — only CRUD and caching.
When to choose Repository — data navigation with multiple sources (API + DB + cache), offline-first architecture, the need for caching and transparent source switching. Repository is mandatory in Clean Architecture and is recommended by Google for Android applications. In VIPER architecture on iOS, the Repository role is performed by the Interactor layer, interacting with Manager or Service for data access.
When Service is enough — simple applications with a single data source, read-only screens without writing, projects without offline mode. In such cases, DataSource is used directly by the ViewModel or Presenter, and Repository becomes an unnecessary layer. However, adding Repository early does not require significant effort and simplifies adding caching and tests in the future.
Frequently Asked Questions
DataSource is a class that works with a single source (API, DB, cache). Repository is a class that manages multiple DataSources and provides a unified interface. The Repository decides which DataSource to use and coordinates caching. DataSource does not know about other sources; Repository does not know the implementation details of each source.
Yes, Repository is useful in SwiftUI for separating data from View. The ViewModel subscribes to a Publisher from the Repository, and the Repository manages caching and synchronization. In simple applications, you can use URLSession directly in the ViewModel, but for testability and scalability, Repository is preferred. Apple does not enforce the pattern, but it is compatible with SwiftData and Network.framework.
DataSources are replaced with mock objects through Dependency Injection. The test creates a mock RemoteDataSource (returns predefined JSON) and a mock LocalDataSource (verifies data is saved). The Repository is tested in isolation: caching strategy, error handling, and correct call order are verified. For integration tests, TestDispatcher (Kotlin) or MainActor.run (Swift) is used.
It is possible but not recommended. Without a protocol, it is impossible to replace the implementation in tests and previews. In Kotlin, the Repository interface allows replacing the implementation via DI (Dagger, Hilt, Koin). In Swift, the Repository protocol is mandatory for testing async-await and Combine code. The exception is simple projects with a single data source where Repository has no caching logic.
Offline-first is a strategy where the application works without the internet using local data. Repository plays a key role: it first returns data from the local DataSource and then synchronizes with the server in the background. The user sees data instantly, and Repository updates it after loading from the network. Room with Flow provides reactive UI updates when data changes in the local database.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also