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:

  1. Success — update the UI with fresh data
  2. Network error — show cached data with a "Offline" indicator, disable trade actions
  3. 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.