#16 — Clean Architecture in Flutter, One Level Deeper
- 01 / The Shift: Justification over Definition
- 02 / The BLoC Trap: Why Use Cases Matter
- 03 / Dependency Inversion at the Module Level
- 04 / Real-World Interface: The KYC Example
- 05 / The Rehearsed Line: How to Pitch It
- 06 / When Clean Architecture is Overkill
- 07 / Translate Your Experience (Worksheet)
If you're interviewing for an intermediate or senior Flutter role at a major product company—think CRED, Swiggy, or Razorpay—the system design round is going to touch on architecture. But here's the trap: 90% of candidates answer by drawing three boxes on a whiteboard and reciting the definitions of Presentation, Domain, and Data.
That answer scores you exactly zero points. The interviewer assumes you've read Uncle Bob's book or watched a ResoCoder tutorial. At the senior level, the question isn't "do you know the pattern?" The real question is: "Can you build a structure that 10 other engineers can work on without burning it down?"
01 / The Shift: Justification over Definition
To pass this round, you must shift your framing entirely around justification and consequences. You need to be able to justify EVERY layer boundary you drew, and explicitly name what breaks in the codebase when an engineer on your team violates it.
Let's visualize the standard flow. Hover over the layers below to see how a senior dev explains the dependency rule.
(UI, BLoCs, Cubits)
(Entities, Use Cases, Repository Interfaces)
(Repositories, APIs, Local DBs)
02 / The BLoC Trap: Why Use Cases Matter
Let's do the lightning-fast recap: Presentation handles the UI and state management. Domain holds the pure business logic and interfaces. Data implements those interfaces and talks to the outside world.
Now, let's look at the actual interview probe: "Why do we need Use Cases? Why not just put the business logic inside your BLoC or ViewModel?"
If you answer, "Because it separates concerns," you sound like a textbook. Here is the concrete, senior answer: "If I embed business logic directly in a BLoC, two things break immediately."
- Testing becomes incredibly heavy: To test a simple business rule (e.g., "User cannot transfer funds if KYC is pending"), I now have to spin up
bloc_test, mock streams, and deal with emit states. If it lives in a Use Case, it's a pure Dart function. I write a standardsetUp()and test the logic in milliseconds. - Silent UI duplication: Suppose tomorrow we need a Flutter Web admin panel that allows an agent to force-trigger the same fund transfer. If the logic is in the Mobile UI's
TransferBloc, the web team has to either import a mobile-specific UI class, or worse, silently duplicate the business rule into anAdminTransferBloc. When the logic inevitably updates, they diverge.
Use Cases exist to protect business rules from the framework orchestrating the UI.
03 / Dependency Inversion at the Module Level
This is the concept that separates candidates who've casually read about Clean Architecture from those who've actually enforced it in a multi-team codebase.
Most developers understand class-level dependency inversion: a Use Case depends on an abstract IUserRepository, not a concrete UserRepositoryImpl. That's great. But if both files live in the same Flutter project, and your Domain layer file has an import '../../data/repositories/user_repo_impl.dart'; somewhere at the top, you haven't inverted anything. You've just created extra boilerplate.
Real inversion happens at the module/package level. The domain package must have ZERO physical dependency on the data package in its pubspec.yaml.
The code uses interfaces, but the pubspec.yaml still imports the data package. A lazy developer can bypass the interface and instantiate the Data model directly in the Domain layer. The compiler won't stop them.
The app is split into packages (e.g., via Melos). domain_pkg has absolutely no dependency on data_pkg in its pubspec. If an engineer tries to import a Data model into Domain, the CI pipeline fails the build instantly.
Pro Tip for the Interview: Mention that you enforce this using static analysis. Tools like import_linter or defining strict package boundaries via Melos ensure that if the domain imports data, CI fails the PR before it ever reaches code review.
04 / Real-World Interface: The KYC Example
Let's make this concrete with a fintech-adjacent flow: a KYC (Know Your Customer) verification process. The Domain layer defines what needs to happen. It does not care how it happens.
Notice how the Domain code below knows nothing about REST APIs, JSON parsing, or local caching. If the company switches from a third-party KYC provider to an on-device ML model next year, the Domain layer doesn't change a single byte.
// --- DOMAIN LAYER (Zero dependencies on external packages) ---
import 'package:core_domain/entities/kyc_status.dart';
import 'package:core_domain/entities/document_data.dart';
abstract class KycRepository {
Future<KycStatus> verifyDocument(DocumentData document);
Stream<KycStatus> watchVerificationStatus(String userId);
}
// --- DATA LAYER (Depends on Domain, implements the interface) ---
import 'package:core_domain/repositories/kyc_repository.dart';
import 'package:http/http.dart' as http;
class KycRemoteRepository implements KycRepository {
final http.Client _client;
KycRemoteRepository(this._client);
@override
Future<KycStatus> verifyDocument(DocumentData document) async {
// Converts pure domain entity to DTO, makes API call,
// parses JSON response, returns pure domain entity.
final payload = DocumentDto.fromDomain(document).toJson();
final response = await _client.post(Uri.parse('/api/v1/kyc'), body: payload);
return KycStatusDto.fromJson(response.body).toDomain();
}
// ... watchVerificationStatus implementation
}
05 / The Rehearsed Line: How to Pitch It
When asked about the benefits of your architectural choices, don't just say "it's cleaner." Use a verifiably real-world payoff. This is the exact rehearsed line we recommend in the roadmap:
"We aggressively applied Clean Architecture and module-level dependency inversion so our core fintech logic could be completely reused across our mobile app and our internal web admin panel without touching UI code. Because our Domain layer was completely isolated in its own package, the web team just imported it, wrote their own Web BLoCs and UI, and instantly inherited all our complex validation rules and caching logic."
Why this lands well: It shows you understand that architecture is a tool to solve business problems (code reuse across teams), not just a fun academic exercise for developers.
06 / When Clean Architecture is Overkill
A true senior developer knows when a pattern is a bad idea. Blind advocacy for Clean Architecture on every single project is a junior-level tell. Every architecture has a trade-off, and Clean Architecture's trade-off is friction and boilerplate.
If you are building a short-lived MVP screen, a rapid prototype, or an internal data-entry tool with purely CRUD operations (Create, Read, Update, Delete), Clean Architecture is massive overkill. If the app is just reading JSON and throwing it into a ListView without any complex client-side transformations, forcing Use Cases and Entity mappers into the flow adds development time while solving a problem you don't actually have. The senior move is recognizing that upfront and opting for a simpler Feature-First or MVC structure.
07 / Translate Your Experience (Worksheet)
Don't borrow my KYC example for your interview. Use your own. Read the prompts on these cards and mentally map them to the production Flutter app you work on right now. Deepen the stories you already have.
Before moving to the next post, answer this unscripted: "If a Use Case just forwards data from a Repository to a BLoC without changing it, should you delete the Use Case to save boilerplate?" (Hint: The answer involves consistency and anticipating future requirement changes).
Comments
Post a Comment