Skip to content

Worked example: Expensify

This is the second real teardown, run the same way as the Bluesky example: probes on an emulator, outcomes recorded in the worksheet, and every conclusion then checked against source code the app’s maker publishes.

Expensify was chosen because it is the opposite of Bluesky in the way that matters most to this handbook. Bluesky keeps its data in memory. Expensify is built to work offline. If the probes can tell the two apart, they are measuring something real.

They did tell them apart. They also produced three wrong readings, which are listed in full here and have been corrected in the chapters.

AppExpensify for Android, version 9.4.99-2
DeviceAndroid 15 emulator
Date4 October 2026
ModePrivate test items only. Two test messages in the account’s own self-chat, deleted afterwards. Nothing sent to another person.
Probes run24, from 9 chapters
Source checkedExpensify/App, commit 473e5b0 of 2 October 2026 (version 9.5.1-9, newer than the app tested)
Verdict after checking the sourceConclusions
Confirmed The code does what the probe concluded14
Consistent The code agrees, but the full path was not traced6
Not in the client A measurement, not a design decision in the code1
Contradicted The reading was wrong3
24 probe outcomes and 26 worksheet fields.

The same probes, two apps, opposite outcomes. Both sets of outcomes were confirmed by source.

ProbeBlueskyExpensify
P7.3 Launch in airplane mode after a force-quitLayout and placeholders, no content, no messageThe full Home screen, with a line saying the app appears to be offline
P11.1 Memory or diskContent gone after a force-quitContent present after a force-quit
P12.1 Offline readVisited screens only, while the app is aliveScreens never opened in that run also show their content
P12.2 Offline writeNot run, read-onlyAccepted at once and drawn dimmed
P4.1 Where storage growsThe cache area, by 19 MBUser data, by 2.5 MB. The cache area shrank
P32.2 System text sizeText growsText does not grow
P8.7 Link into a running appOpens over the current place, back returns thereReplaces the current place, back goes to the link’s parent
Offline level, from Chapter 12Level 1 within one run, Level 0 across launchesLevel 3, a local replica with an outbox
A green screen with the app's logo in the center
The splash stays until the complete Home screen is ready, online and offline (P7.2).
A chat showing one normal message, one message struck through, and a line saying You appear to be offline
Offline: a test message deleted while offline is struck through until the network returns. The line under the composer reports the offline state.

Only screenshots with no personal content are published. The other evidence is recorded as text.

This is the chapter Expensify exercises best, and the probes ran in sequence on one test message.

ProbeWhat was seenReadingConfidence
P12.1 Offline readAfter an offline cold start, the Inbox listed its chats with previews, and a chat opened with its history from 2025. Neither had been opened in that runA local replica, filled ahead of needInferred
P12.2 Offline writeA test message sent offline appeared at once, drawn dimmedAn optimistic update with a tracked mutationObserved
P12.3 Kill while pendingForce-quit and offline relaunch. The message was still there, still dimmed, and was the preview on the chat’s Inbox rowThe outbox and the local change are stored durablyInferred
P12.4 ReconnectWith the chat closed and the app on its Home tab, the network came back for 25 seconds. Reopened offline, the message was at full brightness and its time had moved from 9:41 to 9:42 PMA sync engine at app level drains the outboxInferred

Two further things were seen that match signals in the chapter’s table, though no probe asks for them:

  • The time shift. The message’s time changed by a minute once it was confirmed. That is the “item order or timestamp shifts slightly after sending” signal: confirmed backend values replacing local ones.
  • The pending delete. A second test message deleted offline was struck through and dimmed, and vanished when the network returned. Pending adds and pending deletes are drawn differently.
ProbeWhat was seenReadingConfidence
P7.1 Cold against warmA splash for about 12 seconds on this emulator. A return to the running app took 1.7 secondsA real cold start with visible workObserved
P7.2 First-screen strategyThe splash stayed until the complete Home screen appeared. No placeholder frameA blocking fetch behind a splashInferred
P7.3 Launch in airplane modeThe same splash, then the full Home screen with content and an offline lineStored content. The session check is localInferred
P11.1 Memory or diskContent present offline before and after a force-quitMemory and diskInferred
P11.6 Unvisited detailOffline, a chat opened with far more than its one-line previewThe data was on the device before the screen was openedInferred
P9.1 Change and go backA sent message was the Inbox row’s preview at once, online and offlineOne copy of the data serves both screensInferred
P4.1 FootprintUser data grew from 3.7 MB to 6.2 MB. The cache figure fell from 1.4 MB to 0.9 MBStored data that grows with use, kept as user data and not as cacheInferred
P13.2 RecoveryOn each return of the network, untouched, the offline line vanished, pending items were sent and missing history filled inThe app watches the network and resumes by itselfInferred

P4.2 (launch in airplane mode) and P3.1 (offline tour) were also recorded. They restate P7.3 and P12.2.

