TCA vs Clean Architecture: Reflections from Rewriting a Production App
Introduction
After more than 8 years of iOS development, I've learned to appreciate architecture that serves the team and project, not the other way around. Some time ago, I joined a project where the app was written in The Composable Architecture (TCA). After several months of challenges, we decided to rewrite it using Clean Architecture. I want to share what this taught us.
Challenges We Faced
-
Accumulation of Technical Issues
The application struggled with state management problems: race conditions during concurrent actions, unexpected state mutations, issues with subscription lifecycles. TCA promises predictability through strict unidirectional data flow. In practice, however:
- Every action can trigger an effect,
- Effects can emit subsequent actions,
- Reducers modify shared state,
- Everything happens in an asynchronous environment.
For a team that wasn't 100% comfortable with every aspect of the library, this became a source of frustration and bugs.
-
Debugging Required Detective Work
Example: A user reported an issue with data display after re-login. In traditional architecture, we would check the flow: Repository → Use Case → ViewModel. In TCA, we had to go through multiple layers of reducers, effects, shared state management, and environment dependencies. Finding the source took significantly longer than usual.
The problem? One of the reducers wasn't properly clearing state during logout - something we would have noticed in minutes with simpler architecture.
-
Game-changer: Cross-platform Support
This point tipped the scales in our decision. The Android app was written in Clean Architecture. At some point, the pressure to deliver features became so intense that management moved several developers from the Android team to work on iOS.
This is where the real problem emerged. These developers - experienced seniors who knew Kotlin, Jetpack Compose, and Clean Architecture - hit a wall with TCA. They mastered Swift and SwiftUI quickly. But the specifics of TCA (Reducers, Actions, Effects, Store) were foreign to them and required weeks of learning.
Moreover, architectural differences translated into business logic discrepancies. The Android team operated on clearly defined Use Cases, while we worked with Actions and State mutations. Both apps, despite using the same API, sometimes presented data differently.
After rewriting to Clean Architecture, the situation looked completely different. When we needed support from the Android team again, we received it practically immediately:
- They understood Use Cases - exactly the same pattern,
- They understood Repository pattern - identical to their code,
- They understood SOLID principles - the same principles they applied daily,
One colleague told me: "This is exactly what we write on our side, just in Swift."
Why Did We Decide to Refactor?
The decision wasn't hasty. We considered it for several weeks. Three factors prevailed:
- Synchronization with Android team - common architectural language eliminated discrepancies in business logic,
- Reduced friction - simpler debugging, faster development, shorter onboarding,
- Long-term perspective - Clean Architecture and SOLID are industry standards that will last a decade. TCA, while excellently developed by Point-Free, remains a single-vendor library.
Results
The refactor took 3 months. Results:
- Number of state-related bugs dropped by ~60%,
- Onboarding new developers: reduced from 3-4 weeks to several days,
- Debugging time for typical issues cut in half,
- Rotation between iOS and Android teams became seamless.
Summary
Is TCA a bad choice? No. It's a well-thought-out framework that works in many contexts - especially in smaller teams where everyone is trained in it. But in our case - large, long-term project, requirement for cross-platform synchronization, developer rotation, need to deliver new features as quickly as possible - independence from a specific library outweighed the benefits of TCA.
Key lesson: there are no bad architectures, only wrong choices for a given context. Choose consciously, considering:
- Team size and experience,
- Expected project lifespan,
- Coordination with other platforms,
- Developer availability in the market.
Architecture should serve the project and team. Not the other way around.

Rafał Dubiel
Senior iOS Developer with 8+ years of experience building mobile applications. Passionate about clean architecture, Swift, and sharing knowledge with the community.
Share this article: