Most mobile applications assume that the user has an internet connection.
That's reasonable for many consumer apps.
But what happens when you're building software for field employees who spend most of their day moving between shops, warehouses, roads, and areas where the network isn't reliable?
You can't simply show:
"No internet connection."
and expect the business workflow to stop.
I've been working on PAFood Booking System 2.0, an FMCG sales and order management system used around real field operations.
One of the interesting technical challenges has been making the mobile application work reliably when the network isn't available.
This is what led me toward an offline-first approach.
What does offline-first actually mean?
Offline-first doesn't simply mean adding a loading screen when the internet disappears.
The idea is that the application should continue performing important operations locally and synchronize with the backend when connectivity becomes available again.
For a field order booking application, that can mean:
- Viewing previously synchronized shop data
- Creating orders without internet
- Recording visits
- Saving payments
- Storing GPS-related activity
- Queueing changes for synchronization
- Automatically syncing when connectivity returns
The user shouldn't have to think about whether the API is currently reachable.
The application should handle that complexity.
The basic architecture
For the mobile application, the general flow looks like:
User Action
↓
React Native Application
↓
Local Storage
↓
Sync Queue
↓
Network Available?
↓
Backend API
↓
PostgreSQL
Instead of every action immediately depending on the backend, important operations can first be persisted locally.
The synchronization layer then takes responsibility for sending those changes to the server.
This creates a different way of thinking about mobile application architecture.
Why local persistence matters
Suppose a booker visits a shop and creates an order.
The user enters:
- Shop
- Products
- Quantities
- Prices
- Discounts
- Order information
If the application immediately sends everything to the server and the connection fails, the user could lose the order.
That's unacceptable for a field sales system.
Instead, the application can persist the order locally.
The UI can then treat the order as successfully recorded locally while marking it as waiting for synchronization.
For example:
Order
Status: PENDING_SYNC
Sync ID: abc-123
Created locally: true
Once connectivity returns, the sync manager can process the pending operation.
The duplicate request problem
One of the biggest issues with offline synchronization is not simply sending data.
It's knowing what happens when the same operation is sent more than once.
Imagine this:
- User creates an order.
- App stores it locally.
- Internet becomes available.
- App sends the order.
- Server processes it.
- Network response is lost.
- App doesn't know whether the server received it.
- App retries the request.
Now the backend might receive the same order twice.
For a sales system, that's a serious problem.
You don't want:
1 customer order → 2 database orders
Using an idempotency key
One approach I use for this kind of problem is generating a unique client-side request identifier.
For example:
client_request_id = "8f2c7e..."
The mobile application generates this identifier when creating the operation.
The backend stores it along with the resulting record.
If the same request arrives again, the backend can recognize that it has already been processed instead of creating another record.
Conceptually:
Mobile App
Create Order
↓
Generate client_request_id
↓
Save locally
↓
Send to API
↓
Backend checks client_request_id
↓
Already processed?
/ \
Yes No
↓ ↓
Return Process
existing order
This small design decision can make a significant difference in the reliability of an offline system.
Sync isn't just "send everything"
Another lesson is that synchronization needs its own logic.
You need to think about:
- What gets synchronized?
- In what order?
- What happens when synchronization fails?
- Should failed operations retry automatically?
- How many times should they retry?
- What happens if the server rejects the operation?
- What happens if the local data is outdated?
- What happens if two users modify related data?
A simple if (online) sendRequest() approach isn't enough for a serious offline workflow.
The synchronization layer becomes a small system of its own.
Handling connectivity changes
Network availability can change constantly on mobile devices.
A user might have:
Online
↓
Offline
↓
Online
↓
Weak connection
↓
Offline
The application shouldn't require the user to manually manage these states.
Instead, connectivity changes can trigger synchronization attempts.
But even then, the application needs to assume that a connection can disappear during synchronization.
For example:
Upload order
↓
Connection lost
↓
Request status unknown
↓
Retry safely
↓
Backend checks client_request_id
This is another reason why idempotency is so important.
Offline-first changes the way you design the UI
There's also a UX implication.
If an order is saved locally but hasn't reached the server yet, the user should be able to understand its state.
For example:
Order #1042
✓ Saved
↻ Syncing
or:
Order #1042
⚠ Sync failed
Tap to retry
The UI doesn't necessarily need to expose all the technical details.
But the user needs enough information to know whether their work is safe.
For field employees, confidence matters.
They need to know:
"Did my order get saved?"
not:
"Did the HTTP request return a 200?"
Low-end devices make the problem more interesting
Another consideration with field applications is hardware.
Not every employee is using a high-end phone with plenty of RAM and storage.
The application needs to remain usable on lower-end Android devices.
That affects decisions around:
- Memory usage
- Large lists
- Image handling
- Rendering
- Local storage
- Background tasks
- Network requests
- Navigation
- Battery consumption
An application that works perfectly on a developer's machine isn't necessarily ready for real-world field usage.
The environment where the software runs matters.
Offline-first isn't free
There is a trade-off.
Offline-first architecture introduces additional complexity.
You now have:
Local state + Server state + Synchronization state
Instead of simply:
Server state
You need to handle conflicts, retries, failures, stale data, duplicate requests, and synchronization status.
So I wouldn't automatically make every application offline-first.
It makes sense when the business workflow genuinely requires users to continue working without reliable connectivity.
For field sales, delivery, logistics, inspections, and similar applications, that requirement can be fundamental.
What I learned from building it
The biggest lesson for me is that reliable mobile software isn't just about making the API calls work.
You have to think about the environment in which the software will actually be used.
Users lose internet.
Phones run out of battery.
Devices have limited resources.
Requests fail.
Responses disappear.
People tap buttons twice.
And real users don't always follow the workflow exactly as you imagined it.
Good software needs to account for those situations.
Final thoughts
Building an offline-first application changed how I think about mobile development.
The backend being available isn't something I can simply assume.
The network is unreliable.
The device is part of the system.
And synchronization is a core feature, not an afterthought.
For the PAFood Booking System, this approach helps field employees continue their work even when connectivity isn't reliable, while giving the backend mechanisms to safely synchronize those operations later.
The interesting part of software engineering is often not getting the happy path to work.
It's figuring out what happens when everything doesn't go according to plan.
Top comments (0)