Clean Architecture — Grundlagen, Schichten von Entities, Use Cases und Gateways

Autor: IT Sectr Veröffentlicht: 2026-02-17 Lesezeit: 10 Min.

Clean Architecture — eine von Robert Martin (Uncle Bob) im Jahr 2012 vorgeschlagene mehrschichtige Architektur, die eine Anwendung in unabhängige Schichten unterteilt: Domain (Entities, Use Cases), Data (Repositories, DataSources) und Presentation (ViewModels, Views). Das Hauptprinzip ist die Dependency Rule: Abhängigkeiten zeigen nach innen, äußere Schichten hängen von inneren ab, aber nicht umgekehrt. Clean Architecture wird in der mobilen Entwicklung für Projekte mit hoher Komplexität der Geschäftslogik eingesetzt. Weitere Details im Buch The Clean Architecture.

Wichtige Punkte

  • Clean Architecture — drei Schichten: Domain (Geschäftslogik), Data (Daten), Presentation (UI) mit Dependency Rule
  • Dependency Rule — Abhängigkeiten zeigen nach innen, Domain weiß nichts über Data und Presentation
  • Use Cases (Interactors) — Geschäftslogikszenarien, jeder Use Case ist eine Klasse mit einer Methode
  • Repository Interface — Datenabstraktion in Domain, Implementierung in der Data-Schicht
  • Testbarkeit — Domain und Use Cases werden mit Unit-Tests ohne Android SDK und iOS UIKit getestet

Clean Architecture — Grundlagen der mehrschichtigen Architektur

Clean Architecture — ein von Robert Martin (Uncle Bob) im Jahr 2012 formuliertes Architekturmuster. Die Kernidee ist die Unterteilung einer Anwendung in Schichten mit einer strengen Abhängigkeitsregel: Code innerhalb einer Schicht weiß nichts über Code außerhalb. Äußere Schichten (UI, Frameworks, DB) sind Implementierungsdetails. Innere Schichten (Geschäftslogik, Unternehmensregeln) sind das Wesen der Anwendung.

