Skip to main content
Enterprise Architecture 20 min read • Tested on Flutter 3.24.3

Flutter Clean Architecture in Production: Domain Entities, Offline Repository & Safe Sync

By Chirag Shyani, Principal Flutter Engineer • Updated October 15, 2026

Non-Negotiable Production Guarantees

  • ✓ Pure Domain Independence: Business logic, price calculations, and inventory clamping must remain pure Dart classes with zero imports from package:flutter.
  • ✓ Offline-First Persistence: Local storage (SQLite or IndexedDB) serves as the primary source of truth for UI state, persisting changes before remote HTTP requests are initiated.
  • ✓ Integer Minor Currency Units: Never use double for prices; always represent money in minor currency units (cents) to avoid rounding bugs.

1. The Three-Tier Clean Topology in Flutter

Production applications organize code into three concentric rings, where dependencies point strictly inward toward the domain layer:

┌────────────────────────────────────────────────────────┐
│               PRESENTATION LAYER (UI)                 │
│  Widgets, Pages, BLoC/Notifier, Reactive State        │
└──────────────────────────┬─────────────────────────────┘
                           │ calls use cases
┌──────────────────────────▼─────────────────────────────┐
│                 DOMAIN LAYER (PURE DART)               │
│  Entities, Value Objects, Use Cases, Repository API   │
└──────────────────────────▲─────────────────────────────┘
                           │ implements interface
┌──────────────────────────┴─────────────────────────────┐
│                   DATA LAYER (I/O)                    │
│  SQLite/IndexedDB Local, REST/GraphQL Remote, Models  │
└────────────────────────────────────────────────────────┘

2. Domain Layer: Immutable Entities & Integer Money

Let's examine a pure Dart Value Object that prevents monetary calculation defects across checkout flows:

domain/entities/money.dart — Pure Dart Value Object
/// Exact integer minor units (e.g. cents) representation.
/// Eliminates IEEE-754 floating-point inaccuracies.
class Money {
  final int minorUnits; // 1999 = $19.99
  final String currencyCode;

  const Money(this.minorUnits, {this.currencyCode = 'USD'});

  Money operator +(Money other) {
    assert(currencyCode == other.currencyCode, 'Currency mismatch');
    return Money(minorUnits + other.minorUnits, currencyCode: currencyCode);
  }

  Money operator *(int factor) {
    return Money(minorUnits * factor, currencyCode: currencyCode);
  }

  String get formatted {
    final major = (minorUnits / 100).toStringAsFixed(2);
    return '$$major';
  }
}

3. The Offline-First Repository Pattern

The repository encapsulates caching logic. It queries local cache immediately to deliver sub-16ms cold start times, while simultaneously fetching fresh network payloads in the background:

data/repositories/cart_repository_impl.dart
class CartRepositoryImpl implements CartRepository {
  final LocalStorageDataSource localSource;
  final RemoteApiDataSource remoteApi;

  CartRepositoryImpl({required this.localSource, required this.remoteApi});

  @override
  Stream<Cart> watchCart() {
    // 1. Emit instant local cache first
    return localSource.watchCart().asyncMap((cachedCart) async {
      // 2. Opportunistically sync with remote if connected
      try {
        final remoteCart = await remoteApi.fetchLatestCart();
        await localSource.saveCart(remoteCart);
        return remoteCart;
      } catch (_) {
        // Fallback to local cache if offline
        return cachedCart;
      }
    });
  }

  @override
  Future<void> updateItemQuantity(String itemId, int quantity) async {
    // 1. Optimistic local write
    await localSource.updateQuantity(itemId, quantity);
    
    // 2. Dispatch to network
    try {
      await remoteApi.syncQuantity(itemId, quantity);
    } catch (e) {
      // Mark as pending outbox sync for next network reconnection
      await localSource.enqueuePendingSync(itemId, quantity);
    }
  }
}

Frequently Asked Architecture Questions

Why should Flutter applications use Clean Architecture?

Clean Architecture strictly separates business logic from UI widgets and external dependencies. This ensures your domain rules (such as pricing formulas and validation) remain pure Dart classes with zero Flutter framework dependencies, making them instantly testable and resilient to future UI redesigns or backend migrations.

What is the role of the Repository Pattern in Flutter?

The Repository pattern provides an abstract interface between the domain layer and underlying data sources. It coordinates local persistent storage (SQLite or IndexedDB) with remote network APIs, enabling offline-first caching, optimistic UI updates, and conflict resolution without the UI knowing where the data originates.

Why should money amounts always be stored in minor currency units in Flutter?

Using standard double floating-point numbers for currency leads to cumulative rounding errors due to IEEE-754 binary floating-point representation (e.g. 0.1 + 0.2 equals 0.30000000000000004). Storing currency as integers in minor units (e.g. 2999 cents instead of $29.99) guarantees mathematical precision across tax calculations, totals, and payment gateways.

How do you handle offline sync conflicts in Flutter mobile apps?

A resilient offline sync engine uses optimistic mutations with sequence tokens. When re-establishing connectivity, the app fetches the latest server manifest, clamps requested quantities to verified inventory, resolves price changes, and applies atomic commits to local storage with user notifications for any changes.