Skip to main content
Component modules encapsulate business logic for specific features. Each component module contains both the domain layer (business rules, entities, use cases) and the data layer (repository implementations, DTOs, data sources).

Module Overview

cart-component

Shopping cart business logic

money-component

Money and currency domain model

product-component

Product catalog management

user-component

User authentication and management

wishlist-component

Wishlist functionality

Architecture Pattern

All component modules follow the same structure:
component-module/
├── domain/
│   ├── model/          # Domain entities
│   ├── repository/     # Repository interfaces
│   └── usecase/        # Business logic
├── data/
│   ├── model/          # DTOs for serialization
│   └── repository/     # Repository implementations
└── di/
    └── ComponentAssembler.kt  # Dependency injection
The domain layer contains no framework dependencies and represents pure business logic. The data layer implements domain contracts using specific technologies (HTTP, cache, etc.).

cart-component

Manages shopping cart operations including adding items, updating quantities, and observing cart state.

Module Dependencies

sourceSets {
    commonMain {
        dependencies {
            implementation(libs.coroutines.core)
            implementation(libs.kotlin.serialization)
            implementation(project(":cache"))
            implementation(project(":money-component"))
            implementation(project(":user-component"))
        }
    }
}

Domain Layer

1

Domain Model

The Cart entity represents a user’s shopping cart with business logic:
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 item counts - this keeps the presentation layer simple.
2

Repository Interface

Defines contracts for cart data operations:
internal interface CartRepository {
    fun updateCartItem(userId: String, cartItem: CartItem)
    fun observeCart(userId: String): Flow<Cart>
    fun getCart(userId: String): Cart
}
3

Use Cases

Functional interfaces representing business operations:
fun interface UpdateCartItem {
    operator fun invoke(cartItem: CartItem)
}

fun interface ObserveUserCart {
    operator fun invoke(): Flow<Cart>
}

fun interface AddCartItem {
    operator fun invoke(cartItem: CartItem)
}
Using fun interface enables SAM (Single Abstract Method) conversion, allowing lambdas to be used as implementations.

Data Layer

Implements cart persistence using the cache module:
class RealCartRepository(
    private val cacheProvider: CacheProvider
) : CartRepository {
    // Implementation uses FlowCachedObject for reactive cart updates
    // Serializes cart data as JsonCartCacheDto
}

Dependency Injection

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)
    }
}
The CartComponentAssembler uses lazy initialization to avoid creating unnecessary instances. Dependencies are provided via constructor injection.

money-component

Provides a domain model for representing monetary values with currency.

Domain Model

data class Money(val amount: Double, val currencySymbol: String)
This is a pure domain module with no data layer - it only contains the domain model used by other components like cart-component and product-component.

Usage Example

From product-component:
data class Product(
    val id: String,
    val name: String,
    val money: Money,
    val imageUrl: String
)

product-component

Manages product catalog operations including fetching products from a remote API.

Domain Layer

data class Product(
    val id: String,
    val name: String,
    val money: Money,
    val imageUrl: String
)
internal interface ProductRepository {
    suspend fun getProducts(): Answer<List<Product>, String>
}
Uses the Answer type from foundations module for error handling.
fun interface GetProducts {
    suspend operator fun invoke(): Answer<List<Product>, String>
}

Data Layer

Implements product fetching via HTTP:
class RealProductRepository(
    private val httpClient: HttpClient
) : ProductRepository {
    override suspend fun getProducts(): Answer<List<Product>, String> {
        // Makes HTTP request
        // Deserializes JsonProductResponseDTO
        // Maps to domain Product entities
    }
}

Dependency Injection

class ProductComponentAssembler(
    private val httpClient: HttpClient
) {
    private val productRepository by lazy {
        RealProductRepository(httpClient)
    }

    val getProducts by lazy {
        GetProducts(productRepository::getProducts)
    }
}
The ProductComponentAssembler depends on HttpClient from the httpclient module, demonstrating how components can depend on library modules.

user-component

Handles user authentication, session management, and user data persistence.

Domain Layer

1

Domain Models

Email Value Object:
class Email(private val value: String) {
    fun isValid(): Boolean {
        return value.isNotBlank() && value.matches(Regex(EMAIL_ADDRESS_PATTERN))
    }

    private companion object {
        const val EMAIL_ADDRESS_PATTERN =
            "(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+..."
    }
}
Password Value Object:Similar to Email, encapsulates validation logic.User Entity:
data class User(
    val id: String,
    val username: String,
    val email: String,
    val token: String
)
2

Use Cases

fun interface Login {
    suspend operator fun invoke(
        loginRequest: LoginRequest
    ): Answer<Unit, LoginError>
}