ProbeWhat was seenReadingConfidence
P8.2 Half a formAn unsent draft survived an app switch and a force-quitThe draft is stored as data, apart from the screenInferred
P8.4 Force-quit and reopenHome tab, with the draft back in the composer when the chat was openedThe draft survives. The screen does notInferred
P7.4 Where it landsHome after a force-quit from a chat, and after a process death from a chat. Repeated with longer waits, same resultThe app does not persist the last screenInferred
P8.3 Process death mid-taskWith a chat open, the process was ended. The app cold-started on HomeMemory only, or a time limitSpeculative
P8.5 Link while signed inA link to a settings page cold-started the app on that pageA router maps the link to a screenObserved
P8.6 Back after a linkBack led to the Account tab, the page’s parentA synthesized back stackInferred
P8.7 Link into a running appFrom the Inbox, the link opened the page. Back led to the Account tab, not the InboxThe link replaces the current placeInferred

P4.3 (force-quit and reopen) restates P8.4.

ProbeWhat was seenReadingConfidence
P32.1 Long-press textA long press on a message opened the app’s own action list, with Copy message. No selection handlesText is not selectable thereObserved
P32.2 Raise the system text sizeFrom 1.0 to 1.5, labels on the Account tab kept exactly the same pixel bounds. Each change also sent the app back to its Home tabNo screen growsObserved
ConclusionVerdictWhat the source shows
Offline writes are optimistic and drawn dimmed (P12.2, P4.2, P3.1)ConfirmedThe maker’s own offline design notes describe queuing a request and proceeding “as if the request succeeded”. A wrapper component dims its content while offline with a pending action
The outbox is durable (P12.3)ConfirmedQueued requests are kept in the app’s persistent store under a key named networkRequestQueue
An app-level sync engine drains it (P12.4, P13.2)ConfirmedA sequential queue in the network layer subscribes to network state and flushes when the device is back online. It belongs to no screen
Content is stored on the device (P7.3, P11.1, P12.1)ConfirmedThe app keeps its data in a persistent store that every screen reads
Drafts are stored as data (P8.2, P8.4, P4.3)ConfirmedThe store has a collection keyed by chat for draft comments
Synthesized back stack for links (P8.6)ConfirmedThe link handler computes a matching full-screen route to place under the target, for back to lead to
Text does not follow the system size (P32.2)ConfirmedThe app’s text component turns font scaling off
Pending deletes are struck throughConfirmedThe same wrapper applies a strikethrough when the pending action is a delete. Not counted in the score, since no probe asked for it
Links open the page (P8.5), replace the current place (P8.7)ConsistentThe link configuration and the adapted-state helper exist and match the behavior. The path was not traced end to end
Detail on device before open (P11.6), one copy for list and detail (P9.1), growth in user data (P4.1)ConsistentAll three follow from one persistent store that screens subscribe to. Not traced for these specific screens
No text selection, own action list (P32.1)ConsistentA context menu component provides the copy actions
Cold start timing (P7.1)Not in the clientA measurement
A blocking fetch behind the splash (P7.2)ContradictedThe splash is hidden when navigation is ready and the language is loaded. Both are local. Probe P7.3 had already shown the same thing: the splash ended in content with no network
The app does not persist the last screen (P7.4)ContradictedThe app saves the last visited path to its store on every navigation, and has startup code that restores it. That code returns early for an account on a public email domain. The test account uses a consumer email address, which would explain what was seen, though the flag was not checked on the account itself. The behavior seen was real. The design claim drawn from it was wrong
Memory only, or a time limit (P8.3)ContradictedSame cause. The reading offered two candidate designs and the true one was neither

The three wrong readings, and what changed

Section titled “The three wrong readings, and what changed”

Each failure had the same shape: the behavior was recorded correctly, and the probe’s reading claimed more than the behavior supports.

1. P7.2 read a splash as a network wait. Its first outcome said “a blocking fetch behind a splash”. A splash can equally cover local work, such as opening a stored database. The probe that separates the two, launching in airplane mode, was the very next one, and it gave the right answer. The reading now names both candidates, points to P7.3 to separate them, and is marked Speculative.

2. P7.4 turned “it landed on Home” into “it does not save the last screen”. Expensify does save it, and restores it for some accounts. From one account, a condition on account type is invisible. The reading now says what was seen and lists conditional restoration as a candidate.

3. P8.3 listed two candidates and missed the third. It now includes conditional restoration and says plainly that one account on one device cannot rule it out.

This is the rule from Chapter 5 applied to the handbook itself: list every candidate design before choosing. All three corrections are in the chapter data, so the next teardown, by a person or an agent, gets the corrected readings.

  • The technology. As with Bluesky, the source shows a cross-platform app, and no probe that was run pointed to it.
  • One probe did not apply. P8.1 compares tab histories, but this app hides its tab bar on every pushed screen, so there is no way to switch tabs from inside one.
  • A source newer than the app. The code checked is one version line ahead of the build tested. A behavior could differ between them for that reason alone.
  • What needs a second device was skipped. Conflicts between two devices, real-time delivery to another person, and anything involving a second account.

The recorded teardown is in the repository under teardowns/expensify/.

Terminal window
npm run teardown -- org.me.mobiexpensifyg