DEV Community

zikarelhub
zikarelhub

Posted on

Offline-First Architecture for Nigerian Users — Complete Implementation Guide

Most software assumes reliable internet. Nigerian users have intermittent 3G, budget Android devices and data that costs real money. Here is the complete offline-first implementation for the Nigerian market.

The Problem in Code

// What most apps do — fails Nigerian users
async function loadWalletBalance(userId) {
  const response = await fetch(`/api/wallet/${userId}/balance`);
  // If network is slow → blank screen
  // If network drops → error or infinite spinner
  // If timeout → user sees nothing and leaves
  return response.json();
}

// What Nigerian apps should do
async function loadWalletBalance(userId) {
  // 1. Show cached data immediately — no blank screen
  const cached = await cache.get(`wallet:${userId}:balance`);
  if (cached) updateUI(cached);

  // 2. Refresh from network with retry
  try {
    const fresh = await fetchWithRetry(`/api/wallet/${userId}/balance`);
    await cache.set(`wallet:${userId}:balance`, fresh);
    updateUI(fresh); // Update if different
  } catch {
    // Network failed — cached data already shown, user sees something
    showStaleDataIndicator();
  }
}
Enter fullscreen mode Exit fullscreen mode

1. Exponential Backoff Retry

async function fetchWithRetry(url, options = {}, maxRetries = 4) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(15000) // 15s timeout for 3G
      });
      if (response.ok) return response.json();
      if (response.status < 500) throw new Error(`Client error ${response.status}`);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 30000);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}
// 1s → 2s → 4s → 8s → give up
// Handles Nigerian 3G momentary drops
Enter fullscreen mode Exit fullscreen mode

2. Write Queue — No Lost Transactions

// Queue actions when offline — sync when back online
class SyncQueue {
  async enqueue(action) {
    const db = await openDB('sync-queue', 1);
    await db.add('actions', {
      ...action,
      idempotencyKey: `${action.type}-${Date.now()}`,
      attempts: 0
    });
    showUserMessage('Action queued — will complete when back online');
  }

  async processAll() {
    const db = await openDB('sync-queue', 1);
    const actions = await db.getAll('actions');

    for (const action of actions) {
      try {
        await fetch(action.url, {
          method: action.method,
          headers: {
            'Idempotency-Key': action.idempotencyKey // Server handles duplicates
          },
          body: JSON.stringify(action.payload)
        });
        await db.delete('actions', action.id);
      } catch {
        // Will retry next time
      }
    }
  }
}

const queue = new SyncQueue();
window.addEventListener('online', () => queue.processAll());
Enter fullscreen mode Exit fullscreen mode

3. Explicit Offline State

function useConnectivity() {
  const [state, setState] = useState({
    online: navigator.onLine,
    quality: 'good'
  });

  useEffect(() => {
    const update = () => {
      const conn = navigator.connection;
      setState({
        online: navigator.onLine,
        quality: !navigator.onLine ? 'offline' :
          conn?.effectiveType === '3g' ? 'degraded' :
          conn?.effectiveType === '2g' ? 'poor' : 'good'
      });
    };
    window.addEventListener('online', update);
    window.addEventListener('offline', update);
    return () => {
      window.removeEventListener('online', update);
      window.removeEventListener('offline', update);
    };
  }, []);

  return state;
}

// Every screen shows connectivity state — no silent failures
function ConnectivityBanner() {
  const { quality } = useConnectivity();
  if (quality === 'good') return null;
  return (
    <div style={{ background: quality === 'offline' ? '#dc2626' : '#d97706', color: 'white', padding: '0.5rem', textAlign: 'center' }}>
      {quality === 'offline'
        ? 'Offline — actions will sync when back online'
        : 'Slow connection — some things may take longer'}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Nigerian Device Testing Protocol

// Chrome DevTools settings for Nigerian testing
const nigerianTestConfig = {
  network: 'Slow 3G (500kbps down, 500kbps up, 400ms RTT)',
  device: 'Tecno Spark — 2GB RAM (emulate or use real device)',
  targets: {
    lcp: '< 2.5 seconds',
    totalPageSize: '< 1MB',
    lighthouseMobile: '> 80',
    timeToInteractive: '< 5 seconds on Slow 3G'
  }
};
Enter fullscreen mode Exit fullscreen mode

The Rule

Build for intermittent 3G, 2GB RAM, data-conscious users
Cache first → queue writes → explicit states → retry always → optimize data
Test on real Tecno or Infinix — not your MacBook on office WiFi
Enter fullscreen mode Exit fullscreen mode

ZikarelHub LTD is Nigeria's #1 software and digital agency — software built for Nigerian network reality from day one.

What offline-first techniques have you found most impactful for Nigerian users? 👇

Top comments (0)