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 waitInstant feedback - smooth UX
User loses typed messageMessage saved immediately
Requires constant internetWorks offline completely
High bounce rateBetter 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

dart
class 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

dart
import '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

dart
class 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

dart
class 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

text
User 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

BenefitDescription
Instant FeedbackUser sees message immediately
Works OfflineFull chat functionality without internet
Auto-SyncNo manual retry needed
No Data LossAll messages saved locally
Better UXSmooth, WhatsApp-like experience
ReliableRetry logic ensures delivery

Edge Cases Handled

  1. App killed before sync: Messages stay in local DB, sync on next app open
  2. Network flaky: Exponential backoff prevents spam
  3. Duplicate sends: Idempotency key prevents duplicates
  4. Message order: Timestamp ensures correct ordering
  5. 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: