UseCase Best Practices (Kotlin / Clean Architecture)

UseCase Best Practices (Kotlin / Clean Architecture)

UseCases (also called Interactors) represent one business action in Clean Architecture.
Well-written UseCases make your app clean, testable, and scalable.

This guide gives you real-world best practices, anti-patterns, and production-ready examples.


1. What Exactly is a UseCase?

A UseCase:

  • Represents ONE user or system action

  • Contains business logic

  • Sits in the Domain layer

  • Is framework-independent

Examples:

  • LoginUser

  • GetUsers

  • CreateOrder

  • UpdateProfile


2. One UseCase = One Responsibility (MOST IMPORTANT)

❌ Bad

class UserUseCase {
fun login() {}
fun logout() {}
fun register() {}
}

✅ Good

class LoginUserUseCase
class LogoutUserUseCase
class RegisterUserUseCase

💡 Small UseCases are easier to test and reuse


3. Use operator fun invoke()

This makes UseCases readable and expressive.

class GetUsersUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(): List<User> {
return repository.getUsers()
}
}

Usage:

getUsersUseCase()

4. Keep UseCases Pure (No Android / UI)

❌ Wrong

class LoginUseCase {
fun login(context: Context) { }
}

✅ Correct

class LoginUseCase(
private val repository: AuthRepository
)

✔ No Context
✔ No ViewModel
✔ No LiveData


5. UseCases Should NOT Know About UI State

❌ Bad

class GetUsersUseCase {
fun invoke(): UiState { }
}

✅ Good

class GetUsersUseCase {
fun invoke(): List<User>
}

UI mapping belongs to Presentation layer.


6. Use Sealed Result for Errors (Recommended)

sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}

UseCase:

class GetUsersUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(): Result<List<User>> {
return try {
Result.Success(repository.getUsers())
} catch (e: Exception) {
Result.Error(e.message ?: "Unknown error")
}
}
}

7. Do NOT Catch Exceptions Blindly

❌ Bad

catch (e: Exception) {
return emptyList()
}

✅ Good

catch (e: IOException) { }
catch (e: HttpException) { }

Or let repository handle mapping.


8. UseCases Should Be Synchronous in Design

Even though they use suspend, they should feel synchronous.

val result = getUsersUseCase()

❌ Avoid callbacks inside UseCases


9. UseCases Can Be Composed

UseCases can call other UseCases.

class LoginAndFetchProfileUseCase(
private val login: LoginUseCase,
private val getProfile: GetProfileUseCase
) {
suspend operator fun invoke() {
login()
getProfile()
}
}

✔ Reusable logic
✔ Cleaner ViewModel


10. UseCases Should Not Expose Flow Directly (Usually)

❌ Avoid

fun invoke(): Flow<List<User>>

✅ Prefer

suspend fun invoke(): List<User>

📌 Exception:

  • Streams (location, socket, DB observers)


11. Parameter Object Pattern

Instead of many parameters:

❌ Bad

fun invoke(a: Int, b: String, c: Boolean)

✅ Good

data class Params(val id: Int, val name: String)

fun invoke(params: Params)


12. Naming Conventions (Very Important)

Use verb-based names:

GetUsers
UpdateProfile
DeleteItem

UserManager
UserHelper


13. Unit Testing UseCases (Easy & Fast)

@Test
fun get users returns list() = runTest {
val fakeRepo = FakeUserRepository()
val useCase = GetUsersUseCase(fakeRepo)

val result = useCase()

assertTrue(result is Result.Success)
}


14. When NOT to Create a UseCase

❌ Pure UI logic
❌ Simple data mapping
❌ One-line repository calls in small apps

Don’t over-architect


15. Common Anti-Patterns (Avoid)

❌ Fat UseCases
❌ UI logic in UseCases
❌ Repository duplication
❌ UseCase returning DTOs
❌ God UseCase classes


Final Golden Rules

1️⃣ One UseCase = One action
2️⃣ No Android / UI in UseCases
3️⃣ Use invoke()
4️⃣ Keep UseCases small
5️⃣ Test UseCases heavily


Final Summary

  • UseCases are the heart of business logic

  • Good UseCases = clean, testable apps

  • Kotlin syntax makes UseCases elegant

  • Avoid overengineering

You may also like...