πŸ›οΈ Android Architecture and Clean Code

Use Cases and Domain Layers: Are They Necessary?

Some teams wrap every repository call in a use case; others never write one. Google's guidance calls the domain layer optional, and that word matters. Here's a practical test for when a use case earns its place, and how to write good ones.

✍️ Yunok Tech πŸ“… Updated ⏱ 3 min read 🎯 Intermediate
On this page
  1. The three-question test
  2. Writing a good use case
  3. Testing
  4. The pass-through problem
  5. Where use cases live
  6. Good candidates in real apps
  7. Frequently asked questions
  8. Conclusion

A use case (also called an interactor) is a class that performs one business operation, such as "place order" or "get visible articles", typically by combining repositories and applying rules. Use cases can make code clearer and more reusable, or they can be empty wrappers that double the number of files without adding meaning.

The three-question test

Create a use case when at least one of these is true:

  1. Real business logic exists beyond fetching data: rules, calculations or validation that the product team would describe ("orders over β‚Ή500 get free delivery unless…").
  2. Reuse: the same logic is needed by several ViewModels, a widget, a notification handler or a worker.
  3. Combination: the operation coordinates several repositories, such as user + cart + pricing.

If none apply, call the repository directly from the ViewModel.

Optional domain layer between ViewModels and repositories Two ViewModels and a WorkManager worker use a CalculateDeliveryFeeUseCase, which combines CartRepository, UserRepository and PricingRules. A simple settings ViewModel calls SettingsRepository directly, skipping the domain layer. CartViewModel CheckoutViewModel ReminderWorker CalculateDeliveryFeeUseCase (rules) CartRepository UserRepository SettingsRepository SettingsViewModel (no use case)
Use cases appear where logic is shared or complex, not uniformly.

Writing a good use case

CalculateDeliveryFeeUseCase.ktKotlin
data class DeliveryQuote(val feePaise: Long, val freeDeliveryShortfallPaise: Long)

class CalculateDeliveryFeeUseCase @Inject constructor(
    private val cartRepository: CartRepository,
    private val userRepository: UserRepository,
) {
    /** Free delivery above β‚Ή500 for regular users; always free for members. */
    operator fun invoke(): Flow<DeliveryQuote> =
        combine(cartRepository.observeCart(), userRepository.observeUser()) { cart, user ->
            val subtotal = cart.items.sumOf { it.unitPaise * it.qty }
            when {
                user.isMember || subtotal >= FREE_THRESHOLD -> DeliveryQuote(0, 0)
                else -> DeliveryQuote(STANDARD_FEE, FREE_THRESHOLD - subtotal)
            }
        }

    private companion object {
        const val FREE_THRESHOLD = 50_000L    // β‚Ή500 in paise
        const val STANDARD_FEE = 4_000L       // β‚Ή40
    }
}

// In a ViewModel: val quote = calculateDeliveryFee().stateIn(viewModelScope, WhileSubscribed(5000), null)
  • One public operation, often exposed via operator fun invoke so callers write calculateDeliveryFee().
  • Named as verb + noun: PlaceOrderUseCase, GetVisibleArticlesUseCase.
  • Pure Kotlin, with no Android types. Main-safe, because threading is handled in repositories.
  • Returns domain models or typed results, never UI strings or colours.

Testing

CalculateDeliveryFeeUseCaseTest.ktKotlin
@Test
fun `members always get free delivery`() = runTest {
    val useCase = CalculateDeliveryFeeUseCase(
        FakeCartRepository(items = listOf(CartItem("a", unitPaise = 10_000, qty = 1))),
        FakeUserRepository(User(isMember = true)),
    )
    assertEquals(DeliveryQuote(0, 0), useCase().first())
}

The pass-through problem

A use case that adds nothingKotlin
class GetUserUseCase @Inject constructor(private val repo: UserRepository) {
    operator fun invoke(id: String) = repo.getUser(id)    // just forwards: delete it
}

Some teams mandate use cases everywhere for consistency, so ViewModels never touch repositories. That's a legitimate team choice in very large codebases, but weigh the cost: more files, more indirection, slower navigation. Google's guidance treats the domain layer as optional.

Where use cases live

  • Small app: a domain package inside the feature.
  • Modular app: a pure-Kotlin :core:domain or :feature:x:domain module, where the compiler enforces "no Android" automatically.

Good candidates in real apps

Checkout validation

Stock, address, payment and coupon rules combined into a ValidateCheckoutUseCase used by the screen and by a background retry.

Feed ranking

Filtering muted authors, pinning and sorting, shared by the feed screen and a widget.

Sync orchestration

A SyncAllUseCase calling several repositories in order, used by WorkManager and pull-to-refresh.

Frequently asked questions

Can a use case call another use case?

Yes, when composing operations makes sense. Avoid deep chains that are hard to follow.

Should use cases switch dispatchers?

Only if they do heavy CPU work themselves, using withContext(Dispatchers.Default) with an injected dispatcher. Otherwise rely on main-safe repositories.

Use case or extension function?

For small, pure transformations without dependencies, a function or extension can be simpler than a class.

Conclusion

Use cases are valuable for real, reusable or cross-repository business logic, and noise when they only forward calls. Keep them single-purpose, pure Kotlin and easy to test, and let simple screens talk to repositories directly.

Next step: expose the results cleanly to the UI with Managing UI State with ViewModel and StateFlow.

Official references

πŸ“± Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps β†’

Ready to build real Android applications?

Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp