Deep dive into Domain, Data, and Presentation layers with real code examples
Clean Architecture organizes code into distinct layers, each with specific responsibilities and clear dependency rules. This architecture ensures that business logic remains independent of frameworks, databases, and UI.
The fundamental rule of Clean Architecture: Source code dependencies must point only inward, toward higher-level policies.Outer layers can depend on inner layers, but inner layers must never depend on outer layers.
The domain layer is the heart of the application. It contains:
Business entities and domain models
Repository interfaces (contracts)
Use cases that orchestrate business logic
Domain-specific validation and rules
The domain layer is completely framework-independent and implemented in pure Kotlin or Kotlin Multiplatform. It has no dependencies on Android or any external framework.
data class Cart(val cartItems: List<CartItem>) { fun getSubtotal(): Money? { return if (cartItems.isNotEmpty()) { val currency = cartItems[0].money.currencySymbol var subtotal = 0.0 cartItems.forEach { subtotal += it.money.amount * it.quantity } Money(subtotal, currency) } else { null } } fun getNumberOfItems(): Int { return cartItems.sumOf { it.quantity } }}
Notice how the Cart entity contains business logic for calculating subtotals and counting items. This logic is independent of how the data is stored or displayed.
internal interface UserRepository { suspend fun login(loginRequest: LoginRequest): Answer<Unit, LoginError> fun getUser(): User fun isLoggedIn(): Boolean}
internal class RealCartViewModel( observeUserCart: ObserveUserCart, private val updateCartItem: UpdateCartItem, private val stateDelegate: StateDelegate<CartScreenState>) : CartViewModel, StateViewModel<CartScreenState> by stateDelegate, ViewModel() { init { stateDelegate.setDefaultState(CartScreenState(Cart(emptyList()))) observeUserCart().onEach { cart -> stateDelegate.updateState { CartScreenState(cart) } }.launchIn(viewModelScope) } override fun updateCartItemQuantity(cartItem: CartItem) { updateCartItem(cartItem) }}
The ViewModel depends on use case interfaces (ObserveUserCart, UpdateCartItem), not on repository implementations. This maintains the dependency inversion principle.
class CompositionRoot private constructor( applicationContext: Context) { private val httpClient by lazy { RealHttpClientProvider.getClient() } private val cacheProvider by lazy { AndroidCacheProvider(applicationContext) } private val userComponentAssembler by lazy { UserComponentAssembler(httpClient, cacheProvider) } private val productComponentAssembler by lazy { ProductComponentAssembler(httpClient) } private val wishlistComponentAssembler by lazy { WishlistComponentAssembler(cacheProvider, userComponentAssembler.getUser) } private val cartComponentAssembler by lazy { CartComponentAssembler(cacheProvider, userComponentAssembler.getUser) } val plpUIAssembler by lazy { PLPUIAssembler( userComponentAssembler.getUser, productComponentAssembler.getProducts, wishlistComponentAssembler, cartComponentAssembler.addCartItem ) } // ...}
class CartComponentAssembler( private val cacheProvider: CacheProvider, private val getUser: GetUser) { private val cartRepository by lazy { RealCartRepository(cacheProvider) } val updateCartItem: UpdateCartItem by lazy { UpdateCartItemUseCase(getUser, cartRepository) } val observeUserCart: ObserveUserCart by lazy { ObserveUserCartUseCase(getUser, cartRepository) } val addCartItem: AddCartItem by lazy { AddCartItemUseCase(getUser, cartRepository, updateCartItem) }}