Balance sheet cache layer, non-blocking sync UI #2356

Merged
zachgoll merged 7 commits from zachgoll/better-balance-sheet-caching into main 2025-06-11 06:20:06 +08:00
zachgoll commented 2025-06-11 03:26:17 +08:00 (Migrated from github.com)

This PR tackles two performance related issues with the accounts sidebar:

  1. Proper caching of balance sheet queries + balance sheet simplifications via value objects
  2. Less intrusive sync states (non-blocking)

Balance sheet

This PR breaks down the BalanceSheet into more manageable, cacheable classes + value objects. Most notably, it introduces two key classes with aggressive caching:

  1. BalanceSheet::AccountTotals - responsible for fetching account groups, rolling up their sums with exchange rate conversion, and caching the result. This query only re-fetches when a user has an account sync that alters balance sheet data.
  2. BalanceSheet::SyncStatusMonitor - responsible for determining which of the family's accounts are currently "syncing" and should show an indicator. This is cached during idle times, and is invalidated on every sync-related event (sync started, completed, errored, etc.)

The Sync class updates family.latest_sync_activity_at and family.latest_sync_completed_at timestamps, which are the inputs to each of these class cache keys. The purpose of these timestamp fields is to delegate all cache-busting to the background Sync process, which allows us to avoid running "is syncing?" queries on every page load (major performance issue).

Sync states UI / UX

Previously, we showed blocking sync states when accounts were running background jobs. This was a poor UI/UX pattern because when queues were blocked, running slow, or the user had a large number of accounts, the dashboard (high touchpoint view) would show what seemed to be a "stuck" loader.

The original rationale for this was—"If we don't have the latest data, don't show user anything that could be outdated or wrong". While good in theory (assuming data syncs complete quickly), this broke down in practice as we faced scaling issues with our data syncs.

New pattern: non-blocking sync status indicators

This PR introduces a much less-intrusive sync state UI/UX pattern where we show tiny loading indicators next to accounts, account groups, or charts that are currently calculating and updating data in the background. This has several benefits:

  • We show the user the data we have immediately (even if it's slightly outdated)
  • If syncs get "stuck", the loading indicator doesn't block the user from using the app (it is just a small, non-blocking annoyance)
This PR tackles two performance related issues with the accounts sidebar: 1. Proper caching of balance sheet queries + balance sheet simplifications via value objects 2. Less intrusive sync states (non-blocking) ## Balance sheet This PR breaks down the `BalanceSheet` into more manageable, cacheable classes + value objects. Most notably, it introduces two key classes with aggressive caching: 1. `BalanceSheet::AccountTotals` - responsible for fetching account groups, rolling up their sums with exchange rate conversion, and caching the result. **This query only re-fetches when a user has an account sync that alters balance sheet data**. 2. `BalanceSheet::SyncStatusMonitor` - responsible for determining which of the family's accounts are currently "syncing" and should show an indicator. This is cached during idle times, and is invalidated on every sync-related event (sync started, completed, errored, etc.) The `Sync` class updates `family.latest_sync_activity_at` and `family.latest_sync_completed_at` timestamps, which are the _inputs_ to each of these class cache keys. The purpose of these timestamp fields is to delegate all cache-busting to the background Sync process, which allows us to avoid running "is syncing?" queries on every page load (major performance issue). ## Sync states UI / UX Previously, we showed _blocking_ sync states when accounts were running background jobs. This was a poor UI/UX pattern because when queues were blocked, running slow, or the user had a large number of accounts, the dashboard (high touchpoint view) would show what seemed to be a "stuck" loader. The original rationale for this was—"If we don't have the latest data, don't show user anything that could be outdated or wrong". While good in theory (assuming data syncs complete quickly), this broke down in practice as we faced scaling issues with our data syncs. ### New pattern: non-blocking sync status indicators This PR introduces a much less-intrusive sync state UI/UX pattern where we show tiny loading indicators next to accounts, account groups, or charts that are currently calculating and updating data in the background. This has several benefits: - We show the user the data we have immediately (even if it's slightly outdated) - If syncs get "stuck", the loading indicator doesn't block the user from using the app (it is just a small, non-blocking annoyance)
Sign in to join this conversation.