I will describe a scenario, and you need to explain the complete flow of how you would handle it. For example: Suppose you are building a chat screen in a mobile application. When a user types a message and presses the Send button, the device may have no internet connection or very poor connectivity at that moment. How would you design the system to handle this situation? Would you prevent the user from sending the message until a stable internet connection is available, or would you store the message locally and send it automatically once the network connection is restored? Please explain the complete flow and the approach you would take.
#architecture#offline-first#sync#database#networking
Answer
Overview
This is a classic offline-first architecture problem. The recommended approach is to store messages locally and auto-sync when connectivity is restored, rather than blocking the user.
Recommended Approach: Offline-First with Auto-Sync
Why Not Block the User?
| Blocking Approach ❌ | Offline-First Approach ✅ |
|---|---|
| Poor UX - frustrating wait | Instant feedback - smooth UX |
| User loses typed message | Message saved immediately |
| Requires constant internet | Works offline completely |
| High bounce rate | Better user retention |
Complete System Architecture
text┌─────────────────────────────────────────────────────────────┐ │ Offline-First Chat Architecture │ └─────────────────────────────────────────────────────────────┘ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ │ │ UI Layer │────────►│ State Layer │────────►│ Data Layer │ │ (Widgets) │ │ (Repository) │ │ (Cache) │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Network │ │ Local DB │ │ Service │ │ (SQLite/Hive)│ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Backend │ │ Sync Queue │ │ API │ │ Manager │ └──────────────┘ └──────────────┘
Complete Flow Breakdown
Phase 1: User Sends Message (No Internet)
dart// 1. User types and clicks send onSendPressed(String text) async { // 2. Create message object final message = ChatMessage( id: uuid.v4(), // Generate local ID text: text, senderId: currentUserId, timestamp: DateTime.now(), status: MessageStatus.pending, // ← Local state syncStatus: SyncStatus.notSynced, // ← Sync state ); // 3. Save to local database IMMEDIATELY await localDB.insertMessage(message); // 4. Update UI immediately (optimistic update) chatMessages.add(message); notifyListeners(); // 5. Add to sync queue await syncQueue.add(message); // 6. Try to send to backend final result = await _attemptSync(message); if (result.success) { // ✅ Sent successfully await _updateMessageStatus(message.id, MessageStatus.sent, serverMessageId: result.serverMessageId ); } else { // ❌ Failed - will retry later print('Message queued for retry: ${message.id}'); } }
Phase 2: Store Message Locally
dart// Local database schema class LocalMessageDB { Future<void> insertMessage(ChatMessage message) async { final db = await database; await db.insert( 'messages', { 'id': message.id, 'text': message.text, 'senderId': message.senderId, 'timestamp': message.timestamp.millisecondsSinceEpoch, 'status': message.status.toString(), 'syncStatus': message.syncStatus.toString(), 'serverMessageId': message.serverMessageId, 'retryCount': 0, }, conflictAlgorithm: ConflictAlgorithm.replace, ); } Future<List<ChatMessage>> getUnsynced Messages() async { final db = await database; final maps = await db.query( 'messages', where: 'syncStatus = ?', whereArgs: [SyncStatus.notSynced.toString()], orderBy: 'timestamp ASC', ); return maps.map((map) => ChatMessage.fromMap(map)).toList(); } }
Phase 3: Display Message in UI
dartclass ChatScreen extends StatelessWidget { Widget build(BuildContext context) { return Consumer<ChatViewModel>( builder: (context, viewModel, child) { return ListView.builder( itemCount: viewModel.messages.length, itemBuilder: (context, index) { final message = viewModel.messages[index]; return ChatMessageWidget( message: message, // Show different indicator based on status trailing: _buildStatusIndicator(message), ); }, ); }, ); } Widget _buildStatusIndicator(ChatMessage message) { switch (message.status) { case MessageStatus.pending: return Icon(Icons.schedule, color: Colors.grey); // Clock icon case MessageStatus.sending: return SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ); case MessageStatus.sent: return Icon(Icons.check, color: Colors.grey); // Single check case MessageStatus.delivered: return Icon(Icons.done_all, color: Colors.grey); // Double check case MessageStatus.read: return Icon(Icons.done_all, color: Colors.blue); // Blue double check case MessageStatus.failed: return Icon(Icons.error_outline, color: Colors.red); // Error icon } } }
Phase 4: Network Connectivity Monitoring
dartimport 'package:connectivity_plus/connectivity_plus.dart'; class ConnectivityService { final _connectivity = Connectivity(); Stream<ConnectivityResult> get connectivityStream => _connectivity.onConnectivityChanged; Future<bool> get isConnected async { final result = await _connectivity.checkConnectivity(); return result != ConnectivityResult.none; } } // In your app class ChatViewModel extends ChangeNotifier { late StreamSubscription<ConnectivityResult> _connectivitySubscription; ChatViewModel() { _connectivitySubscription = connectivityService .connectivityStream .listen(_onConnectivityChanged); } void _onConnectivityChanged(ConnectivityResult result) async { if (result != ConnectivityResult.none) { print('✅ Internet connected - starting sync...'); await syncPendingMessages(); } else { print('❌ Internet disconnected - messages will queue'); } } void dispose() { _connectivitySubscription.cancel(); super.dispose(); } }
Phase 5: Auto-Sync When Connection Restored
dartclass MessageSyncService { final LocalMessageDB localDB; final ChatApiService apiService; final ConnectivityService connectivityService; Future<void> syncPendingMessages() async { // Check connectivity first if (!await connectivityService.isConnected) { print('No internet - skipping sync'); return; } // Get all unsynced messages final unsyncedMessages = await localDB.getUnsyncedMessages(); if (unsyncedMessages.isEmpty) { print('No messages to sync'); return; } print('Syncing ${unsyncedMessages.length} messages...'); for (final message in unsyncedMessages) { await _syncSingleMessage(message); } } Future<void> _syncSingleMessage(ChatMessage message) async { try { // Update status to sending await localDB.updateMessageStatus( message.id, MessageStatus.sending, ); // Send to backend final response = await apiService.sendMessage( text: message.text, localMessageId: message.id, timestamp: message.timestamp, ); // Update with server response await localDB.updateMessage( message.id, serverMessageId: response.messageId, status: MessageStatus.sent, syncStatus: SyncStatus.synced, ); print('✅ Synced message: ${message.id}'); } catch (e) { print('❌ Sync failed for ${message.id}: $e'); // Increment retry count final retryCount = message.retryCount + 1; if (retryCount < 3) { // Retry later await localDB.updateMessageRetryCount(message.id, retryCount); } else { // Mark as failed after 3 retries await localDB.updateMessageStatus( message.id, MessageStatus.failed, ); } } } }
Phase 6: Retry Logic with Exponential Backoff
dartclass SyncQueueManager { Timer? _syncTimer; int _retryInterval = 5; // seconds void startPeriodicSync() { _syncTimer?.cancel(); _syncTimer = Timer.periodic( Duration(seconds: _retryInterval), (_) async { if (await connectivityService.isConnected) { await syncService.syncPendingMessages(); // Reset retry interval on success _retryInterval = 5; } else { // Exponential backoff _retryInterval = min(_retryInterval * 2, 300); // Max 5 minutes } }, ); } void stopSync() { _syncTimer?.cancel(); } }
Phase 7: Handle Duplicate Messages
dart// Backend API - Idempotent endpoint class ChatApiService { Future<SendMessageResponse> sendMessage({ required String text, required String localMessageId, required DateTime timestamp, }) async { final response = await http.post( Uri.parse('$baseUrl/messages'), headers: {'Content-Type': 'application/json'}, body: json.encode({ 'text': text, 'localMessageId': localMessageId, // ← Idempotency key 'timestamp': timestamp.toIso8601String(), 'senderId': currentUserId, }), ); // Backend checks if message with localMessageId already exists // If exists, returns existing message instead of creating duplicate return SendMessageResponse.fromJson(json.decode(response.body)); } } // Backend (Node.js example) app.post('/messages', async (req, res) => { const { text, localMessageId, timestamp, senderId } = req.body; // Check if message already exists (idempotency) let message = await Message.findOne({ localMessageId }); if (message) { // Already processed - return existing message return res.json({ messageId: message.id, status: 'already_exists' }); } // Create new message message = await Message.create({ text, localMessageId, timestamp, senderId, }); // Broadcast to other users via WebSocket/MQTT broadcastMessage(message); res.json({ messageId: message.id, status: 'created' }); });
Complete Implementation
dart// Message model enum MessageStatus { pending, sending, sent, delivered, read, failed } enum SyncStatus { notSynced, syncing, synced, failed } class ChatMessage { final String id; // Local ID (UUID) final String text; final String senderId; final DateTime timestamp; final MessageStatus status; final SyncStatus syncStatus; final String? serverMessageId; // Server ID after sync final int retryCount; ChatMessage({ required this.id, required this.text, required this.senderId, required this.timestamp, required this.status, required this.syncStatus, this.serverMessageId, this.retryCount = 0, }); } // Repository pattern class ChatRepository { final LocalMessageDB localDB; final ChatApiService apiService; final ConnectivityService connectivityService; Future<void> sendMessage(String text) async { // 1. Create message final message = ChatMessage( id: uuid.v4(), text: text, senderId: currentUserId, timestamp: DateTime.now(), status: MessageStatus.pending, syncStatus: SyncStatus.notSynced, ); // 2. Save locally await localDB.insertMessage(message); // 3. Try to sync if (await connectivityService.isConnected) { await _syncMessage(message); } } Future<void> _syncMessage(ChatMessage message) async { try { await localDB.updateMessageStatus(message.id, MessageStatus.sending); final response = await apiService.sendMessage( text: message.text, localMessageId: message.id, timestamp: message.timestamp, ); await localDB.updateMessage( message.id, serverMessageId: response.messageId, status: MessageStatus.sent, syncStatus: SyncStatus.synced, ); } catch (e) { await localDB.updateMessageStatus(message.id, MessageStatus.pending); } } Stream<List<ChatMessage>> watchMessages() { return localDB.watchMessages(); } }
User Experience Flow
textUser Action Status Indicator Backend ─────────────────────────────────────────────────────────────── User types message [ ] User clicks Send [⏱ Pending ] Message saved locally [⏱ Pending ] UI updated instantly Message appears Network check... [↻ Sending... ] IF ONLINE: → Send to backend [↻ Sending... ] ──► Received ← Response received [✓ Sent ] ◄── ACK IF OFFLINE: → Queue for retry [⏱ Queued ] → User continues typing [⏱ Queued ] WHEN ONLINE AGAIN: → Auto-sync starts [↻ Sending... ] ──► Received ← Success [✓ Sent ] ◄── ACK ← Delivery confirmation [✓✓ Delivered ] ◄── Delivered
Benefits of This Approach
| Benefit | Description |
|---|---|
| Instant Feedback | User sees message immediately |
| Works Offline | Full chat functionality without internet |
| Auto-Sync | No manual retry needed |
| No Data Loss | All messages saved locally |
| Better UX | Smooth, WhatsApp-like experience |
| Reliable | Retry logic ensures delivery |
Edge Cases Handled
- App killed before sync: Messages stay in local DB, sync on next app open
- Network flaky: Exponential backoff prevents spam
- Duplicate sends: Idempotency key prevents duplicates
- Message order: Timestamp ensures correct ordering
- Failed messages: User can manually retry or delete
Key Principle: Offline-first architecture provides the best user experience. Save locally, show immediately, sync in background.
Resources: