To fetch a list of items, you parse the network response into DTOs, map them into Domain entities, and then map those into UI models.
For a screen with a handful of items, this overhead is negligible.
But what if you are processing a fast-scrolling feed, real-time socket events, or parsing thousands of records?
- Every mapper creates new intermediate objects.
- Short-lived objects pile up quickly.
- The garbage collector works harder, causing micro-stutters and frame drops.
You have perfect separation of concerns, but you’ve traded memory churn for architectural purity.
2. Layers of Indirection
Clean Architecture relies heavily on abstraction:
UI → ViewModel → UseCase → Repository Interface → Repository Impl → DataSource Interface → DataSource Impl
Each layer introduces an interface, an extra method call, and additional indirection.
While modern compilers and runtimes are great at inlining simple calls, deep layers of indirection can still:
- Increase stack depth.
- Hurt CPU cache locality.
- Increase app startup time due to massive dependency injection graphs resolving dozens of objects before the first frame even draws.
If an entire UseCase does nothing except call repository.getData() without any validation, transformation, or business logic, it isn’t adding value—it’s just runtime luggage.
Clean vs. Performant: Two Different Objectives
Optimizing for readability and optimizing for hardware efficiency often pull in opposite directions:
1. Data Pipelines
- Clean Focus: Dedicated models per layer (
DTO → Domain → UI) - Performance Cost: High memory churn and GC pauses from continuous mapping
2. Dependency Flow
- Clean Focus: Use cases and interface boundaries for decoupling
- Performance Cost: Deep call stacks, virtual method dispatch, and DI resolution
3. Data Access
- Clean Focus: Granular, single-purpose queries per repository
- Performance Cost: Missed batch reads, cache misses, and excessive roundtrips
4. State Handling
- Clean Focus: Strict immutability and defensive copying across layers
- Performance Cost: Unnecessary allocations compared to flat observable states
5. App Startup
- Clean Focus: Eager initialization of full dependency graphs
- Performance Cost: Increased cold-start latency before the first frame draws
A piece of code can be architecturally spotless, fully covered by unit tests, perfectly decoupled—and still drop frames or feel sluggish.
Conversely, some of the fastest code in game development, high-frequency systems, and performance-critical algorithms looks completely "dirty" by typical Clean Architecture standards.
So, Should We Stop Using Clean Architecture?
Of course not.
The takeaway isn't that Clean Architecture is bad. It's that we shouldn't expect it to solve problems it was never designed to solve.
Architecture gives you development speed, testability, and team scalability.
Performance requires profiling, memory analysis, efficient data structures, and understanding the platform runtime.
Once you decouple those two ideas, decision-making becomes clearer:
- In everyday features, favor clean boundaries because maintainability and team velocity usually matter most.
- In hot paths—like tight animation loops, audio/video pipelines, high-frequency streams, or complex list rendering—be willing to flatten abstractions and skip unnecessary mapping layers.
Final Thoughts
Good architecture isn't about following every rule from a book; it's about making conscious trade-offs.
Clean code makes life easier for your team today. Performance optimization makes life better for your users right now.
Knowing when to choose one over the other is where real engineering begins.
