<FlutterSolution/> flutter · dart · clean architecture

Flutter Interview Prep #16 — Clean Architecture in Flutter, One Level Deeper

#16 — Clean Architecture in Flutter, One Level Deeper

Stop explaining what the layers are. Start explaining what breaks when you violate their boundaries.

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.

Presentation Layer
(UI, BLoCs, Cubits)
Domain Layer
(Entities, Use Cases, Repository Interfaces)
Data Layer
(Repositories, APIs, Local DBs)
Hover over the Domain or Data layers to inspect dependency rules.

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."

  1. 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 standard setUp() and test the logic in milliseconds.
  2. 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 an AdminTransferBloc. 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.

⚠️ Class-Level Only (Junior)

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.

✅ Module-Level (Senior)

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.

lib/domain/repositories/kyc_repository.dart
// --- 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:

🎙️ Senior-Level Sample Answer

"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.

😌 Honest Take: When to Skip It

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.

1
Name one specific screen in your current app where business logic currently "leaks" into the BLoC or UI. How hard would it be to unit test that logic right now?
2
If your backend team replaced REST API endpoints with a GraphQL or gRPC service tomorrow, how many different UI files would you have to modify?
3
Look at your pubspec.yaml. Can your Domain/Core models access third-party UI packages or HTTP libraries? How would you configure a linter to stop that?
✅ Quick Self-Check

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).

Official Resources

Nachiketa Pandey
// written by

Nachiketa Pandey

Senior Flutter Developer · Cross-Platform & Fintech Applications

Building production Flutter apps at Kotak Neo — clean architecture, BLoC, agentic AI integrations, and fintech-grade security. Sharing everything I learn, one post at a time.

Flutter & Dart Clean Architecture BLoC Firebase FinTech
View Full Portfolio

Comments

Nachiketa
Meet Author// tap to open