← back to writing

Clean Architecture in Flutter — A Practical Layering Guide

10 February 20267 minFlutter · Architecture · BLoC

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

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.