Sparround

Clean Architecture & the Repository pattern

Clean Architecture splits code into layers with a strict dependency direction:

  • Presentation (UI + ViewModels) — what the user sees
  • Domain (use cases, business models, repository interfaces) — framework-free, pure Kotlin
  • Data (repository implementations, API/DB sources, DTOs) — dealing with the outside world

The dependency rule: arrows always point inward — Data and Presentation depend on Domain; Domain depends on nothing. Dependency inversion makes this possible: the interface lives in domain, the implementation in data.

The Repository pattern hides data sources (network, DB, cache) behind one facade: the ViewModel says "give me the balance" without knowing where it comes from. The repository is exactly where offline-first decisions live.

Each layer keeps its own model: AccountDto (data) → Account (domain) → AccountUiModel (presentation), with mappers between.

kotlin
// DOMAIN — təmiz Kotlin, Android importu yoxdur
data class Account(val id: String, val balance: BigDecimal)

interface AccountRepository {
    suspend fun getAccounts(): List<Account>
    fun observeAccounts(): Flow<List<Account>>
}

class GetAccountsUseCase(private val repo: AccountRepository) {
    suspend operator fun invoke(): List<Account> =
        repo.getAccounts().sortedByDescending { it.balance }
}

// DATA — Retrofit/Room burada yaşayır
class AccountRepositoryImpl @Inject constructor(
    private val api: AccountApi,
    private val dao: AccountDao
) : AccountRepository {
    override suspend fun getAccounts(): List<Account> {
        val remote = api.fetchAccounts()          // List<AccountDto>
        dao.upsertAll(remote.map { it.toEntity() })
        return dao.getAll().map { it.toDomain() }  // DB — single source of truth
    }
    override fun observeAccounts() = dao.observeAll().map { list -> list.map { it.toDomain() } }
}

The layers in code: interface in domain, implementation in data

Be ready for the "are use cases always necessary?" debate. A balanced, non-dogmatic answer is expected: on a simple CRUD screen, ViewModel → Repository directly is acceptable; use cases earn their keep when (1) logic repeats across ViewModels, (2) the operation is multi-step (a transfer: check limits → compute fees → confirm), (3) domain rules must be testable independently of UI. A banking transfer flow is the ideal use-case example.

🛠 Practice task

Split one feature into three layers: AccountDto (data) → Account (domain) → AccountUiModel (presentation), with mappers between.

  • The domain file must contain *no `android.` or Retrofit imports** — verify it.
  • Put the repository interface in domain, the implementation in data.
  • Write one use case (e.g. GetAccountsUseCase) and justify in a sentence whether it actually earns its place.

Done when: you have a balanced answer ready for "are use cases always necessary".

📚 Sources and documentation