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:
Author
Socials
Response wrapper
// 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)
}
Define module
Create a Hilt module installed in ViewModelComponent for ViewModel injection.
Provide implementations
Each use case interface gets a provider function returning its implementation.
Inject dependencies
Hilt automatically injects gateway dependencies into use case constructors.
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
Models
Gateways
Use Cases
Use Bo suffix: ArticleBo, UserBo
Descriptive names: ArticleResponseBo for API responses
Use Gateway suffix: ArticleGateway
Named after domain concept, not implementation
Verb + noun + UseCase: GetArticlesUseCase
Clear action: SaveOrRemoveFavoriteArticleUseCase