Applications used by people who move - field engineers, delivery staff, sales teams, anyone in a warehouse or a basement - meet connectivity that is intermittent rather than absent. Handling that well is the difference between software people rely on and software they work around with paper. It is also considerably harder than it appears, and the difficulty is entirely in writes.
Reading offline is a caching problem with well-known answers: store what the user needs locally, refresh when connected, show clearly how fresh the data is. That last part matters more than it sounds - a user acting on stale information without knowing it is stale is a worse outcome than showing them nothing, particularly for stock levels or prices.
Writing offline is where the design lives. The device must accept the change, queue it, and apply it later - which means every action taken offline is provisional until the server agrees. Queue operations rather than final states: record that the user set the status to complete, not that the record now reads complete, because when it syncs, the server may have received other changes and the difference between those two framings determines whether they can be reconciled.
Conflicts are inevitable and need a policy per data type rather than one global rule. Last-write-wins is simple and silently discards someone's work. Field-level merging works when different people edit different attributes. Some data genuinely needs a human to choose, which means building a conflict resolution interface - unglamorous and the only correct answer for anything consequential. Deciding this per entity, in advance, is what stops it becoming a support problem.
Identifiers are the practical detail that trips implementations. A record created offline needs an identifier before the server has seen it, so the device must generate one - a UUID rather than a server sequence - and the server must accept it. Systems that assign identifiers only on the server end up with a mapping layer between local and remote identity that leaks into every part of the codebase.
Make sync state visible, because the alternative is users who do not trust the application. Show what is pending, what has synced, and what failed, with a way to retry. A user who has spent an hour recording work in a warehouse needs to know it has actually left the device before they walk away - and an app that fails silently, once, teaches them to duplicate everything on paper permanently.