Architecture Patterns
Irbisa · cheatsheetSeptember 13, 2026

Architecture Patterns

MVC, MVP, MVVM, Clean Architecture, Repository pattern

Middle Developer20 itemscompressed for a skim
  1. 01

    Explain Clean Architecture in Flutter with layers and responsibilities.

    Medium

    Clean Architecture organizes code in concentric layers governed by one rule: source-code dependencies point inward only, never outward.

    // ── Domain Layer (pure Dart, zero imports) ─────────
    class Product {
      final int id; final String name; final double price;
      const Product({required this.id, required this.name, required this.price});
    }
    …
  2. 02

    How do you structure a large Flutter app by features (feature-first)?

    Hard

    Feature-first (vertical slicing) groups files by FEATURE rather than by layer.

    // ── Feature-first project structure ───────────────
    /*
    lib/
    ├── main.dart
    ├── app.dart                    # MaterialApp, router setup
    ├── core/
    …
  3. 03

    Compare MVC, MVP, MVVM, and MVI patterns and how they map to Flutter.

    Medium

    All four organize the relationship between UI and business logic but differ in who calls whom and how data flows.

    // ── MVP-style — explicit View interface ──
    abstract class LoginView {
      void showLoading();
      void showError(String message);
      void navigateHome();
    }
    …
  4. 04

    What does it mean to apply SOLID principles in Flutter?

    Medium

    In Flutter, Single Responsibility and Dependency Inversion do most of the work; the other three mostly bite at package and interface boundaries.

    // ── Single Responsibility ──
    // ❌ Mixed concerns
    class UserService {
      Future<User> fetchUser(int id) async { /* HTTP */ }
      String formatUserName(User u) => u.name.toUpperCase();    // formatting
      Future<void> saveToFile(User u) async { /* file IO */ }
    …
  5. 05

    How do you split a large Flutter app into modular packages or workspaces?

    Hard

    Once a Flutter codebase passes ~50k lines or 3+ teams, packing everything into one lib/ becomes painful: long compile cycles, accidental cross-feature imports, no enforced boundaries.

    # ── Pub workspaces, Dart 3.6+ (recommended for new monorepos) ──
    # Top-level pubspec.yaml
    name: my_app_workspace
    environment: { sdk: ^3.6.0 }
    workspace:
      - app
    …
  6. 06

    How do you map between domain entities and data DTOs cleanly?

    Medium

    The data layer talks JSON; the domain layer talks Dart.

    // ── Domain — pure ──
    class User {
      final int id;
      final String fullName;
      final String? email;
      final bool active;
    …
  7. 07

    How do you design a typed AppError hierarchy that flows from data layer to UI?

    Hard

    Every layer in a Flutter app can fail differently.

    // ── Domain hierarchy ──
    sealed class AppError implements Exception { final String message; const AppError(this.message); }
    class NetworkError      extends AppError { const NetworkError([super.m = 'No network']); }
    class TimeoutError      extends AppError { const TimeoutError([super.m = 'Timeout']); }
    class ServerError       extends AppError { final int statusCode; const ServerError(this.statusCode, [super.m = 'Server error']); }
    class UnauthorizedError extends AppError { const UnauthorizedError([super.m = 'Sign in required']); }
    …
  8. 08

    How do you set up dependency injection at scale (composition root, scopes, environments)?

    Medium

    Dependency injection at scale is three problems, not one: where the graph is built, how long each object lives, and how it changes per environment.

    // ── Composition root with Riverpod ──
    
    // 1) App scope
    final apiBaseUrlProvider = Provider<String>(
      (_) => const String.fromEnvironment('API_URL', defaultValue: 'https://staging.example.com'),
    );
    …
  9. 09

    How do feature flags and staged rollouts fit into a Flutter app's architecture?

    Medium

    Feature flags decouple deploy from release: ship the code dark, flip a switch later for some or all users.

    // ── Flag client with bundled defaults ──
    class FeatureFlags {
      FeatureFlags._(this._values);
      final Map<String, dynamic> _values;
    
      static late FeatureFlags instance;
    …
  10. 10

    Who decides where the app navigates next — the widget, the state holder, or the router?

    Medium

    Deciding where the user goes next is application logic, while pushing a route is a framework mechanism, and a layered app keeps those two apart.

    // ── 1) Global gates: the router reads state, nobody pushes ──
    final router = GoRouter(
      refreshListenable: sessionNotifier,          // re-evaluates on sign-in/out
      redirect: (context, state) {
        final signedIn = sessionNotifier.isSignedIn;
        final goingToLogin = state.matchedLocation == '/login';
    …
  11. 11

    When is Clean Architecture the wrong choice for a Flutter app, and what do you keep instead?

    Medium

    Layers exist to isolate change, so a layer you cannot name a change for is pure cost — and on a small app that cost is most of the codebase.

    // ── Small app, still testable: two files, one seam ──
    abstract interface class OrdersRepository {
      Future<List<Order>> fetch();
    }
    
    class HttpOrdersRepository implements OrdersRepository {
    …
  12. 12

    You inherit a Flutter app with logic in StatefulWidgets and singletons everywhere. How do you improve it without a rewrite?

    Hard

    Strangle it rather than rewrite it: freeze the old shape, build every new feature in the target shape, and migrate old code only when you have a reason to touch it.

    // STEP 1 — a seam in front of the legacy code, no behaviour change
    abstract interface class ProfileRepository {
      Future<Profile> load(String id);
    }
    
    class LegacyProfileRepository implements ProfileRepository {
    …
  13. 13

    A reviewer rejects your domain entity because it imports package:flutter/material.dart for a Color field — is that pedantic?

    Medium

    No — that single import reverses the dependency arrow, so the layer that is supposed to be the most stable in the app now compiles only where Flutter exists.

    // ❌ domain/order.dart — the arrow now points outward
    import 'package:flutter/material.dart';
    
    class Order {
      const Order({required this.id, required this.statusColor});
      final String id;
    …
  14. 14

    Your UserRepository has fourteen methods that mirror the REST routes one-for-one and returns Response objects — what would you change?

    Medium

    That is the HTTP client wearing a repository's name: a repository is a collection-like interface over a domain concept, so its shape comes from what callers need, not from what the backend happens to expose.

    // ❌ The HTTP client with a new name
    abstract interface class UserRepository {
      Future<Response> getUserV2(int id);
      Future<Response> getUserOrdersPage(int id, int page);
      Future<Response> postUserAvatarUpload(int id, FormData body);
      // ...eleven more, one per route; every caller parses the JSON itself
    …
  15. 15

    Every rule about when an order can be cancelled lives in the Cubit and Order is just a bag of fields — what does that cost you?

    Medium

    That is an anemic domain model: the entity carries data and nothing else, so the rules that define what an order is get copied into every state holder that touches one.

    // ❌ Anemic entity, rules scattered across state holders
    class Order {
      Order(this.status, this.paidAt);
      OrderStatus status;
      DateTime? paidAt;
    }
    …
  16. 16

    QA sends a screenshot with the loading spinner and the error banner on screen at the same time — what is wrong with the state model?

    Medium

    Three independent fields — isLoading, error, items — encode eight combinations while the screen has four legal ones, and the widget is faithfully rendering a state that should never have existed.

    // ❌ Eight combinations, four of them meaningless
    class ArticlesState {
      ArticlesState({this.isLoading = false, this.error, this.items = const []});
      final bool isLoading;
      final AppError? error;
      final List<Article> items;
    …
  17. 17

    You edit your name on the settings screen and the header still shows the old one until restart — which layer do you fix, and how?

    Hard

    Fix it in the repository: two screens each fetched their own copy, so there is no single source of truth — make the repository own the cached entity and hand out a stream that every screen listens to.

    class ProfileRepository {
      ProfileRepository(this._api);
      final ProfileApi _api;
    
      Profile? _current;
      final _changes = StreamController<Profile>.broadcast();
    …
  18. 18

    Signing out must clear the cart, close the chat socket and drop cached images — how do those features find out without importing each other?

    Hard

    Don't let features call each other at all: either one coordinator in the composition root calls a narrow interface that each feature exposes, or auth publishes a domain event and interested features subscribe.

    // Each feature exposes one verb and imports nothing from its siblings.
    abstract interface class SessionScoped {
      Future<void> onSignedOut();
    }
    
    class CartRepository implements SessionScoped {
    …
  19. 19

    Placing an order reserves stock, charges the card and clears the cart — which layer owns that sequence, and what happens when the charge fails?

    Hard

    A use case owns it — the sequence is business logic spanning three repositories, which is exactly what the domain layer exists for — and since there is no transaction across three services, the failure path is compensation plus idempotency, not a rollback.

    sealed class PlaceOrderResult {
      const PlaceOrderResult();
    }
    class OrderPlaced extends PlaceOrderResult {
      const OrderPlaced(this.id);
      final OrderId id;
    …
  20. 20

    Coming from Android you ask where a ViewModel lives in Flutter — yours is rebuilt every frame and loses its state on a tab switch. What is going on?

    Hard

    Flutter has no ViewModel lifecycle owner: the lifetime of a state holder is decided by where the object that creates it sits in the element tree, so "rebuilt every frame" means you constructed it inside build, and "lost on tab switch" means you scoped it under a widget that got unmounted.

    // ❌ A new instance every frame, and nothing is ever disposed
    @override
    Widget build(BuildContext context) {
      return ChangeNotifierProvider.value(   // .value never owns or disposes
        value: CartViewModel(repo),          // constructed inside build
        child: const CartView(),
    …