“Make it work offline” often turns into a storage task: serialize the last successful response, read it when the network fails, and move on.
That is necessary, but it is not the hard part.
The hard part is deciding what the stored response means across launch, failure, reconnect, configuration change, and session recovery. Once a mobile application depends on runtime configuration, offline readiness becomes a state-machine contract. A cache is only one implementation detail inside that contract.
Find the contradictory assumptions first
Consider a .NET MAUI wrapper hosting a Blazor UI. A lower startup layer may correctly treat a reachability probe as diagnostic: the network can be absent, so failure should not prevent launch. But a higher layer may immediately call a runtime-configuration endpoint and require a successful answer before activating the application scope.
Each layer looks reasonable in isolation. Together they disagree.
The lower layer says, “reachability is not a launch gate.” The higher layer says, “no live response, no ready state.” A short outage is then presented as invalid configuration even though nothing has been invalidated.
This is why I like to draw the complete launch state machine before changing storage. List the states—uninitialised, resolving, ready from live data, ready from fallback data, unavailable—and name the events that move between them. Contradictions become much easier to see when they are transitions rather than scattered if statements.
Classify failure before consulting the cache
Not every failed request grants permission to use stale data.
A timeout, lost connection, or temporary server error says, “the current answer is unavailable.” A recent cached configuration may be the best safe answer.
An authoritative not-found or gone response says something different: “this configuration no longer exists here.” Serving the cache would override current server knowledge with old local knowledge. That is not resilience; it is refusal to accept invalidation.
So the order matters:
- Attempt the live load.
- Classify the failure.
- Consider fallback only for the transient class.
- Clear the fallback when an authoritative response makes it wrong.
This distinction is reusable well beyond configuration. Offline permissions, feature manifests, routing metadata, and other control-plane data all need an explicit invalidation rule.
Make “recent enough” a real policy
Even a transient failure does not make every cached value safe.
A useful policy checks at least four things. The entry must belong to the same logical scope. It must come from the same configured source. Its age must be within a deliberate bound. Its timestamp must not be implausibly far in the future.
Source binding is easy to miss. Runtime configuration often contains addresses and capability switches. If an application is repointed but continues serving a cache captured from the old source, it may look healthy while sending work to the wrong place. Treating a source change as a cache miss is safer and easier to reason about.
The age bound is a product and operational decision, not a magic constant. It should cover the outages the application is expected to tolerate while still placing a limit on how long retired configuration can survive. Document the reasoning so the next engineer knows whether changing it is a reliability decision or a cosmetic edit.
Clock skew deserves a rule too. A future-dated entry can otherwise remain “fresh” for far longer than intended.
Preserve continuity when connectivity returns
An application that opens from fallback data is usable, but not settled. It should carry an explicit “needs refresh” hint.
When connectivity returns, the tempting implementation is to rerun the entire startup pipeline. That can be surprisingly destructive. Full reinitialisation may advance a session epoch, cancel in-flight requests, clear actor state, restart background rails, and remount the renderer. On a flapping connection, users can pay that cost repeatedly.
Prefer an in-place refresh while the current scope remains valid. Update the runtime configuration, clear the refresh hint, and let the active session continue. Escalate to full resolution only if the refresh proves that the scope disappeared or changed in a way that makes continuity unsafe.
This gives reconnect two distinct transitions:
- unavailable to online: resolve fully because there is no active scope to preserve;
- ready-from-fallback to online: refresh in place because there is useful state to protect.
Those transitions may share a network event, but they should not share an indiscriminate reset.
Let storage fail softly—and test the wiring
Device storage is another dependency, not a certainty. Reads can fail, writes can fail, and cleanup can fail. If a cache exists to make cold start more resilient, an escaping storage exception must not become a new cold-start crash.
Fail-soft behaviour should be intentional: a failed read behaves like a miss, a failed write loses future offline convenience, and a failed clear remains bounded by the expiry policy. Cancellation is different and should still propagate.
Test the serializer separately with round trips. A cache that writes successfully but cannot read its own format is indistinguishable from having no fallback at all.
Also test dependency-injection resolution through the real container. Optional constructor dependencies are convenient, but they can compile and quietly fall back to a null implementation when registration is missing. A focused integration test proves that the platform adapter actually reaches the service.
The trade-off is explicit complexity
This design is more complex than “try the network, then read a file.” It introduces failure classification, cache provenance, expiry, clock rules, lifecycle state, reconnect transitions, serialization, and a wider test matrix.
That complexity is buying something concrete: continuity without pretending stale configuration is always correct.
The practical review question is not, “Do we have a cache?” Ask instead:
Which state are we in, what evidence permits this fallback, and what is the least disruptive path back to live truth?
If the implementation can answer that clearly, offline readiness has become a deliberate contract rather than an accidental side effect.
Top comments (0)