fun interface GetUser {
    operator fun invoke(): User
}

fun interface IsUserLoggedIn {
    operator fun invoke(): Boolean
}

Data Layer

Combines HTTP and cache:
class RealUserRepository(
    private val httpClient: HttpClient,
    private val cacheProvider: CacheProvider
) : UserRepository {
    // Login via HTTP, cache user data locally
    // Provide access to cached user data
}

Dependency Injection

class UserComponentAssembler(
    private val httpClient: HttpClient,
    private val cacheProvider: CacheProvider
) {

    private val userRepository by lazy {
        RealUserRepository(httpClient, cacheProvider)
    }

    val login: Login by lazy {
        LoginUseCase(userRepository)
    }

    val getUser by lazy {
        GetUser(userRepository::getUser)
    }

    val isUserLoggedIn by lazy {
        IsUserLoggedIn(userRepository::isLoggedIn)
    }
}
The UserComponentAssembler is used by other components (like cart-component and wishlist-component) to get the current user.

wishlist-component

Manages user wishlist functionality including adding/removing products and observing wishlist changes.

Domain Layer

data class WishlistItem(
    val id: String,
    val name: String,
    val money: Money,
    val imageUrl: String
)
internal interface WishlistRepository {
    fun addToWishlist(userId: String, wishlistItem: WishlistItem)
    fun removeFromWishlist(userId: String, wishlistItemId: String)
    fun observeWishlist(userId: String): Flow<List<WishlistItem>>
    fun observeWishlistIds(userId: String): Flow<List<String>>
}
fun interface AddToWishlist {
    operator fun invoke(wishlistItem: WishlistItem)
}

fun interface RemoveFromWishlist {
    operator fun invoke(wishlistItemId: String)
}

fun interface ObserveUserWishlist {
    operator fun invoke(): Flow<List<WishlistItem>>
}

fun interface ObserveUserWishlistIds {
    operator fun invoke(): Flow<List<String>>
}
ObserveUserWishlistIds is useful for checking if products are in the wishlist without loading full wishlist items.

Data Layer

Persists wishlist data locally:
class RealWishlistRepository(
    private val cacheProvider: CacheProvider
) : WishlistRepository {
    // Uses FlowCachedObject for reactive updates
    // Serializes as JsonWishlistCacheDTO
}

Dependency Injection

class WishlistComponentAssembler(
    private val cacheProvider: CacheProvider,
    private val getUser: GetUser
) {
    private val wishlistRepository by lazy {
        RealWishlistRepository(cacheProvider)
    }

    val addToWishlist: AddToWishlist by lazy {
        AddToWishlistUseCase(getUser, wishlistRepository)
    }

    val removeFromWishlist: RemoveFromWishlist by lazy {
        RemoveFromWishlistUseCase(getUser, wishlistRepository)
    }

    val observeUserWishlist: ObserveUserWishlist by lazy {
        ObserveUserWishlistUseCase(getUser, wishlistRepository)
    }

    val observeUserWishlistIds: ObserveUserWishlistIds by lazy {
        ObserveUserWishlistIdsUseCase(getUser, wishlistRepository)
    }
}

Component Dependencies

Component modules can depend on:
1

Library Modules

All components can use library modules like cache, httpclient, and foundations.
2

Other Component Modules

Components can depend on other components’ domain layer:
  • cart-component depends on money-component and user-component
  • wishlist-component depends on user-component
  • product-component depends on money-component
3

No UI Dependencies

Component modules should never depend on UI modules. Data flows one direction only.

Best Practices

Keep Domain Layer PureThe domain layer should contain no Android or framework dependencies. Use only Kotlin standard library and Kotlin coroutines.
Use Value ObjectsFor domain concepts with validation logic (like Email, Password), create value objects that encapsulate the validation.
Repository PatternAlways define repository interfaces in the domain layer and implementations in the data layer. This keeps the domain independent of data sources.
Functional InterfacesUse fun interface for use cases with a single operation. This enables clean lambda syntax and makes testing easier.

Testing Component Modules

Each component module includes comprehensive tests:
commonTest/
├── data/
│   └── repository/
│       └── RealCartRepositoryTest.kt
├── domain/
│   ├── model/
│   │   └── CartTest.kt
│   ├── repository/
│   │   └── TestCartRepository.kt  # Test double
│   └── usecase/
│       ├── AddCartItemUseCaseTest.kt
│       └── ObserveUserCartUseCaseTest.kt
1

Test Domain Logic

Test entities and value objects directly - they have no dependencies.
2

Test Use Cases

Use test doubles for repositories (like TestCartRepository).
3

Test Repository Implementations

Use TestCacheProvider from cache-test module.

Build docs developers (and LLMs) love