<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Odunayo Dada</title>
    <description>The latest articles on DEV Community by Odunayo Dada (@odunayo_dada).</description>
    <link>https://dev.to/odunayo_dada</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2633308%2Fcbdf918b-3871-4a3f-ba3d-c58f4c5c192a.jpg</url>
      <title>DEV Community: Odunayo Dada</title>
      <link>https://dev.to/odunayo_dada</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/odunayo_dada"/>
    <language>en</language>
    <item>
      <title>Protect Your Node.js API: Rate Limiting with Fixed Window, Sliding Window, and Token Bucket</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 10 Sep 2025 09:50:49 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/protect-your-nodejs-api-rate-limiting-with-fixed-window-sliding-window-and-token-bucket-4278</link>
      <guid>https://dev.to/odunayo_dada/protect-your-nodejs-api-rate-limiting-with-fixed-window-sliding-window-and-token-bucket-4278</guid>
      <description>&lt;p&gt;Rate limiting is a strategy for limiting the number of requests a client or user can make to a network, application or API within a specified time (per minute, per second).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is Rate Limiting Important?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Protects Resources from Misuse&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without rate limiting, a single client (or bot) would be able to make thousands of requests within seconds. This can crash your server, increase expense (if you pay per API call or compute time), and reduce performance for every other user. With rate limiting, you block any single client from taking over your system’s resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stops Denial-of-Service (DoS) Attacks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Attackers will normally try to flood servers with traffic in an effort to make the service unavailable. Rate limiting counteracts the impact of such an attack by turning off abusive requests before they consume all of your bandwidth, memory, or CPU. While it’s not a complete solution against volumetric Distributed DoS (DDoS), it’s an essential first line of defense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Enforces Fair Use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In multi-user systems or public APIs, you want everyone to have a fair share of access. Rate limiting ensures that one client doesn’t hog the service at the expense of others. For example, in an API that allows 100 requests per minute, every user gets the same opportunity, preventing abuse and maintaining a consistent experience across the board.&lt;/p&gt;

&lt;p&gt;In this article, we’ll look at three (3) popular approaches for implementing rate limiting.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Fixed window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sliding window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Token bucket / Leaky bucket&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;1. Fixed Window&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Fixed Window strategy counts requests within a strict time window (like seconds, minutes or hour). When the window resets, the count resets too.&lt;/p&gt;

&lt;p&gt;For example, if an API request is limited to 5 requests per minute. The API cannot take more than 5 requests in every minutes and this threshold resets every 1 minute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Request, Response, NextFunction } from 'express';

// Configuration
const WINDOW_SIZE_IN_MS = 60_000; // 1 minute
const MAX_REQUESTS = 5; // per IP per window

// Store: { ip -&amp;gt; { count, windowStart } }
type Entry = { count: number; windowStart: number };
const store = new Map&amp;lt;string, Entry&amp;gt;();

