Skip to main content
The domain layer is the core of the application architecture, containing business logic, use cases, and domain models. It’s framework-independent and defines contracts that other layers must implement.

Architecture Overview

The domain layer follows Clean Architecture principles with three main components:

Domain Models

Pure data classes representing business entities

Repository Interfaces

Contracts defining data access operations

Use Cases

Business logic orchestration

Domain Models

Domain models are pure Kotlin data classes that represent business entities without any framework dependencies.

Article

Represents a simplified article for list views:
domain/model/Article.kt
data class Article(
    val id: Long,
    val title: String,
    val imageUrl: String,
    val newsSite: String,
    val publishedAt: String
)
The Article model contains only essential fields needed for displaying article lists, optimizing performance and memory usage.

ArticleDetail

Represents a complete article with all details:
domain/model/ArticleDetail.kt
data class ArticleDetail(
    val id: Long,
    val title: String,
    val authors: List<Author>,
    val url: String,
    val newsSite: String,
    val imageUrl: String,
    val summary: String,
    val publishedAt: String,
    val updatedAt: String
)
Having separate models for list and detail views follows the principle of data tailoring - each screen gets exactly the data it needs.

Resource

A sealed class that wraps data with loading and error states:
core/common/Resource.kt
sealed class Resource<out T> {
    data class Success<out T>(val data: T?) : Resource<T>()
    data class Error<out T>(val code: String?, val msg: String, val error: Throwable? = null) : Resource<T>()
    data class Loading<out T>(val data: T? = null) : Resource<T>()
}
The Resource class provides a type-safe way to represent asynchronous operations, making it easy for the UI to handle loading, success, and error states.

Repository Interfaces

Repository interfaces define contracts for data access without specifying implementation details.

ArticleRepository

domain/repository/ArticleRepository.kt
interface ArticleRepository {
    suspend fun getArticles(
        query: String? = null
    ): Resource<List<Article>>

    suspend fun getArticleById(articleId: Long): Resource<ArticleDetail>
}
Repository interfaces provide several benefits:
  • Abstraction: The domain layer doesn’t need to know about implementation details
  • Testability: Easy to create mock implementations for unit tests
  • Flexibility: Can swap implementations without changing business logic
  • Dependency Inversion: High-level modules don’t depend on low-level modules
All repository methods are suspend functions, which means:
  • They can be called from coroutines
  • They’re non-blocking and asynchronous
  • They integrate seamlessly with Kotlin’s coroutine framework
  • They enable structured concurrency

Use Cases

Use cases encapsulate specific business logic operations. Each use case has a single responsibility and coordinates between repositories and the presentation layer.

GetArticlesUseCase

Retrieves a list of articles with optional search filtering:
domain/usecase/GetArticlesUseCase.kt
class GetArticlesUseCase @Inject constructor(
    private val repository: ArticleRepository
) {
    suspend fun getArticles(query: String? = null): Resource<List<Article>> {
        return repository.getArticles(query)
    }
}

GetArticleByIdUseCase

Retrieves detailed information for a specific article:
domain/usecase/GetArticleByIdUseCase.kt
class GetArticleByIdUseCase @Inject constructor(
    private val repository: ArticleRepository
) {
    suspend fun getArticleById(articleId: Long): Resource<ArticleDetail> {
        return repository.getArticleById(articleId)
    }
}
While these use cases appear simple, they serve as a layer of indirection that can be extended with additional business logic, validation, or data transformation as the app grows.

Benefits of Use Cases

Single Responsibility

Each use case has one clear purpose

Reusability

Can be shared across multiple ViewModels

Testability

Easy to unit test in isolation

Business Logic Centralization

All business rules in one place

Dependency Flow

The domain layer is at the center of the architecture:
Notice that dependencies flow inward. The domain layer doesn’t depend on the presentation or data layers, making it the most stable part of the architecture.

Clean Architecture Principles

The domain layer implements several Clean Architecture principles:
Domain models and use cases don’t depend on Android SDK, Retrofit, Room, or any external frameworks. This makes them:
  • Easy to test without Android dependencies
  • Portable to other platforms
  • Free from framework version constraints
Pure business logic with no external dependencies:
class GetArticlesUseCaseTest {
    @Test
    fun `getArticles returns success when repository succeeds`() = runTest {
        // Given
        val mockRepository = mockk<ArticleRepository>()
        coEvery { mockRepository.getArticles(null) } returns 
            Resource.Success(listOf(mockArticle))
        val useCase = GetArticlesUseCase(mockRepository)
        
        // When
        val result = useCase.getArticles()
        
        // Then
        assertTrue(result is Resource.Success)
    }
}
Clear boundaries between different types of logic:
  • Models: Data structure
  • Repositories: Data access contracts
  • Use Cases: Business operations
Each component has a single, well-defined purpose.
The data layer implements interfaces defined in the domain layer:
// Domain layer defines the contract
interface ArticleRepository { ... }

// Data layer implements it
class ArticleRepositoryImpl : ArticleRepository { ... }
This means the domain layer controls the contract while remaining independent of implementation details.

Communication Between Layers

Here’s how the domain layer interacts with other layers:
1

Presentation calls Use Case

ViewModel invokes a use case method:
val result = articleUseCase.getArticles(query)
2

Use Case calls Repository

Use case delegates to repository interface:
return repository.getArticles(query)
3

Repository Implementation executes

The data layer’s implementation performs the actual work:
// In ArticleRepositoryImpl
val response = apiHelper.safeApiCall { ... }
4

Domain Model returned

Data layer maps DTOs to domain models:
Resource.Success(pagination.articles.map { it.toArticleDomain() })

Best Practices

Keep It Pure

Domain models should be pure data classes without business logic methods

Single Purpose

Each use case should do one thing well

Framework Free

No Android or external framework dependencies

Immutable Models

Use val properties to ensure data immutability

Data Layer

Explore repository implementations

Presentation Layer

See how ViewModels use use cases

Build docs developers (and LLMs) love