Clean Architecture Schichten in der mobilen Entwicklung: 1) Domain — Entities (Geschäftsobjekte) und Use Cases (Nutzungsszenarien); 2) Data — RepositoryImpl (Repository-Implementierungen), DataSources (Netzwerk, DB, Cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain ist die innerste Schicht ohne Abhängigkeiten. Data hängt von Domain ab (implementiert Repository-Schnittstellen). Presentation hängt von Domain ab (ruft Use Cases auf, abonniert Ergebnisse).

SchichtEnthältAbhängigkeiten
DomainEntities, Use Cases, Repository InterfacesKeine (reines Kotlin/Swift)
DataRepositoryImpl, DataSources (API, DB, Cache)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, Jetpack, SwiftUI

Dependency Rule — die einzige strenge Regel der Clean Architecture. Quellcode kann nur auf eine Schicht innerhalb seiner selbst oder auf eine darunterliegende Schicht (näher am Zentrum) verweisen. Presentation importiert Domain. Domain importiert NICHT Data oder Presentation. Dies wird durch das Dependency Inversion Principle erreicht: Domain definiert die Repository-Schnittstelle, Data implementiert sie. Presentation hängt von der UseCase-Abstraktion ab, nicht von einem bestimmten Repository.

Domain Layer: Entitäten, Use Cases und Repository-Schnittstellen

Domain — die stabilste Schicht der Anwendung. Entities sind framework-unabhängige Geschäftsobjekte: User, Product, Order. Use Cases sind Klassen mit einer einzigen invoke-Methode (oder operator fun invoke in Kotlin), die ein Szenario implementieren: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces sind Datenzugriffsabstraktionen, die in Domain definiert und in Data implementiert werden. Domain enthält kein Android SDK, iOS UIKit, Retrofit, Room — nur reines Kotlin oder Swift.

swift
// Entity — Geschäftsobjekt (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — Datenabstraktion (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — ein Szenario (Domain)
final class GetUserUseCase {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func execute(id: Int) async throws -> User {
        return try await repository.getUser(id: id)
    }
}

Use Case — «eine Klasse mit einer Methode» ist kein Dogma, sondern eine praktische Empfehlung. Wenn ein Use Case komplexer wird (Validierung + Protokollierung + Repository-Aufruf), werden seine Methoden nach Bedeutung gruppiert: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Die Hauptsache ist, dass ein Use Case nicht wissen sollte, woher die Daten kommen (Netzwerk, DB, Cache) oder wer sie anzeigt (Compose, SwiftUI). Bei IT Sectr weisen wir jeder Operation, die eine Geschäftsregel, Validierung oder Datenkombination aus zwei Quellen enthält, einen Use Case zu.

Reinheit des Domain wird durch DTO-Mapping an den Schichtgrenzen erreicht. Die Data-Schicht erhält JSON-Modelle (DTOs), mapped sie auf Domain-Entities. Presentation erhält Domain-Entities, mapped sie auf ViewModels (DisplayItem). Eine Domain-Entity enthält niemals Retrofit-, Room- oder Codable-Annotationen — dies garantiert, dass die Schicht nicht geändert werden muss, wenn die DB von Room auf Realm gewechselt oder Retrofit durch Ktor ersetzt wird.

Data Layer: Repository-Implementierung und Datenquellen

Data Layer — Implementierung der in Domain definierten Schnittstellen. Enthält RepositoryImpl (Klassen, die UserRepository implementieren) und DataSources (RemoteDataSource — API, LocalDataSource — DB, CacheDataSource — SharedPreferences/NSUserDefaults). Die Data-Schicht hängt von Domain (importiert Repository-Schnittstellen und Entities) und von Frameworks (Retrofit, Room, Ktor, CoreData) ab. RepositoryImpl verbirgt die Datenquelle vor Domain — der Use Case weiß nicht, ob die Daten aus dem Netzwerk oder dem Cache kamen.

kotlin
// DTO — Modell für Netzwerk (Data)
data class UserDto(
    @SerializedName("id") val id: Int,
    @SerializedName("first_name") val firstName: String,
    @SerializedName("last_name") val lastName: String,
    @SerializedName("email") val email: String
)

// RepositoryImpl — Implementierung (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // Versuche aus Cache zu holen
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // Wenn nicht — aus Netzwerk laden
        val dto = remoteDataSource.fetchUser(id)
        val user = dto.toDomain()
        localDataSource.saveUser(user)
        return user
    }

    override suspend fun getUsers(): List<User> {
        return remoteDataSource.fetchAllUsers().map { it.toDomain() }
    }
}

// Mapper — DTO ↔ Domain Konvertierung
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Cache-Strategie in der Data Layer: RepositoryImpl überprüft zuerst den lokalen Speicher; wenn keine Daten gefunden werden, lädt es aus dem Netzwerk und speichert lokal. Wenn das Netzwerk nicht verfügbar ist, gibt es veraltete Daten mit einem isStale-Flag zurück. Der Use Case in Domain weiß nichts von der Strategie — er erhält User über Repository.getUser(id). Eine Änderung der Strategie (z. B. Cache-Invalidierung alle 15 Minuten) beeinflusst Domain und Presentation nicht.

