Offline caching is standard advice for mobile apps. But in a trading app, it comes with a specific danger: stale prices can cause real financial harm. If a user places an order based on a price that's 5 minutes old, they might buy at a rate they never intended.
This made the Peanut Trading App's offline strategy fundamentally different from a typical offline-first app. We weren't caching for continuity — we were caching for resilience, with strict rules about what could and couldn't be done without a live connection.
Two Types of Data, Two Strategies
We split the app's data into two categories:
Safe to cache: Order history, account balance, transaction records, commodity metadata (names, units, descriptions). This data changes infrequently and doesn't affect the safety of a trade decision.
Never cache for action: Live prices, current bid/ask spreads, active order status. Showing a user a cached price and letting them trade on it is dangerous. We chose to show stale prices clearly labelled as stale — and disable the Buy/Sell buttons entirely until a live price was confirmed.
The SQLite Layer
All safe data was stored in SQLite on the device. On app launch, we loaded from SQLite immediately — giving users instant access to their history and account info — while simultaneously fetching fresh data in the background.
The pattern is called stale-while-revalidate: show cached data now, update it silently when fresh data arrives. The UI updates smoothly when the network response comes in, without a loading spinner blocking the user.
Every cached record included a cached_at timestamp. Any price data older than 60 seconds was considered stale and displayed with a warning banner.
Error Boundaries: Failing Without Crashing
Network failures in Flutter throw exceptions. Unhandled exceptions crash apps. We wrapped every network call in explicit error handling with three outcomes:
- Success — update the UI with fresh data
- Network error — show cached data with a "Offline" indicator, disable trade actions
- Server error — show an error message, keep cached data visible, offer a retry button
The key principle: never show a blank screen. Users always see something — even if it's clearly marked as cached or outdated. A blank screen feels broken; a stale-but-labelled screen feels honest.
Order Submission Under Poor Connectivity
When a user places an order on a weak connection, the submission might time out before we get a response. Did the order go through? We don't know.
We handled this with idempotency keys: every order submission included a UUID generated on the client. If the same UUID hit the server twice (due to a retry), the second was ignored. The user could safely retry a timed-out submission without risk of a duplicate order.
In trading, a confusing error message is safer than a confident wrong answer. When in doubt, show uncertainty — don't hide it.
The result: zero crashes from network errors across 2,000+ daily transactions. Users on patchy connections could browse their history and account data freely — they just couldn't trade until the connection was confirmed live. That constraint, clearly communicated, is the honest design.