Skip to main content
The Compose Project Template follows Clean Architecture principles with a modular structure that separates concerns and ensures scalability. The architecture is divided into three main layers: Presentation, Domain, and Data.

Architectural layers

The project implements a unidirectional data flow architecture where each layer has a specific responsibility and depends only on layers below it.

Presentation

UI components, screens, and ViewModels built with Jetpack Compose

Domain

Business logic, use cases, and domain models independent of frameworks

Data

Data sources, repositories, and external API integrations

Module structure

The project is organized into multiple Gradle modules to enforce separation of concerns and enable parallel builds:
CPT/
├── app/                    # Application entry point
├── feature/                # Feature modules (UI layer)
│   ├── home/
│   ├── launcher/
│   └── articledetail/
├── domain/                 # Business logic layer
│   ├── model/             # Business objects (Bo)
│   ├── gateway/           # Repository interfaces
│   └── useCase/           # Use case implementations
├── data/                   # Data layer
│   ├── source/            # Data source interfaces
│   ├── repository/        # Repository implementations
│   ├── remote/            # API data sources
│   ├── local/             # Database data sources
│   └── session/           # Preferences data sources
├── common/                 # Pure Kotlin utilities
├── commonAndroid/          # Android-specific shared code
└── navigation/             # Navigation definitions

Data flow

The architecture enforces a clear data flow pattern:
1

User interaction

User interacts with the UI (Composable screens)
2

ViewModel processing

ViewModel receives the event and calls appropriate use cases
3

Business logic execution

Use cases execute business logic and interact with gateways (repository interfaces)
4

Data retrieval

Repositories coordinate between remote and local data sources
5

State update

Data flows back through use cases to ViewModel, updating UI state
6

UI recomposition

Composables observe state changes and recompose automatically

Dependency flow

The architecture follows the Dependency Rule: dependencies only point inward toward the domain layer.
The domain layer has no Android dependencies, making business logic testable and portable.

Key architectural patterns

Repository pattern

Repositories implement gateway interfaces and coordinate between multiple data sources:
class ArticleRepository(
    private val remote: ArticleRemoteDataSource,
    private val local: ArticleLocalDataSource,
) : ArticleGateway {
    override suspend fun getArticles(
        limit: Long,
        offset: Long
    ): Flow<ArticleResponseBo> = flowOf(remote.getArticles(limit, offset))
    
    override suspend fun getFavoriteArticles(): Flow<List<ArticleBo>> = 
        flowOf(local.getFavoriteArticles())
}

Gateway (interface) pattern

The domain layer defines gateway interfaces that the data layer implements:
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>
}
Gateways are defined in the domain layer but implemented in the data layer, inverting the dependency direction.

Use case pattern

Each use case encapsulates a single business operation:
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, offset)
}

MVVM pattern

Feature modules use the Model-View-ViewModel pattern with Jetpack Compose:
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getArticlesUseCase: GetArticlesUseCase,
    private val getFavoriteArticlesUseCase: GetFavoriteArticlesUseCase,
) : BaseViewModel<HomeUiState>() {
    override val uiState: MutableStateFlow<UiState<HomeUiState>> = 
        MutableStateFlow(value = UiState(data = HomeUiState()))
    
    private suspend fun getArticles() =
        getArticlesUseCase(limit = 5L, offset = 0L).collectLatest { response ->
            uiState.successState { currentUiState ->
                currentUiState.copy(articles = response.results)
            }
        }
}

Benefits of this architecture

  • Pure domain logic without Android dependencies
  • Easy mocking of interfaces for unit tests
  • ViewModels testable without UI framework
  • Clear separation of concerns
  • Changes isolated to specific layers
  • Easy to locate and modify features
  • Modular structure supports team collaboration
  • Parallel builds with Gradle modules
  • Easy to add new features without affecting existing code
  • Swap implementations without changing business logic
  • Support multiple data sources (API, database, cache)
  • Framework-independent domain layer
Never let the domain layer depend on the data or presentation layers. This violates the Dependency Rule and breaks the architecture.

Next steps

Clean architecture

Learn about clean architecture principles

Modules

Explore module organization

Dependency injection

Understand Hilt DI setup

Build docs developers (and LLMs) love