Clean Architecture gets written about a lot and practiced less. In a production Flutter codebase the value shows up on a specific Tuesday: the product team decides the backend is moving from REST to Firestore, or the auth provider swaps from a bespoke service to Firebase Auth. If your architecture has been honest about its layers, that Tuesday is a two-day task instead of a two-week one.
The three layers I actually ship
Presentation — widgets, BLoCs, and the routing shell. This layer knows about BuildContext and flutter_bloc. It never imports anything from data/.
Domain — pure Dart. Entities, use cases, and repository interfaces. No Flutter imports, no packages that touch I/O. If a new junior joins the team, this is where I point them first: the domain layer is the app's vocabulary.
Data — repository implementations, Firebase / REST / local DB sources, DTOs and their mappers. This is where cloud_firestore, dio, and realm live. It depends on domain, never the other way around.
Why BLoC sits in presentation, not domain
A common mistake is pushing BLoCs down into domain because "they have business logic." They don't — they have orchestration logic. BLoCs translate user intent into use-case invocations and map the results into view states. Keep them in presentation and your domain stays testable without a Flutter SDK.
The boundaries that save you
- Repository interfaces in
domain/, implementations indata/ - DTOs never leak out of
data/— map to entities at the boundary freezedfor entities and states; it makes the mapping mechanical- One
get_itregistration file per layer
When not to bother
Throwaway prototypes. Internal tools that will be rewritten. A 3-screen app for a single event. Architecture is insurance — if the app is short-lived, skip the premium.
Next in this series: how I wire Firebase into this layout with flavors and environment-aware initialization.