Zweck des Domain-Layers
Der Domain-Layer isoliert Geschäftslogik und ermöglicht deren Wiederverwendung. Lerne, wann und wie du ihn sinnvoll einsetzt.
Android-Apps wachsen schnell über einfache Repository-Calls hinaus. Sobald dieselbe Geschäftslogik in mehreren ViewModels auftaucht oder sich Regeln über Schichtgrenzen verteilen, lohnt ein dritter Architekturlayer – der Domain-Layer. Er sitzt zwischen UI-Schicht und Datenschicht und trägt die Verantwortung für reine Geschäftsregeln, die weder von Benutzeroberflächen-Details noch von Datenbankstrukturen abhängen sollen.
Was ist das?
Der Domain-Layer ist die mittlere Schicht im offiziellen Android-Architekturmodell von Google. Er ist optional: Die meisten kleinen Apps kommen ohne ihn aus, weil ihre Logik entweder zu trivial ist oder vollständig ins ViewModel oder ins Repository passt. Sobald aber eine Regel an mehreren Stellen benötigt wird – etwa eine Berechtigungsprüfung, die sowohl im Homescreen als auch im Einstellungsscreen gilt –, ist der Domain-Layer der richtige Ort, um Dopplungen zu vermeiden.
Der Kernbaustein dieses Layers ist der Use Case (auch Interactor genannt). Ein Use Case ist eine Klasse mit genau einer öffentlichen Methode, die eine einzelne Geschäftsoperation kapselt: zum Beispiel GetUserProfileUseCase, SubmitOrderUseCase oder LogOutUseCase. Die Klasse kennt keine Jetpack-Compose-Imports, keine Android-Context-Abhängigkeiten und keinen Netzwerkcode – das ist bewusste Designentscheidung, keine Einschränkung.
Aus Architekturperspektive hängt der Domain-Layer von der Datenschicht ab (Repositories), aber niemals von der UI-Schicht. ViewModels rufen Use Cases auf; Use Cases rufen Repositories auf. Diese Einwegabhängigkeit verhindert, dass Logik unbemerkt in die falsche Schicht wandert.
Wie funktioniert es?
Ein Use Case folgt einer einfachen Konvention: Er erhält seine Abhängigkeiten per Konstruktor (typischerweise ein oder mehrere Repositories) und stellt eine invoke-Operator-Methode bereit, damit der Aufruf im ViewModel idiomatisch liest.
class GetUserProfileUseCase(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: String): Result<UserProfile> {
return userRepository.fetchProfile(userId)
}
}
Ein ViewModel injiziert den Use Case per Dependency Injection (z. B. Hilt) und ruft ihn wie eine Funktion auf:
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
fun loadProfile(userId: String) {
viewModelScope.launch {
val result = getUserProfile(userId)
// result verarbeiten …
}
}
}
Der Domain-Layer kann außerdem mehrere Repositories kombinieren. Muss eine Operation z. B. Nutzerdaten mit Produktdaten zusammenführen, bevor sie an die UI geht, erledigt das der Use Case – und nicht das ViewModel, das dadurch schlank bleibt.
Für reaktive Datenflüsse lässt sich statt suspend operator fun invoke auch ein Flow zurückgeben:
class ObserveCartUseCase(
private val cartRepository: CartRepository
) {
operator fun invoke(): Flow<List<CartItem>> =
cartRepository.cartFlow()
}
In der Praxis
Wann lohnt sich der Domain-Layer?
Die Faustregel lautet: Wenn dieselbe Logik in zwei oder mehr ViewModels dupliziert ist oder wenn ein ViewModel so viel Logik enthält, dass Unit-Tests unübersichtlich werden, ist ein Use Case die saubere Lösung. Auch wenn eine einzelne Geschäftsoperation mehrere Repositories koordinieren muss, gehört diese Koordination in den Domain-Layer und nicht ins ViewModel.
Typische Stolperfalle: der leere Use Case
Ein Use Case, der intern nur return repository.getData() aufruft, ohne weitere Logik hinzuzufügen, bringt keinen Mehrwert. Er fügt eine Indirektion hinzu, die Lesbarkeit kostet, ohne echte Abstraktion zu schaffen. In diesem Fall: Lass das ViewModel direkt mit dem Repository kommunizieren. Der Domain-Layer ist optional – nutze ihn gezielt.
Ein weiteres häufiges Antipattern ist der God Use Case, der für dutzende Operationen zuständig ist. Benenne Use Cases spezifisch und halte sie klein. UserUseCase ist kein hilfreicher Name; GetAuthenticatedUserUseCase beschreibt genau eine Verantwortung.
Testen ohne Android-Framework
Einer der größten Vorteile des Domain-Layers ist die Testbarkeit. Da Use Cases keine Android-Abhängigkeiten besitzen, laufen ihre Unit-Tests im JVM – ohne Emulator oder Robolectric:
@Test
fun `gibt Fehler zurück wenn Nutzer nicht authentifiziert ist`() = runTest {
val fakeRepo = FakeUserRepository(isAuthenticated = false)
val useCase = GetUserProfileUseCase(fakeRepo)
val result = useCase("user-123")
assertTrue(result.isFailure)
}
Schnelle, stabile Tests geben dir Sicherheit beim Refactoring – gerade wenn Geschäftsregeln sich weiterentwickeln oder neue Anforderungen hinzukommen.
Fazit
Der Domain-Layer ist kein Pflichtbestandteil jeder Android-App, aber er ist ein mächtiges Werkzeug, sobald Geschäftslogik wiederverwendet oder isoliert werden muss. Use Cases halten ViewModels schlank, entkoppeln Geschäftsregeln von UI-Details und ermöglichen blitzschnelle Unit-Tests ohne Android-Framework. Prüfe jetzt in deinem eigenen Projekt: Gibt es ViewModels, die ähnliche Logik duplizieren? Enthält ein ViewModel Berechnungen, die eigentlich nicht zur Darstellung gehören? Wenn ja, extrahiere einen Use Case, schreibe einen Unit-Test dafür – und erlebe, wie deine Architektur an Klarheit und Testbarkeit gewinnt.