Your Use Cases Are Just Wrappers -And That’s a Bigger Problem Than You Think
Clean Architecture promised to protect your business logic. Instead, you built a bureaucracy around it.
Your Use Cases Are Just Wrappers -And That’s a Bigger Problem Than You Think
Clean Architecture promised to protect your business logic. Instead, you built a bureaucracy around it.

In my last article, I argued against using extension functions for domain mapping. The response was split some agreed, some pushed back, and a surprising number said something like: “If we’re questioning mappers, can we talk about Use Cases too?”
Yes. Let’s talk about Use Cases.
Open any Android or Kotlin Multiplatform project that claims Clean Architecture. You’ll find a domain/usecase package containing dozens of classes that look like this:
class GetUserUseCase(
private val userRepository: UserRepository
) {
suspend operator fun invoke(id: UserId): User {
return userRepository.getUser(id)
}
}
One class. One function. One line of delegation. Repeated two hundred times across the codebase.
This is not Clean Architecture. This is a forwarding layer pretending to be a domain.
How We Got Here
Robert Martin’s original Use Case concept describes an application-specific business rule. It orchestrates entities, enforces invariants, and coordinates workflows that don’t belong to any single entity. The Use Case is where your business lives.
Somewhere along the way probably around 2017 when Clean Architecture became gospel in the Android community the idea got flattened. Blog posts and sample projects reduced Use Cases to a convention: one class per operation, one function called invoke or execute, injected into a ViewModel.
The rationale was sound in theory: even if the Use Case is simple today, wrapping it gives you a place to add logic later without changing the ViewModel.
In practice, “later” almost never comes. And while you’re waiting for it, you’re paying a real cost right now.
1. You’re Paying an Abstraction Tax on Every Feature
Every new feature in a Use-Case-per-operation codebase requires:
- A repository interface method
- A repository implementation
- A Use Case class
- A ViewModel that calls the Use Case
- Dependency injection wiring for the Use Case
For a screen that fetches a user, updates a preference, and logs an event, that’s three Use Case files, three constructor parameters in the ViewModel, and three DI bindings — before you’ve written a single line of actual logic.
A junior developer on my team once spent a full day scaffolding these layers for a feature whose entire business logic was: “fetch the list, sort it by date.” The sorting happened in the ViewModel anyway because the Use Case didn’t feel like the right place for UI-specific ordering. The Use Case class existed, did nothing, and would continue to do nothing.
Every abstraction is a trade: you pay complexity now for flexibility later. A wrapper Use Case gives you no flexibility. It just forwards a call. You’re paying the tax and getting nothing back.
2. It Hides the Fact That You Have No Domain Logic
This is the deeper problem, and the one nobody wants to confront.
If every Use Case in your project is a one-liner that delegates to a repository, your application doesn’t have a domain layer. You have a data layer and a presentation layer with a ceremonial hallway between them. The Use Case package isn’t protecting business logic; it’s disguising the absence of it.
And that’s fine. Not every application has complex business rules. A CRUD app that reads and writes data through an API is a legitimate, common, valuable piece of software. But forcing it into a Clean Architecture template doesn’t add value. It adds indirection.
Acknowledging “this is a thin data-driven app” is an architectural decision. It’s an honest one. Wrapping every repository call in a Use Case so the package structure looks like Clean Architecture is not a decision; it’s cargo culting.
3. The “We’ll Add Logic Later” Argument Doesn’t Hold Up
The most common defense: “Use Cases start simple, but when business logic appears, you already have a place to put it.”
I’ve audited codebases with 150+ Use Cases. Here’s what I typically find:
- 70% are pure delegation. One line. Call the repository, return the result.
- 20% do light transformation: mapping, sorting, combining two repository calls. Logic that could live in the repository or a service without architectural compromise.
- 10% contain genuine business rules:validation, multi-step workflows, conditional branching based on domain state.
That 10% deserves a dedicated class. Absolutely. Those are real Use Cases. The other 90% exist because a convention demanded it, not because a design required it.
“But what if the simple ones grow complex?” Then you extract a class at that point. It takes two minutes. Your IDE can do it in three keystrokes. You don’t pre-build scaffolding for a building that might never get a second floor.
4. It Makes Navigation and Comprehension Harder
When a ViewModel calls getUserUseCase(id), I have to open the Use Case to see what it does. Nine times out of ten, it does nothing it just calls the repository. But I didn't know that until I checked. That's a context switch. Multiply it by every operation on every screen, and code review becomes an exercise in opening files to confirm they're empty.
Compare:
// With wrapper Use Cases
class ProfileViewModel(
private val getUserUseCase: GetUserUseCase,
private val getPostsUseCase: GetUserPostsUseCase,
private val getFollowersUseCase: GetFollowersUseCase,
private val updateBioUseCase: UpdateBioUseCase
)
// Without — when the logic is just delegation
class ProfileViewModel(
private val userRepository: UserRepository,
private val postRepository: PostRepository,
private val socialRepository: SocialRepository
)
The second version tells me exactly where the data comes from. If I want to understand how users are fetched, I go to UserRepository. There's no intermediate layer to check, no empty class to open and close. The code is honest about what it does.
5. It Inflates Your Dependency Graph
Each Use Case is a node in your DI graph. In a Hilt or Koin setup, everyone needs to be declared, scoped, and wired. A screen with five operations means five Use Cases injected into the ViewModel, each of which has its own dependencies.
This isn’t just boilerplate, it has real consequences:
- Build times increase as the DI graph grows
- Constructor parameter lists balloon, making classes harder to instantiate in tests
- DI configuration files become walls of bindings that nobody reads but everyone maintains
When a Use Case contains real orchestration logic, the DI cost is justified you’re injecting a meaningful unit of behavior. When it’s a wrapper, you’re injecting a forwarding address.
6. Testing Becomes Theatrical
@Test
fun `invoke returns user from repository`() {
val user = User(id = UserId("1"), name = "Alice")
coEvery { repository.getUser(UserId("1")) } returns user
val result = getUserUseCase(UserId("1"))
assertEquals(user, result)
}
What does this test verify? That a function returns what another function returns. It’s a test for the Kotlin compiler’s ability to delegate calls. It will never fail unless someone accidentally deletes the line inside the Use Case. It provides zero confidence about business correctness.
But it inflates your test count. Your team reports 400 unit tests. Half of them test wrappers. Your coverage number looks healthy. Your actual behavioral coverage is half of what you think.
Real Use Cases: ones that contain conditional logic, validation, orchestration deserve tests. Those tests verify behavior that could break. Wrapper tests verify nothing.
What to Do Instead
Write Use Cases only when they earn their existence.
A Use Case earns its existence when it does at least one of:
- Orchestrates multiple repositories or services
- Enforces a business invariant or validation rule
- Contains conditional logic based on domain state
- Transforms data in a way that reflects a business rule, not a display concern
If the operation is “get X from the repository and return it,” let the ViewModel call the repository. Your architecture won’t collapse. The dependency rule isn’t violated the ViewModel already depends on a domain-layer interface. You’re just removing a passthrough.
// Before: wrapper Use Case
class GetUserUseCase(private val repo: UserRepository) {
suspend operator fun invoke(id: UserId): User = repo.getUser(id)
}
// After: ViewModel calls the repository directly
class ProfileViewModel(private val userRepository: UserRepository) {
fun loadUser(id: UserId) {
viewModelScope.launch {
val user = userRepository.getUser(id)
_state.value = ProfileState.Loaded(user)
}
}
}
When the day comes that fetching a user requires checking permissions, validating account status, and merging data from two sources that’s when you introduce GetUserUseCase. It will contain real logic. It will be worth testing. It will justify its existence.
The Pattern That Actually Works
On teams I’ve led, we follow a simple rule: Use Cases are for orchestration, repositories are for data access, and ViewModels are for presentation state. If an operation doesn’t need orchestration, it doesn’t get a Use Case.
The result? A domain/usecase package with 15 classes instead of 150. Every one of them contains meaningful logic. Every one has tests that verify actual behavior. New developers can read the package and immediately understand what the application's business rules are because the noise is gone.
It’s About Intellectual Honesty
Clean Architecture is a set of principles, not a folder structure. The principle is: protect your business logic from infrastructure concerns. If your business logic is “fetch data and display it,” then a repository interface already provides that protection. Adding an empty class on top doesn’t make the architecture cleaner. It makes it deeper.
The best codebases I’ve worked in weren’t the ones with the most layers. They were the ones where every layer did something. Where opening a file always taught you something about the system. Where the architecture reflected the actual complexity of the problem, not the aspirational complexity of the template.
Write Use Cases when you have use cases. Not before.
👋 Let’s Connect
If you enjoy content about Android development, Jetpack Compose, Kotlin, software architecture, and engineering best practices, I’d love to stay connected.
📺 **YouTube** In-depth Android tutorials, real-world projects, architecture discussions, and practical development tips.
💼 **LinkedIn** Professional updates, technical insights, articles, and lessons from my software engineering journey.
✍️ Medium More deep dives into Android development, clean architecture, testing, performance optimization, and modern engineering practices.
📚 Want to Go Deeper?
If you’re looking for structured, comprehensive learning resources, check out my Android development books, where I cover concepts in much greater depth than a typical article.
☕ Support My Work
Creating free technical content, books, tutorials, and open educational resources takes considerable time and effort. If this article helped you learn something valuable, you can support my work by:
• Purchasing one of my books • Sharing my articles with others • Buying me a coffee
Every bit of support helps me continue creating high-quality content for the Android community.

SOLID Principles for Android Developers — Learn how to write maintainable, scalable, and testable Android applications using SOLID principles with real-world examples.
👉 Explore all my books: Book Link
메타데이터
- post_id
- f3feed34e0d7
- slug
- your-use-cases-are-just-wrappers-and-thats-a-bigger-problem-than-you-think-f3feed34e0d7
- url
- https://medium.com/@himanshugaur684/your-use-cases-are-just-wrappers-and-thats-a-bigger-problem-than-you-think-f3feed34e0d7
- canonical_url
- https://medium.com/@himanshugaur684/your-use-cases-are-just-wrappers-and-thats-a-bigger-problem-than-you-think-f3feed34e0d7
- author_url
- https://medium.com/@himanshugaur684
- status
- ok
- fetched_at
- 2026-06-09 15:37:30