# Clean Architecture And The 4-Layer Model

> The dependency rule, the four layers UI, ViewModel, Domain and Data mapped onto the repo's real modules, and one game start traced through all four layers.

- URL: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/06-clean-architecture/lesson-01-clean-architecture-overview
- Course: [Mobile Action Pro](https://dantech.academy/en/courses/kotlin-accelerator.md)
- Module: Clean Architecture in Depth
- Duration: 25 min
- Difficulty: intermediate
- Access: Free, full lesson
- Tiếng Việt: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/06-clean-architecture/lesson-01-clean-architecture-overview.md

## Objectives

- Explain the dependency rule: outer layers know inner layers, inner layers don't know outer layers
- Map the four layers UI, ViewModel, Domain and Data onto the repo's real modules
- Trace one game start from GameRoute to MockQuestionRepository and back

---

## Lesson Goal

After this lesson, you can explain what problem Clean Architecture solves, read the dependency rule, and point out which modules hold the four layers UI, ViewModel, Domain and Data in the `kotlin-accelerator-ai` repo.

The end goal: look at any class and know which layer it belongs to and what it's allowed to import.

## Part 1: The Problem Clean Architecture Solves

Let's start with a ViewModel that calls the database itself:

```kotlin
// Illustrative example, not in the repo
class GameViewModel(private val supabase: SupabaseClient) : ViewModel() {
    fun loadQuestions() = viewModelScope.launch {
        val rows = supabase.from("questions").select().decodeList<QuestionRow>()
        // mapping, filtering, error handling... all of it here
    }
}
```

This code works, but it has three costs:

- To test the ViewModel, you need a real Supabase or a very complicated mock.
- Changing the data source (for example from Supabase to a JSON file) means editing the ViewModel.
- Business logic (picking questions, calculating prize money) gets mixed up with network details.

Clean Architecture separates those concerns into layers, and sets one single rule for the direction of dependencies between them.

## Part 2: The Dependency Rule

The original rule in Robert C. Martin's article: source code dependencies can only point inward, toward higher-level policy. An inner layer knows nothing about the names of anything in an outer layer.

Applied to a mobile app:

| Layer | Knows about | Must not know about |
| --- | --- | --- |
| UI | ViewModel, UI state | Repositories, network SDKs, databases |
| ViewModel | Use cases, domain models | Supabase, DataStore, Firebase |
| Domain | Itself (models, interfaces, rules) | Compose, Android, Koin, Ktor, Supabase |
| Data | Domain interfaces to implement | UI, ViewModel |

The most important point is in the Data row. Data depends on Domain, and never the other way around. Domain declares the `QuestionRepository` interface, and Data provides the class that implements it. This technique of inverting the dependency is called Dependency Inversion.

## Part 3: The Four Layers In The Repo

```mermaid
%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "primaryBorderColor": "#475569", "secondaryColor": "#1e293b", "tertiaryColor": "#0f172a", "lineColor": "#64748b", "textColor": "#e2e8f0", "clusterBkg": "#0f172a", "clusterBorder": "#475569", "titleColor": "#cbd5e1", "edgeLabelBackground": "#334155", "fontSize": "15px"}, "flowchart": {"wrappingWidth": 420}}}%%
flowchart TB
    classDef ui fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#e2e8f0
    classDef vm fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef dom fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#e2e8f0
    classDef data fill:#1e293b,stroke:#8b5cf6,stroke-width:2px,color:#e2e8f0
    classDef root fill:#1e293b,stroke:#94a3b8,stroke-width:2px,color:#e2e8f0
    classDef bad fill:#1e293b,stroke:#ef4444,stroke-width:2px,color:#e2e8f0
    classDef good fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef note fill:#0f172a,stroke:#475569,stroke-dasharray:4 3,color:#cbd5e1
    accTitle: The four Clean Architecture layers in the repo
    accDescr: An arrow means depends on. Feature modules depend on domain. Data also depends on domain, because it implements the domain interfaces. Domain depends on no other layer. shared-app and app are where everything is assembled, and they see both domain and data.

    subgraph ROOT["Assembly"]
        direction TB
        APP[":app<br/>Android composition root"]:::root --> SHARED[":shared-app<br/>Koin wiring for Android and iOS"]:::root
    end

    subgraph FEAT["Feature: game, menu, paywall"]
        direction TB
        UI["<b>UI</b><br/>screen package, plus :ui-components, :theme<br/>GameRoute, GameScreenCompose"]:::ui
        VM["<b>ViewModel</b><br/>viewmodel package<br/>GameViewModel, MenuViewModel"]:::vm
        UI -->|"observes state, calls functions"| VM
    end

    DOMAIN["<b>Domain</b>: :domain<br/>GameStateMachine, UseCase<br/>interface QuestionRepository<br/>depends only on :dre-core"]:::dom
    DATA["<b>Data</b>: :data<br/>MockQuestionRepository<br/>LeaderboardRepositoryImpl"]:::data

    SHARED --> FEAT
    SHARED --> DATA
    VM -->|"uses use cases"| DOMAIN
    DATA -->|"implements the interface"| DOMAIN
    FEAT ~~~ NOTE["Arrow = depends on.<br/>Features don't depend on :data."]:::note
```

The repo has 14 Gradle modules (declared in [settings.gradle.kts](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/settings.gradle.kts)). The four layers map onto them like this:

| Layer | Where it lives | Real examples |
| --- | --- | --- |
| UI | The `screen` package in `:feature:game`, `:feature:menu`, `:feature:paywall`, plus `:ui-components` and `:theme` | `GameRoute`, `GameScreenCompose` |
| ViewModel | The `viewmodel` package in the feature modules | `GameViewModel`, `MenuViewModel` |
| Domain | The `:domain` module | `GameStateMachine`, `UseCase`, the `QuestionRepository` interface |
| Data | The `:data` module | `MockQuestionRepository`, `LeaderboardRepositoryImpl`, `DataModule` |

Besides the four layers, two modules do the assembly: `:shared-app` (the Koin wiring shared by Android and iOS) and `:app` (the Android composition root). They're the only places allowed to see both Domain and Data at once.

## Part 4: The Module Graph Enforces The Rule For You

In KMP, the dependency rule is written into the Gradle files as well as being a convention. Here's the KDoc at the top of [domain/build.gradle.kts](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/domain/build.gradle.kts):

```kotlin
/**
 * Pure Kotlin. Coroutines, the DRE reducer, and persistent collections.
 *
 * No Compose, navigation, Compose resources, Koin, Ktor, or Supabase.
 * Koin wiring for these types lives in `:shared-app`.
 */
```

The `commonMain` of `:domain` declares only three dependencies: `kotlinx-coroutines-core`, `:dre-core` and `kotlinx-collections-immutable`. There's no `androidMain` and no `iosMain`. So the domain code runs exactly the same on both platforms and can be tested with `commonTest`.

On the feature side, [feature/game/build.gradle.kts](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/feature/game/build.gradle.kts) declares `:core`, `:domain`, `:kernel`, `:theme`, `:ui-components`. There's no `:data`. If you try to import `MockQuestionRepository` in `GameViewModel`, the build reports an unresolved reference right away. The compiler is enforcing the dependency rule in place of code review.

## Part 5: Follow One Round Through The Four Layers

Let's follow the start-game action from the UI down to Data and back up.

UI: `GameRoute` calls `gameViewModel.ensureGameStarted()`, then collects `gameScreenUiState` and passes it to `GameScreenCompose`. The composable doesn't know where the questions come from.

ViewModel: [GameViewModel](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/feature/game/src/commonMain/kotlin/com/creative/kta/feature/game/viewmodel/GameViewModel.kt) dispatches `GameAction.StartGame`. Its constructor takes use cases, and no repositories:

```kotlin
class GameViewModel(
    reducer: GameStateMachine,
    // ...
    logAnalytics: LogAnalyticsEventUseCase,
    loadGameQuestions: LoadGameQuestionsUseCase,
    lifelineHelper: LifelineHelper,
    recordGameStats: RecordGameStatsUseCase,
    dispatchers: QzDispatchers,
    // ...
) : DreStoreViewModel<GameState, GameAction, GameSideEffect, GameAsyncOp>(
    reducer = reducer,
    dispatchContext = dispatchers.mainImmediate,
), IGameViewModel
```

Domain: [GameStateMachine](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/domain/src/commonMain/kotlin/com/creative/kta/domain/game/GameStateMachine.kt) is a pure reducer. The `handleStartGame` function returns a new state along with `GameAsyncOp.LoadGame`. The reducer does no I/O; it only describes the work to be done.

The ViewModel runs the async op through `GameAsyncOpExecutor.loadGame()`, which calls [LoadGameQuestionsUseCase](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/domain/src/commonMain/kotlin/com/creative/kta/domain/usecase/game/GameUseCases.kt):

```kotlin
class LoadGameQuestionsUseCase(
    private val questionRepository: QuestionRepository,
) : UseCase<Unit, List<QuizData>>() {
    override suspend fun invoke(input: Unit): List<QuizData> = questionRepository.newGameQuestions()
}
```

Data: [DataModule](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/data/src/commonMain/kotlin/com/creative/kta/data/di/DataModule.kt) binds the interface to the implementation:

```kotlin
single<QuestionRepository> { MockQuestionRepository() }
```

[MockQuestionRepository](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/data/src/commonMain/kotlin/com/creative/kta/data/repo/question/MockQuestionRepository.kt) takes questions from `SampleQuestions` following the difficulty distribution in `QuizGameConfig.QUESTION_DIFFICULTIES`.

On the way up: the executor creates a `CurrentGameSession` (its id comes from the Kotlin stdlib's `Uuid.random()`, which runs on both Android and iOS), the ViewModel dispatches `GameAction.GameLoaded`, the reducer updates the state, and `gameScreenUiState` emits a new value to the UI.

When you swap `MockQuestionRepository` for an implementation that reads from Supabase, only the bind line in `DataModule` changes. `GameViewModel`, `GameStateMachine` and `LoadGameQuestionsUseCase` don't need any edits.

## Practice Exercise

### Task

Map the Leaderboard feature onto the four layers. For each layer, write down the real class name and why it belongs to that layer.

### Requirements

1. Find the class in each layer: UI, ViewModel, Domain, Data.
2. Point out which interface lives in Domain and which class implements it.
3. Answer: in which layer is the rank calculated?
4. Answer: why can't `LeaderboardViewModel` import `LeaderboardRepositoryImpl`?

<Solution>

| Layer | Class | Reason |
| --- | --- | --- |
| UI | `LeaderboardPopup`, `LeaderBoardList` in `:feature:menu` | Only renders `LeaderBoardListUiState` |
| ViewModel | `LeaderboardViewModel` | Calls `FetchLeaderboardUseCase`, maps `LeaderboardEntry` to `UserProfileUiState` |
| Domain | `FetchLeaderboardUseCase`, the `LeaderboardRepository` interface, the `LeaderboardEntry` model | Imports no SDK |
| Data | `LeaderboardRepositoryImpl`, `LeaderboardRow` | Calls Supabase, decodes rows, returns `DomainResult` |

The `LeaderboardRepository` interface lives in `:domain`, `LeaderboardRepositoryImpl` in `:data` implements it, and `DataModule` binds the two together.

The rank is calculated in the database. The `leaderboard` view in `scripts/sql/setup.sql` uses `ROW_NUMBER() OVER (ORDER BY total_money_won DESC, level DESC, id)`. The repository only asks for ordering by the `rank` column and a row limit. The ViewModel uses the rank value it receives to pick the GOLD, SILVER or BRONZE tier.

`LeaderboardViewModel` lives in `:feature:menu`, and that module declares no dependency on `:data`. That import won't compile.

</Solution>

## Code Reading Path

1. [settings.gradle.kts](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/settings.gradle.kts): the list of 14 modules.
2. [domain/build.gradle.kts](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/domain/build.gradle.kts): the KDoc spells out what the domain must not contain.
3. [feature/game/build.gradle.kts](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/feature/game/build.gradle.kts): the feature doesn't depend on `:data`.
4. [GameViewModel](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/feature/game/src/commonMain/kotlin/com/creative/kta/feature/game/viewmodel/GameViewModel.kt): the constructor takes use cases.
5. [GameStateMachine](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/domain/src/commonMain/kotlin/com/creative/kta/domain/game/GameStateMachine.kt): `handleStartGame` and `handleSubmitAnswer`.
6. [DataModule](https://github.com/dan-tech-academy/kotlin-accelerator-ai/blob/d845c58aa0497a84c2151cd5969b66fc423ea568/data/src/commonMain/kotlin/com/creative/kta/data/di/DataModule.kt): which interface is bound to which class.

## Signs To Avoid

| Smell | Better direction |
| --- | --- |
| The ViewModel takes a `SupabaseClient` or a repository implementation | Take use cases; implementations are bound at the composition root |
| The domain imports `androidx.*`, Compose or Koin | Keep the domain in `commonMain` with pure Kotlin |
| A feature module adds a `:data` dependency for convenience | Add an interface to `:domain` and implement it in `:data` |
| A composable checks whether the answer is right or wrong itself | Send the intent to the ViewModel; the reducer decides |
| Adding a layer just to have all four layers | Each layer has to isolate one reason to change |

## References

- [The Clean Architecture (Robert C. Martin)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Guide to app architecture (Android Developers)](https://developer.android.com/topic/architecture)
- [Domain layer (Android Developers)](https://developer.android.com/topic/architecture/domain-layer)

## Key Points

Clean Architecture is a rule about the direction of dependencies: outer layers know inner layers, and inner layers don't know outer layers.

In this repo, that rule is written into Gradle. `:domain` contains only pure Kotlin, feature modules can't see `:data`, and only `:shared-app` and `:app` get to see both in order to assemble them.

When you're unsure where a class belongs, ask: if the SDK or the UI changes, does this class have to change? If yes, it doesn't belong in the domain.

## Practice checklist

- [ ] Read the ViewModel-calls-Supabase-directly example
- [ ] Identify the tight-coupling problem
- [ ] Define the UserRepository interface
- [ ] Implement UserRepositoryImpl wrapping AppDatabase
- [ ] Refactor UserViewModel to depend on the interface
- [ ] Write FakeUserRepository for testing
- [ ] Write a test that loadUser updates state

---

- Previous lesson: [Run The Release Readiness Check](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/05-testing-security-release/lesson-05-run-release-readiness-check.md)
- Next lesson: [Gradle Multi-Module Structure](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/06-clean-architecture/lesson-03-multi-module-structure.md)
