Skip to main content
The domain layer encapsulates business logic and defines contracts for data access. It contains use cases that orchestrate operations, domain models representing business entities, and gateway interfaces that define data operations without implementation details.

Domain architecture

The domain layer is organized into three key modules:
  • domain/model - Business objects (BOs) representing core entities
  • domain/gateway - Interfaces defining data access contracts
  • domain/useCase - Business logic implementations
The domain layer is framework-agnostic and contains pure Kotlin code with no Android dependencies.

Domain models (Business Objects)

Business objects represent core entities with all necessary business data:
domain/model/src/main/java/es/mobiledev/domain/model/article/ArticleBo.kt
data class ArticleBo(
    val id: Long,
    val title: String,
    val authors: List<AuthorBo>,
    val url: String,
    val imageUrl: String,
    val newsSite: String,
    val summary: String,
    val publishedAt: String,
    val updatedAt: String,
)
The “Bo” suffix (Business Object) distinguishes domain models from DTOs (network) and DBOs (database).
Complex entities reference other domain models:
// domain/model/src/main/java/es/mobiledev/domain/model/article/AuthorBo.kt
data class AuthorBo(
    val name: String,
    val socials: SocialsBo,
)

Gateway interfaces

Gateways define contracts for data access without implementation details:
domain/gateway/src/main/java/es/mobiledev/domain/gateway/article/ArticleGateway.kt
interface ArticleGateway {
    suspend fun getArticles(
        limit: Long,
        offset: Long,
    ): Flow<ArticleResponseBo>

    suspend fun getArticleById(
        id: Long
    ): Flow<ArticleBo>

    suspend fun getFavoriteArticles(): Flow<List<ArticleBo>>

    suspend fun saveFavoriteArticle(articleBo: ArticleBo)

    suspend fun removeFavoriteArticle(articleBo: ArticleBo)

    suspend fun isArticleFavorite(id: Long): Flow<Boolean>
}
The gateway pattern provides several advantages:
  • Testability: Easy to mock for unit tests
  • Flexibility: Swap implementations without changing business logic
  • Separation: Domain layer doesn’t depend on data layer details
  • Contract clarity: Clear definition of required operations

Preferences gateway

Gateways handle different data types:
domain/gateway/src/main/java/es/mobiledev/domain/gateway/preferences/PreferencesGateway.kt
interface PreferencesGateway {
    suspend fun getLastOpenTime(): Flow<Long>
    suspend fun saveLastOpenTime(time: Long)
}

Use cases

Use cases encapsulate single business operations with clear inputs and outputs:
domain/useCase/src/main/java/es/mobiledev/domain/usecase/article/GetArticlesUseCase.kt
interface GetArticlesUseCase {
    suspend operator fun invoke(
        limit: Long,
        offset: Long
    ): Flow<ArticleResponseBo>
}

class GetArticlesUseCaseImpl(
    private val articleGateway: ArticleGateway,
) : GetArticlesUseCase {
    override suspend fun invoke(
        limit: Long,
        offset: Long
    ): Flow<ArticleResponseBo> = 
        articleGateway.getArticles(limit = limit, offset = offset)
}
The operator fun invoke() allows calling use cases like functions: getArticlesUseCase(limit, offset).

Use case patterns

Simple pass-through use case

Delegates directly to the gateway:
domain/useCase/src/main/java/es/mobiledev/domain/usecase/article/GetArticleByIdUseCase.kt
interface GetArticleByIdUseCase {
    suspend operator fun invoke(id: Long): Flow<ArticleBo>
}

class GetArticleByIdUseCaseImpl(
    private val articleGateway: ArticleGateway,
) : GetArticleByIdUseCase {
    override suspend fun invoke(id: Long): Flow<ArticleBo> = 
        articleGateway.getArticleById(id = id)
}

Business logic use case

Applies business rules and coordinates multiple operations:
domain/useCase/src/main/java/es/mobiledev/domain/usecase/article/SaveOrRemoveFavoriteArticleUseCase.kt
interface SaveOrRemoveFavoriteArticleUseCase {
    suspend operator fun invoke(article: ArticleBo, isFavorite: Boolean)
}

class SaveOrRemoveFavoriteArticleUseCaseImpl(
    private val articleGateway: ArticleGateway,
) : SaveOrRemoveFavoriteArticleUseCase {
    override suspend fun invoke(
        article: ArticleBo,
        isFavorite: Boolean
    ) {
        if (isFavorite) {
            articleGateway.saveFavoriteArticle(article)
        } else {
            articleGateway.removeFavoriteArticle(article)
        }
    }
}

Query use case

Returns processed data:
domain/useCase/src/main/java/es/mobiledev/domain/usecase/article/IsArticleFavoriteUseCase.kt
interface IsArticleFavoriteUseCase {
    suspend operator fun invoke(id: Long): Flow<Boolean>
}

class IsArticleFavoriteUseCaseImpl(
    private val articleGateway: ArticleGateway,
) : IsArticleFavoriteUseCase {
    override suspend fun invoke(id: Long): Flow<Boolean> = 
        articleGateway.isArticleFavorite(id)
}

Collection use case

Retrieves lists of entities:
domain/useCase/src/main/java/es/mobiledev/domain/usecase/article/GetFavoriteArticlesUseCase.kt
interface GetFavoriteArticlesUseCase {
    suspend operator fun invoke(): Flow<List<ArticleBo>>
}

class GetFavoriteArticlesUseCaseImpl(
    private val articleGateway: ArticleGateway,
) : GetFavoriteArticlesUseCase {
    override suspend fun invoke(): Flow<List<ArticleBo>> = 
        articleGateway.getFavoriteArticles()
}