Modularität in Android — Kotlin Multiplatform ermöglicht es, Domain ohne Android-SDK-Abhängigkeiten in ein separates KMP-Modul zu extrahieren. Data ist ein separates Modul mit einer Abhängigkeit von Domain. Presentation ist ein Android-Modul mit einer Abhängigkeit von Domain. Gradle-Abhängigkeiten: domain (reines Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Eine solche Modularität ist für große Projekte unerlässlich — CI baut Domain separat, Domain-Unit-Tests benötigen keinen Android-Emulator.

Presentation Layer: ViewModels und Views

Presentation Layer — die äußerste Schicht der Clean Architecture. Enthält ViewModels (Android) / ObservableObject (iOS) und Views (Compose/SwiftUI). Das ViewModel ruft einen Use Case auf, erhält das Ergebnis und wandelt es in einen UI-Zustand um. Die View abonniert den Zustand und rendert ihn. Presentation hängt von Domain ab — importiert Use Cases und Entities. Presentation importiert die Data-Schicht nicht — Daten kommen durch den Use Case, der intern ein Repository verwendet.

ViewModel in Clean Architecture enthält keine Geschäftslogik — es ruft den Use Case auf. Wenn ein Use Case User zurückgibt, wandelt das ViewModel ihn in UserDisplayItem (name, emailFormatted, avatarUrl) um — ein reines Präsentationsmodell. Der Use Case weiß nichts von DisplayItem — er gibt eine Entity zurück. Diese Trennung ermöglicht es, den Use Case ohne UI und das ViewModel ohne UseCase (über Mocks) zu testen. Bei IT Sectr halten wir uns strikt daran: Use Case — Geschäftslogik, ViewModel — nur Präsentation, View — nur Anzeige.

kotlin
// Use Case (Domain) — reine Geschäftslogik
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — nur Präsentation
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {

    private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
    val state: StateFlow<UserScreenState> = _state.asStateFlow()

    fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = UserScreenState.Loading
            val user = getUserUseCase(id)
            val displayItem = UserDisplayItem(
                name = user.name,
                email = user.email,
                initials = user.name.split(" ").joinToString("") { it.first().toString() }
            )
            _state.value = UserScreenState.Success(displayItem)
        }
    }
}

data class UserDisplayItem(
    val name: String,
    val email: String,
    val initials: String
)

sealed interface UserScreenState {
    data object Loading : UserScreenState
    data class Success(val displayItem: UserDisplayItem) : UserScreenState
    data class Error(val message: String) : UserScreenState
}

Navigation in der Presentation-Schicht ist ebenfalls Teil des äußeren Rings. Clean Architecture schreibt keinen Navigationsmechanismus vor — es kann NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) oder Router (VIPER) sein. Wichtig ist, dass Navigationsentscheidungen von der Presentation getroffen werden, aber die Navigation nicht in den Use Case eindringen sollte. Der Use Case gibt ein Ergebnis zurück, das ViewModel entscheidet, zu welchem Bildschirm navigiert wird. In Clean Architecture ist Navigation ein Detail, das ohne Änderung des Domain ausgetauscht werden kann.

Clean Architecture auf iOS und Android: Codebeispiele

Clean Architecture auf Android wird über Gradle-Module implementiert: domain (reines Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Ordnerstruktur: domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt; presentation/ui/user/UserViewModel.kt, UserScreen.kt. DI (Hilt) verbindet die Schichten: UserRepositoryImpl wird an die UserRepository-Schnittstelle im domain-Modul gebunden.

Clean Architecture auf iOS verwendet SPM oder Xcode-Gruppen ohne separate Module (aufgrund von Xcode-Einschränkungen). Domain — ein Ordner mit Dateien, die UIKit oder SwiftUI nicht importieren. Data — ein Ordner mit APIClient, CoreDataStack, RepositoryImpl. Presentation — ein Ordner mit ViewModels und SwiftUI-Views. DI über Konstruktor oder Assembly in der App. Der Hauptaufruf ist async/await über UseCase.execute() mit MainActor-Überprüfung für UI-Updates.

swift
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
    private let apiClient: APIClient

    func fetchUser(id: Int) async throws -> UserDTO {
        return try await apiClient.get("/users/\(id)")
    }
}

// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
    private let remote: UserRemoteDataSource
    private let local: UserLocalDataSource

    func getUser(id: Int) async throws -> User {
        if let cached = try await local.getUser(id) {
            return cached
        }
        let dto = try await remote.fetchUser(id)
        let user = dto.toDomain()
        try await local.saveUser(user)
        return user
    }
}

// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var state: UserScreenState = .loading
    private let getUserUseCase: GetUserUseCase

    func loadUser(id: Int) {
        Task {
            state = .loading
            if let user = try? await getUserUseCase.execute(id: id) {
                state = .success(user)
            } else {
                state = .error("Failed to load")
            }
        }
    }
}