export function fixedWindowLimiter(req: Request, res: Response, next: NextFunction) {
  const ip = req.ip || req.connection.remoteAddress || 'unknown';
  const now = Date.now();

  let entry = store.get(ip);

  if (!entry) {
    // First request for this IP
    store.set(ip, { count: 1, windowStart: now });
    return next();
  }

  // If current window expired → reset counter
  if (now - entry.windowStart &amp;gt;= WINDOW_SIZE_IN_MS) {
    entry = { count: 1, windowStart: now };
    store.set(ip, entry);
    return next();
  }

  // Still inside the window
  entry.count++;

  if (entry.count &amp;gt; MAX_REQUESTS) {
    const retryAfter = Math.ceil((entry.windowStart + WINDOW_SIZE_IN_MS - now) / 1000);

    res.setHeader('Retry-After', retryAfter.toString());
    return res.status(429).json({
      success: false,
      message: `Too many requests. Try again in ${retryAfter}s`
    });
  }

  return next();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Sliding window&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike fixed window rate limiting (which resets counters at regular intervals), the sliding window method continuously evaluates requests based on a moving time window.&lt;/p&gt;

&lt;p&gt;For example, an API with a limit of 100 requests per minute:&lt;/p&gt;

&lt;p&gt;If a user sends 90 requests in the last 50 seconds, they can only send 10 more in the next 10 seconds.&lt;/p&gt;

&lt;p&gt;Every second, the window slides forward, dropping old requests and including new ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis();
const WINDOW_SIZE = 60; // seconds
const MAX_REQUESTS = 100;

export async function slidingWindowLimiter(req: Request, res: Response, next: NextFunction) {
  const key = `sliding:${req.ip}`;
  const now = Date.now();

  const windowStart = now - WINDOW_SIZE * 1000;

  // Remove old requests outside window
  await redis.zremrangebyscore(key, 0, windowStart);

  // Count requests in window
  const count = await redis.zcard(key);

  if (count &amp;gt;= MAX_REQUESTS) {
    res.setHeader('Retry-After', String(WINDOW_SIZE));
    return res.status(429).json({ message: 'Too many requests' });
  }

  // Add current request timestamp
  await redis.zadd(key, now, now.toString());
  await redis.expire(key, WINDOW_SIZE);

  next();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Token bucket / Leaky bucket&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Allows bursts up to a capacity; then refills gradually. It enforces a strict, constant rate of processing, smoothing out traffic.&lt;/p&gt;

&lt;p&gt;Here is how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requests are added to a queue (the bucket).&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The bucket leaks at a fixed rate (requests are processed one at a time at. regular intervals).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the bucket overflows (too many requests), excess requests are dropped.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Request, Response, NextFunction } from 'express';
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';

const redis = new Redis();

const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'bucket',
  points: 150, // bucket capacity
  duration: 60, // refill window (60s → 150 tokens per minute)
  execEvenly: true // smooth out evenly
});