Dependency injection

Register use cases with Hilt for dependency injection:
domain/useCase/src/main/java/es/mobiledev/domain/usecase/di/UseCaseModule.kt
@Module
@InstallIn(ViewModelComponent::class)
object UseCaseModule {
    @Provides
    fun provideGetArticlesUseCase(
        articleGateway: ArticleGateway,
    ): GetArticlesUseCase = GetArticlesUseCaseImpl(articleGateway)

    @Provides
    fun provideGetArticleByIdUseCase(
        articleGateway: ArticleGateway,
    ): GetArticleByIdUseCase = GetArticleByIdUseCaseImpl(articleGateway)

    @Provides
    fun provideGetFavoriteArticlesUseCase(
        articleGateway: ArticleGateway,
    ): GetFavoriteArticlesUseCase = GetFavoriteArticlesUseCaseImpl(articleGateway)

    @Provides
    fun provideSaveOrRemoveFavoriteArticleUseCase(
        articleGateway: ArticleGateway,
    ): SaveOrRemoveFavoriteArticleUseCase = 
        SaveOrRemoveFavoriteArticleUseCaseImpl(articleGateway)

    @Provides
    fun provideIsArticleFavoriteUseCase(
        articleGateway: ArticleGateway,
    ): IsArticleFavoriteUseCase = IsArticleFavoriteUseCaseImpl(articleGateway)
}
1

Define module

Create a Hilt module installed in ViewModelComponent for ViewModel injection.
2

Provide implementations

Each use case interface gets a provider function returning its implementation.
3

Inject dependencies

Hilt automatically injects gateway dependencies into use case constructors.
4

Use in ViewModels

ViewModels can now inject use cases through constructor parameters.

Using use cases in ViewModels

Inject and call use cases from presentation layer:
class HomeViewModel(
    private val getFavoriteArticlesUseCase: GetFavoriteArticlesUseCase,
    private val saveOrRemoveFavoriteUseCase: SaveOrRemoveFavoriteArticleUseCase,
) : BaseViewModel<HomeUiData>() {
    
    fun getFavoriteArticles() {
        viewModelScope.launch {
            uiState.loadingState()
            getFavoriteArticlesUseCase()
                .collect { articles ->
                    uiState.successState { currentData ->
                        currentData.copy(articles = articles)
                    }
                }
        }
    }
    
    fun onFavoriteClick(article: ArticleBo, isFavorite: Boolean) {
        viewModelScope.launch {
            saveOrRemoveFavoriteUseCase(article, isFavorite)
            getFavoriteArticles() // Refresh list
        }
    }
}

Complex business logic example

Combine multiple use cases for complex operations:
interface SyncArticlesUseCase {
    suspend operator fun invoke(): Flow<Result<Unit>>
}

class SyncArticlesUseCaseImpl(
    private val getArticlesUseCase: GetArticlesUseCase,
    private val getFavoriteArticlesUseCase: GetFavoriteArticlesUseCase,
    private val saveLastSyncTimeUseCase: SaveLastSyncTimeUseCase,
) : SyncArticlesUseCase {
    override suspend fun invoke(): Flow<Result<Unit>> = flow {
        try {
            // Fetch latest articles
            getArticlesUseCase(limit = 50, offset = 0)
                .collect { response ->
                    // Get current favorites
                    getFavoriteArticlesUseCase()
                        .collect { favorites ->
                            val favoriteIds = favorites.map { it.id }
                            
                            // Mark favorites in new articles
                            val updated = response.results.map { article ->
                                article.copy(
                                    isFavorite = favoriteIds.contains(article.id)
                                )
                            }
                            
                            // Save sync timestamp
                            saveLastSyncTimeUseCase(System.currentTimeMillis())
                            
                            emit(Result.success(Unit))
                        }
                }
        } catch (e: Exception) {
            emit(Result.failure(e))
        }
    }
}
Complex use cases should still maintain single responsibility. If a use case becomes too large, consider breaking it into smaller, focused use cases.

Testing use cases

Use cases are easy to test with mocked gateways:
class GetArticlesUseCaseTest {
    private val mockGateway = mockk<ArticleGateway>()
    private val useCase = GetArticlesUseCaseImpl(mockGateway)
    
    @Test
    fun `invoke returns articles from gateway`() = runTest {
        // Given
        val expectedArticles = listOf(
            ArticleBo(id = 1, title = "Test", ...)
        )
        coEvery { 
            mockGateway.getArticles(any(), any()) 
        } returns flowOf(ArticleResponseBo(results = expectedArticles))
        
        // When
        val result = useCase(limit = 10, offset = 0).first()
        
        // Then
        assertEquals(expectedArticles, result.results)
        coVerify { mockGateway.getArticles(10, 0) }
    }
}

Best practices

Single responsibility

Each use case should perform one clear business operation.

Interface segregation

Define specific gateway interfaces rather than one large interface.

No Android dependencies

Keep domain layer pure Kotlin for maximum testability.

Operator invoke

Use operator fun invoke() for cleaner call syntax.

Flow returns

Return Flow for reactive data streams that update over time.

Suspend functions

Use suspend for async operations without blocking threads.

Result types

Wrap results in sealed classes or Result type for error handling.

Testability first

Design use cases to be easily testable with mocked dependencies.

Naming conventions

  • Use Bo suffix: ArticleBo, UserBo
  • Descriptive names: ArticleResponseBo for API responses

Build docs developers (and LLMs) love