Clean Architecture in IT Sectr Projekten — unser Standard für Projekte ab 30 Tagen. Wir verwenden seit 2022 eine Dreischicht-Architektur mit Kotlin Multiplatform für Android/iOS. Domain — ein gemeinsames KMP-Modul, Data — Plattformmodule (Retrofit auf Android, URLSession auf iOS), Presentation — native UI. Dies ergibt 60–80% gemeinsamen Geschäftslogik-Code zwischen iOS und Android und reduziert die Entwicklungszeit um 30–40% im Vergleich zu zwei separaten Implementierungen.

Häufig gestellte Fragen

Wie viele Schichten sollte Clean Architecture haben?

Mindestens drei: Domain, Data, Presentation. Für große Projekte werden Framework (Android SDK/iOS UIKit-Abhängigkeiten) und Device (GPS, Kamera, Sensoren) hinzugefügt. Die Anzahl der Schichten ist keine strenge Regel, sondern eine Frage der Bequemlichkeit. Die Hauptsache ist, der Dependency Rule zu folgen: Abhängigkeiten zeigen nach innen, zu Domain hin. Sie können mit drei beginnen und Schichten hinzufügen, während das Projekt wächst.

Erhöht Clean Architecture die Code-Menge?

Ja — um 30–50% im Vergleich zu MVVM aufgrund der Extraktion von Repository-Schnittstellen, Use Cases und Mappern. Für eine einfache CRUD-Anwendung ist dies übermäßig. Clean Architecture ist für Projekte mit komplexer Geschäftslogik gerechtfertigt, wo Testbarkeit und Schichtenisolierung wichtiger sind als Entwicklungsgeschwindigkeit. Für MVP oder Prototypen verwenden Sie MVVM — Clean Architecture wird den Start verlangsamen.

Kann Clean Architecture mit MVI kombiniert werden?

Ja, das ist eine gängige Praxis. Die Use Cases bleiben in Domain, während Presentation den MVI-Zyklus (Intent → Reducer → State) verwendet. Die Data-Schicht bleibt gleich, Domain bleibt gleich. MVI in Presentation bietet einen vorhersagbaren Bildschirmzustand, Clean Architecture bietet Isolierung der Geschäftslogik. Diese Kombination wird in großen Projekten mit Dutzenden von Entwicklern verwendet.

Brauche ich Use Cases für jede Datenanfrage?

Ein Use Case ist erforderlich, wenn die Operation eine Geschäftsregel beinhaltet: Validierung, Kombination von Daten aus zwei Quellen, Berechnung, Protokollierung, Zugriffsberechtigungsprüfung. Eine einfache getUser(id)-Anfrage ohne zusätzliche Logik kann das Repository direkt vom ViewModel aus aufrufen. Aus Gründen der architektonischen Konsistenz erstellen viele Teams jedoch für jede öffentliche Repository-Methode einen Use Case — dies fügt 5–10% Code hinzu, vereinfacht aber das Lesen.

Wie testet man Clean Architecture?

Domain: Unit-Tests der Use Cases mit mock Repository — reines Kotlin/Swift ohne Android SDK. Data: Integrationstests von RepositoryImpl mit mock/fake DataSource. Presentation: ViewModel-Tests mit mock UseCase. Dank der Dependency Rule wird jede Schicht isoliert getestet. Bei IT Sectr erreicht die Domain-Abdeckung 95%, Data — 70–80%, Presentation — 60–70%.

Zusammenfassung

  • Clean Architecture — drei Schichten (Domain, Data, Presentation) mit nach innen zeigender Dependency Rule
  • Dependency Rule — Domain weiß nichts über Data und Presentation, Isolierung durch Schnittstellen
  • Domain — Entities, Use Cases, Repository Interfaces — reines Kotlin/Swift ohne Frameworks
  • Data — RepositoryImpl, DataSources (Netzwerk, DB, Cache) — Implementierung der Domain-Schnittstellen
  • Presentation — ViewModels, Views — nur Anzeige, Geschäftslogik in Use Cases
  • Testen — Domain durch Unit-Tests zu 90–95% abgedeckt
  • KMP — Clean Architecture mit Kotlin Multiplatform ergibt 60–80% gemeinsamen iOS + Android-Code

Wir entwickeln eine mobile Applikation schlüsselfertig

IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.

Projekt besprechen

Lesen Sie auch