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:
/// 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:
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.