export async function tokenBucketLimiter(req: Request, res: Response, next: NextFunction) {
  try {
    await limiter.consume(req.ip, 1);
    next();
  } catch (rejRes) {
    const retrySecs = Math.ceil(rejRes.msBeforeNext / 1000) || 1;
    res.set('Retry-After', String(retrySecs));
    res.status(429).json({ message: 'Too many requests, retry later' });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate limiting is one of the simplest yet most effective tools for protecting your APIs. Whether you choose fixed window, sliding window or token bucket, the right strategy depends on your app’s traffic patterns and scaling needs.&lt;/p&gt;

&lt;p&gt;Fixed Window :easy to implement, good for small projects.&lt;br&gt;
Sliding Window: fairer distribution, great for APIs with steady load.&lt;br&gt;
Token Bucket / Leaky Bucket: best for production, balances bursts and consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Rate limiting is more than just a performance trick — it’s a safeguard against abuse, downtime, and unfair usage. We’ve explored three powerful strategies: Fixed Window, Sliding Window, and Token/Leaky Bucket.&lt;/p&gt;

&lt;p&gt;But this is just the beginning. In my YouTube video, I’ll show you how to set up real rate limiting in a Node.js app, step by step, with live coding examples and best practices to keep your APIs secure and efficient.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.youtube.com/watch?v=kqVFuG_zbEM" rel="noopener noreferrer"&gt;Watch the full video here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>Offline-First Mobile App Architecture: Syncing, Caching, and Conflict Resolution</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 16 Jul 2025 14:46:39 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-1j58</link>
      <guid>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-1j58</guid>
      <description>&lt;p&gt;In many parts of the world, network connectivity is unreliable. Even in major cities, mobile users frequently lose signal while commuting, entering buildings, or during power outages.&lt;/p&gt;

&lt;p&gt;If your app stops working the moment internet access is lost, you’re building for ideal conditions , not the real world.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" alt="Mobile App with no network" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline-First Design&lt;/strong&gt;&lt;br&gt;
An offline-first application is designed to work seamlessly with or without the internet. It uses local storage as the primary data source and synchronizes changes with a remote server once connectivity is available.&lt;/p&gt;

&lt;p&gt;This approach ensures your users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Continue working uninterrupted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid data loss&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trust your app to be available at all times&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Use Case:&lt;/strong&gt; Field Data Collection in Rural Areas&lt;br&gt;
I will share my approach to building a field data collection app for areas with poor internet. I had to ensure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User input was never lost&lt;/li&gt;
&lt;li&gt;Data could be submitted at any time — whether online or not&lt;/li&gt;
&lt;li&gt;The app could sync data automatically once network resumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how I implemented this using Room, WorkManager, and NetworkCallback in Android.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisting Data Locally with Room&lt;/strong&gt;&lt;br&gt;
Whenever a user captures data (e.g., survey responses or inspection records), it is saved in a local SQLite database using Room.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Entity(tableName = "field_data")
data class FieldDataEntity(
    @PrimaryKey val id: String = UUID.randomUUID().toString(),
    val formName: String,
    val content: String,
    val isSynced: Boolean = false,
    val timestamp: Long = System.currentTimeMillis()
)

@Dao
interface FieldDataDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(fieldData: FieldDataEntity)

    @Query("SELECT * FROM field_data WHERE isSynced = 0")
    suspend fun getUnsyncedData(): List&amp;lt;FieldDataEntity&amp;gt;

    @Query("UPDATE field_data SET isSynced = 1 WHERE id = :id")
    suspend fun markAsSynced(id: String)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Syncing with the Server Using WorkManager:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the device regains network access, a background worker is triggered to sync all unsynced data in the room database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataSyncWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    private val dao = AppDatabase.getInstance(context).fieldDataDao()
    private val api = ApiService.create() 

    override suspend fun doWork(): Result {
        val unsynced = dao.getUnsyncedData()
        for (item in unsynced) {
            try {
                val response = api.uploadFieldData(item)
                if (response.isSuccessful) {
                    dao.markAsSynced(item.id)
                    sendInAppNotification("Data synced: ${item.formName}")
                }
            } catch (e: Exception) {
                // Retry later
            }
        }
        return Result.success()
    }

    private fun sendInAppNotification(message: String) {
        // Optionally notify user
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detecting Network Availability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Used Connectivity Manager to detect when the device regains internet access and enqueue the sync worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun registerNetworkCallback(context: Context) {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val request = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()

    connectivityManager.registerNetworkCallback(request, object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            enqueueSyncWorker(context)
        }
    })
}

fun enqueueSyncWorker(context: Context) {
    val request = OneTimeWorkRequestBuilder&amp;lt;DataSyncWorker&amp;gt;()
        .setConstraints(Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build())
        .build()

    WorkManager.getInstance(context).enqueue(request)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conflict Resolution Strategy&lt;/strong&gt;&lt;br&gt;
In my case, conflicts were minimal because users didn’t edit the same record from multiple devices. However, in more collaborative apps, you can handle conflicts by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last-write-wins:&lt;/strong&gt; simplest, but risky&lt;br&gt;
Merge strategies: combine changes from client + server&lt;br&gt;
User-assisted: notify user to choose the correct version&lt;br&gt;
User Feedback: In-App and Push Notifications&lt;/p&gt;

&lt;p&gt;Once sync completes, we notify the user via:&lt;/p&gt;

&lt;p&gt;In-app snackbar/toast (if app is foregrounded)&lt;br&gt;
Push notification using a local notification&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Lessons:&lt;/strong&gt;&lt;br&gt;
Prioritize local-first design when working in regions with poor internet&lt;br&gt;
Always queue unsynced data instead of blocking the user&lt;br&gt;
Use WorkManager + Room + NetworkCallback for resilient, testable sync logic.&lt;br&gt;
Don’t forget about conflict resolution — design for edge cases&lt;/p&gt;

&lt;p&gt;Have you built offline-first apps before? I’d love to hear how you approached syncing and caching in the comments.&lt;/p&gt;

</description>
      <category>offline</category>
      <category>design</category>
      <category>android</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Offline-First Mobile App Architecture: Syncing, Caching, and Conflict Resolution</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 16 Jul 2025 14:46:39 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-518n</link>
      <guid>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-518n</guid>
      <description>&lt;p&gt;In many parts of the world, network connectivity is unreliable. Even in major cities, mobile users frequently lose signal while commuting, entering buildings, or during power outages.&lt;/p&gt;

&lt;p&gt;If your app stops working the moment internet access is lost, you’re building for ideal conditions , not the real world.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" alt="Mobile App with no network" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline-First Design&lt;/strong&gt;&lt;br&gt;
An offline-first application is designed to work seamlessly with or without the internet. It uses local storage as the primary data source and synchronizes changes with a remote server once connectivity is available.&lt;/p&gt;

&lt;p&gt;This approach ensures your users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Continue working uninterrupted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid data loss&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trust your app to be available at all times&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Use Case:&lt;/strong&gt; Field Data Collection in Rural Areas&lt;br&gt;
I will share my approach to building a field data collection app for areas with poor internet. I had to ensure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User input was never lost&lt;/li&gt;
&lt;li&gt;Data could be submitted at any time — whether online or not&lt;/li&gt;
&lt;li&gt;The app could sync data automatically once network resumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how I implemented this using Room, WorkManager, and NetworkCallback in Android.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisting Data Locally with Room&lt;/strong&gt;&lt;br&gt;
Whenever a user captures data (e.g., survey responses or inspection records), it is saved in a local SQLite database using Room.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Entity(tableName = "field_data")
data class FieldDataEntity(
    @PrimaryKey val id: String = UUID.randomUUID().toString(),
    val formName: String,
    val content: String,
    val isSynced: Boolean = false,
    val timestamp: Long = System.currentTimeMillis()
)

@Dao
interface FieldDataDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(fieldData: FieldDataEntity)

    @Query("SELECT * FROM field_data WHERE isSynced = 0")
    suspend fun getUnsyncedData(): List&amp;lt;FieldDataEntity&amp;gt;

    @Query("UPDATE field_data SET isSynced = 1 WHERE id = :id")
    suspend fun markAsSynced(id: String)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Syncing with the Server Using WorkManager:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the device regains network access, a background worker is triggered to sync all unsynced data in the room database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataSyncWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    private val dao = AppDatabase.getInstance(context).fieldDataDao()
    private val api = ApiService.create() 

    override suspend fun doWork(): Result {
        val unsynced = dao.getUnsyncedData()
        for (item in unsynced) {
            try {
                val response = api.uploadFieldData(item)
                if (response.isSuccessful) {
                    dao.markAsSynced(item.id)
                    sendInAppNotification("Data synced: ${item.formName}")
                }
            } catch (e: Exception) {
                // Retry later
            }
        }
        return Result.success()
    }

    private fun sendInAppNotification(message: String) {
        // Optionally notify user
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detecting Network Availability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Used Connectivity Manager to detect when the device regains internet access and enqueue the sync worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun registerNetworkCallback(context: Context) {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val request = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()

    connectivityManager.registerNetworkCallback(request, object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            enqueueSyncWorker(context)
        }
    })
}

fun enqueueSyncWorker(context: Context) {
    val request = OneTimeWorkRequestBuilder&amp;lt;DataSyncWorker&amp;gt;()
        .setConstraints(Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build())
        .build()

    WorkManager.getInstance(context).enqueue(request)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conflict Resolution Strategy&lt;/strong&gt;&lt;br&gt;
In my case, conflicts were minimal because users didn’t edit the same record from multiple devices. However, in more collaborative apps, you can handle conflicts by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last-write-wins:&lt;/strong&gt; simplest, but risky&lt;br&gt;
Merge strategies: combine changes from client + server&lt;br&gt;
User-assisted: notify user to choose the correct version&lt;br&gt;
User Feedback: In-App and Push Notifications&lt;/p&gt;

&lt;p&gt;Once sync completes, we notify the user via:&lt;/p&gt;

&lt;p&gt;In-app snackbar/toast (if app is foregrounded)&lt;br&gt;
Push notification using a local notification&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Lessons:&lt;/strong&gt;&lt;br&gt;
Prioritize local-first design when working in regions with poor internet&lt;br&gt;
Always queue unsynced data instead of blocking the user&lt;br&gt;
Use WorkManager + Room + NetworkCallback for resilient, testable sync logic.&lt;br&gt;
Don’t forget about conflict resolution — design for edge cases&lt;/p&gt;

&lt;p&gt;Have you built offline-first apps before? I’d love to hear how you approached syncing and caching in the comments.&lt;/p&gt;

</description>
      <category>offline</category>
      <category>design</category>
      <category>android</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
