# Layer Responsibilities And How To Read File Names

> Don't mix UI, business logic and backend code. And learn to guess where a file lives from its name alone.

- URL: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-04-spot-layer-responsibilities
- Course: [Mobile Action Pro](https://dantech.academy/en/courses/kotlin-accelerator.md)
- Module: Production App Map Architecture
- Duration: 7 min
- Difficulty: beginner
- Access: Free, full lesson
- Tiếng Việt: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-04-spot-layer-responsibilities.md

## Objectives

- Put each responsibility in the right layer: UI, Route, ViewModel, reducer, use case, repository, service
- Tell a repository contract (data access) apart from a service contract (capability) and the data implementation
- Read the codebase's naming conventions to guess a file's location and role before opening it

---

## Lesson Goal

In a real app, writing a new feature is half the job. The other half is knowing which layer that responsibility belongs in.

This lesson helps you tell apart the responsibilities of the UI, Route, ViewModel, reducer/state machine, use case, repository, service, data implementation and shared UI layers.

The second half of the lesson teaches a skill people rarely talk about: reading a file name to guess its role and location. This codebase names things very consistently. Once you know the conventions, you find code much faster than by searching blindly.

In a Kotlin Multiplatform project, the question "which layer" comes with a second one: does this code run on every platform, or only on Android or iOS? Part 5 answers that.

## The Real Problem

In a small app you can write almost everything into a `ViewModel` and the app still runs. In a production app, that starts to hurt.

You fix a small sign-in bug but have to open the UI, the ViewModel, the Supabase client, analytics, navigation and local storage. You add a button and accidentally touch a business rule. You change the AI hint provider and the game screen has to change too.

At that point the layers no longer have clear ownership.

This is also a common bottleneck with vibe coding: you build the app very fast, but the prompt never states what each layer is responsible for, so the code piles up in one place.

## Part 1: The Layer Map

Use this table when you're not sure where code should go:

| Layer                        | Main responsibility                                                             | What it should not do                                      |
| ---------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| UI composable                | Render `UiState`, take input, call callbacks.                                   | Call the backend, hold business rules, pick the data source itself. |
| Route composable             | Connect the ViewModel to the screen, collect one-time effects, wire navigation. | Stuff in layout or business logic.                         |
| ViewModel                    | Hold screen state, take UI events, dispatch actions, call use cases/services.   | Create its own HTTP client or know Supabase/Firebase SDK details. |
| Reducer/state machine        | Turn the current state plus an action into the next state and effects, predictably. | Call I/O, navigation, analytics or the backend directly. |
| Use case                     | Express one specific business operation.                                        | Render UI or know Compose classes.                         |
| Repository contract          | Define what data the app needs to read/write.                                   | Contain SDK/provider implementation details.               |
| Service contract             | Define a capability such as auth, AI hint, audio, haptics, subscription.        | Return UI state or depend on a screen.                     |
| Data implementation          | Talk to Supabase, Firebase, RevenueCat, DataStore, Ktor and each platform's audio SDK. | Decide the UI flow.                                 |
| `:core`                      | Shared route contracts, base types, feature registries.                         | Own feature UI.                                            |
| `:kernel`                    | The lowest technical primitives, for example `QzDispatchers`.                   | Depend on other app modules.                               |
| `:ui-components` and `:theme` | Reusable composables, design tokens, spacing, typography, colors.              | Call a ViewModel or the backend.                           |

Learners often miss this point: a repository and a service are different things.

A repository answers "how is this data stored and read?". A service answers "how is this capability carried out?".

For example, `LeaderboardRepository` reads the leaderboard, while `AuthService` handles sign-in and sign-out. Both have their contract in `:domain` and their implementation in `:data`.

The features' reducers, side effect handlers and ViewModels are built on an in-house library in two modules, `:dre-core` and `:dre-viewmodel`. `MenuReducer` implements the `Reducer` interface from `:dre-core`, the handlers implement `SideEffectHandler`, and `MenuViewModel` extends `DreStoreViewModel` from `:dre-viewmodel`. Because every feature shares the same skeleton, the ViewModel folders in Part 4 look so alike.

## Part 2: Example, Email Sign In

Trace this flow:

```text
Menu screen
-> MenuViewModel
-> SignInWithEmailUseCase
-> AuthService
-> AuthServiceSupabaseImpl
-> Supabase
```

The right ownership is:

| Piece                      | Owner                                                             |
| -------------------------- | ----------------------------------------------------------------- |
| Email form UI              | `:ui-components` auth components or `:feature:menu` screen code   |
| User taps sign in          | A `MenuRoute` or `MenuScreen` callback into `MenuViewModel`       |
| Loading/error/dialog state | `MenuViewModel` and the reducer                                   |
| Business operation         | `SignInWithEmailUseCase` in `:domain`                             |
| Auth capability            | The `AuthService` contract in `:domain`                           |
| Supabase details           | `AuthServiceSupabaseImpl` in `:data`                              |

If `MenuViewModel` created its own Supabase client, it would be carrying too much: screen state, the business flow and transport details.

The consequences are concrete: change the auth provider and the menu has to change. Add a retry policy and the menu has to know about it. Reuse sign-in in onboarding and you copy the logic.

## Part 3: Example, AI Hint

Trace this flow:

```text
Game feature
-> GameViewModel
-> lifeline/action handling
-> AIHintService
-> AIHintServiceImpl
-> GeminiHintProvider or LocalHintProvider
-> GeminiHintClient (SupabaseGeminiHintClient), on the Gemini branch only
-> the gemini-hint Edge Function on Supabase
-> Gemini API
```

To the user this is one action: "give me a hint". In the codebase it passes through several layers:

| Responsibility                                 | Sensible layer                         |
| ---------------------------------------------- | -------------------------------------- |
| Render the Ask AI button                       | The game screen composable             |
| Take the user's intent                         | `GameViewModel`                        |
| Decide whether the game state allows a hint    | The reducer/state machine              |
| Express the "get hint" capability              | The `AIHintService` contract           |
| Choose Gemini or the local fallback            | The data implementation and provider wiring |

The screen should render the Ask AI button. The ViewModel should coordinate state. Only the data layer knows where the request goes and what happens when it fails. `GeminiHintProvider` runs only when the `feature_ai_hints` flag in Remote Config is on and the player is signed in. It sends the question and its options through `SupabaseGeminiHintClient` to the `gemini-hint` Edge Function, and on a transient error retries up to `max_retries` times (read from Remote Config). If the call fails, `AIHintServiceImpl` switches to `LocalHintProvider`. The Gemini API key is not in the app: it is a Supabase secret that only the `gemini-hint` function can read.

## Part 4: Read File Names Like A Map

This is the fastest way to navigate a production codebase. Each file name suffix tells you the file's role and module before you open it.

### Prefixes

| Prefix        | Meaning                                           | Module                         |
| ------------- | ------------------------------------------------- | ------------------------------ |
| `Qzds*`       | A shared design system component or token         | `:ui-components` or `:theme`   |
| `I*ViewModel` | A ViewModel interface, used for tests and previews | `:feature:*`                  |
| `*Impl`       | A concrete implementation of a contract           | usually `:data`                |

The `Qzds` prefix shows up in a lot of files. When you see it, you know right away: this is shared, it doesn't belong to any one feature, and changing it affects many screens.

### Suffixes Inside A Feature

| Suffix               | Role                                                       |
| -------------------- | ---------------------------------------------------------- |
| `*RouteData`         | A route contract, `@Serializable`, lives in `:core`        |
| `*Navigation`        | The feature navigation provider; registers destinations    |
| `*Route`             | A composable that connects the ViewModel to the screen and handles one-time effects |
| `*Screen`            | A composable that renders purely from `UiState`            |
| `*UiState`           | An immutable data class describing what the screen shows   |
| `*Mapper`            | Turns internal state into `UiState`                        |
| `*Contract`          | Declares a screen's State, Action and SideEffect           |
| `*Reducer`           | A pure function that turns state plus an action into new state |
| `*SideEffectHandler`, `*UiEffectHandler` | Where effects actually run, with I/O   |
| `*ViewModel`         | The coordinator that ties all of the above together        |

Open `feature/menu/.../viewmodel/` and you see exactly this set: `MenuContract.kt`, `MenuReducer.kt`, `MenuViewModel.kt`, plus a `handler/` folder with 4 side effect handlers.

Then open `feature/menu/.../settings/viewmodel/`. The same set again: `SettingContract.kt`, `SettingReducer.kt`, `SettingUiEffectHandler.kt`, `SettingViewModel.kt`.

Then `leaderboard/viewmodel/`. Still the same set.

The codebase doesn't have ten ways of organizing ten screens. It has exactly one shape, repeated everywhere.

Once you understand that shape, you can read every other feature. And when you write a new feature, you already know which files to create.

## Part 5: Layers And Platforms

Every layer in the Part 1 table lives in `commonMain`: UI composables, Routes, ViewModels, reducers, use cases, contracts, and most data implementations. Android and iOS share one copy.

Only implementations that must call a platform's own SDK are split out. For example, the `AudioManager` contract lives in `commonMain`, and the implementation comes in two versions:

| Contract (commonMain) | Android (`DataModule.android.kt`) | iOS (`DataModule.ios.kt`) |
| --------------------- | --------------------------------- | ------------------------- |
| `AudioManager`        | `SoundPoolAudioManager`           | `AvAudioManager`          |
| `HapticManager`       | `VibratorHapticManager`           | `UiKitHapticManager`      |
| `AnalyticsManager`    | `FirebaseAnalyticsManager`        | `NoOpAnalyticsManager`    |

The feature only knows `AudioManager`. Koin picks the implementation per platform. The shared UI has no `if (platform)` branches: one composable, two Koin bindings.

When you add code that touches a platform SDK, put the contract in `commonMain` and the two implementations in `androidMain`/`iosMain`. This tip applies to every KMP project, not just this repo.

## Part 6: The ViewModel Doesn't Do Everything

A common misunderstanding: since the ViewModel coordinates the reducer and the handlers, everything has to go into the ViewModel.

The `ViewModel` only coordinates: it takes UI events, exposes `UiState`, dispatches actions, calls the reducer, and executes effects through injected collaborators.

Complex business rules still belong in use cases. Data access still goes through repository/service contracts. Provider details still live in `:data`.

## Bad Smells

| Smell                                            | Better direction                                  |
| ------------------------------------------------ | ------------------------------------------------- |
| A composable imports Supabase or Firebase classes | Move the service call behind the domain/data layers. |
| A ViewModel creates its own `HttpClient`         | Inject a repository/service implementation.       |
| A reducer or state machine calls I/O directly    | Emit a side effect and execute it in a handler.   |
| A repository returns `UiState`                   | Return a domain model and map it to UI in the feature layer. |
| A service implementation lives in `:domain`      | Keep the domain as contracts; implementations go in `:data`. |
| A feature copies a button another screen could reuse | Extract it to `:ui-components` with the `Qzds` prefix. |
| Feature A imports Feature B's screen             | Use route contracts and callbacks.                |
| A composable branches by platform                | Put the contract in `commonMain` and bind the implementation per platform through Koin. |

## Checkpoint

Open `feature/menu/.../viewmodel/` and `feature/menu/.../settings/viewmodel/` side by side.

You are done with this lesson when you can answer:

- What shape do those two folders share?
- Which file is the only one allowed to call I/O in that shape?
- From the name `LeaderboardUiState.kt` alone, can you guess what it contains and what it doesn't?
- How do `AIHintService` and `AIHintServiceImpl` differ, and which module does each live in?
- If you need a button shared by the menu and the game, where do you put it and what do you call it?
- Why does `AudioManager` have two implementations, and does the feature need to know?

---

- Previous lesson: [Feature Registration And Navigation Boundaries](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-03-understand-feature-discovery.md)
- Next lesson: [Your First Safe Change](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-05-make-your-first-change.md)
