How Account Balance Synchronization Works in Maybe Finance: Inside the Sync Jobs Pipeline
Account balance synchronization in Maybe Finance is orchestrated by SyncJob, which delegates to the Sync model's state machine to execute Account::Syncer and Balance::Materializer, ultimately calculating, persisting, and caching daily balances within a database transaction.
The maybe-finance/maybe repository implements a robust background processing system to keep financial data current. Account balance synchronization operates through a carefully orchestrated pipeline of Sidekiq jobs, state machines, and materialization services that ensure accurate daily balance calculations while maintaining data integrity.
The Sync Job Entry Point and State Management
SyncJob: The Sidekiq Entry Point
The synchronization process begins in app/jobs/sync_job.rb, a minimal Sidekiq worker that serves as the entry point for all sync operations. This job pulls a Sync record from the queue and invokes the orchestration logic.
# app/jobs/sync_job.rb
class SyncJob < ApplicationJob
def perform(sync)
sync.perform
end
end
When you enqueue a sync, the system creates a parent Sync record linked to a syncable object (typically an Account), then hands it to Sidekiq:
account = Account.find(42) # any account you own
sync = Sync.create!(syncable: account) # creates a parent sync record
SyncJob.perform_later(sync) # Sidekiq will run the job
The Sync Model State Machine
The Sync model in app/models/sync.rb implements a state machine that manages the lifecycle of a synchronization task. It validates sync windows, handles error rescuing, and delegates the actual work to the syncable object.
The status flows from pending → syncing → completed or failed. During execution, the model:
- Starts the sync (
start!) - Delegates to the syncable's
perform_syncmethod - Rescues and reports errors to Sentry without breaking the chain
- Finalizes child syncs
- Triggers post-sync actions
Account Balance Calculation and Materialization
Account::Syncer Orchestration
For regular bank or cash accounts, the syncable object is Account::Syncer located in app/models/account/syncer.rb. This class implements the perform_sync method, which serves as the bridge between the generic sync framework and account-specific balance logic.
The Account::Syncer performs two critical functions:
- Imports any missing market data required for valuation
- Invokes the
Balance::Materializerto calculate and persist balances
Crucially, the syncer determines the calculation strategy based on whether the account is linked to an external provider (like Plaid) or managed manually:
# app/models/account/syncer.rb
strategy = account.linked? ? :reverse : :forward
Balance::Materializer.new(account, strategy: strategy).materialize_balances
Forward strategy is used for manual or cash-only accounts, calculating balances from the oldest entry forward and updating the account's cached totals.
Reverse strategy is used for linked accounts (e.g., Plaid), walking the ledger backwards because the external source supplies the most recent balance and older balances must be derived.
Balance::Materializer Implementation
The core algorithm resides in app/models/balance/materializer.rb. This service runs inside a database transaction and executes a five-step pipeline to ensure atomic balance updates:
materialize_holdings– Delegates toHolding::Materializerto generate the holdings required for balance calculationscalculate_balances– Builds a list ofBalanceobjects using eitherBalance::ForwardCalculatororBalance::ReverseCalculatordepending on the strategypersist_balances– Bulk upserts the new balance rows into the databasepurge_stale_balances– Removes out-of-range balance records that no longer align with the current sync windowupdate_account_info– For forward syncs, writes the latest aggregate balance onto theaccountstable'sbalanceandcash_balancefields
You can manually trigger this process for debugging or data repair:
account = Account.find(42)
materializer = Balance::Materializer.new(account, strategy: :forward)
materializer.materialize_balances
Post-Sync Operations and Error Handling
Transfer Auto-Matching
After the main synchronization completes, the system runs post-sync hooks defined in Sync#perform_post_sync. This method calls account.family.auto_match_transfers!, which automatically reconciles transfer transactions between accounts—ensuring that a withdrawal from one account matches the corresponding deposit in another.
The sync also broadcasts a SyncCompleteEvent, allowing the UI to react to finished synchronizations in real-time.
Transaction Safety and Error Reporting
The entire sync operation runs within a database transaction, ensuring that balance updates are atomic—either all changes persist or none do. Error handling follows a defensive pattern:
- Any exception inside
syncable.perform_synctriggers thefail!state transition - Errors are logged and reported to Sentry via
report_errorwithout breaking the overall sync chain - The system attempts to finalize child syncs so that failures cascade correctly through dependent records
You can inspect sync state transitions in tests or monitoring:
sync = Sync.create!(syncable: account)
assert_equal "pending", sync.status
sync.perform
assert_includes %w[completed failed], sync.reload.status
Key Source Files for Account Balance Synchronization
Summary
- Account balance synchronization in Maybe Finance runs as a Sidekiq job (
SyncJob) that orchestrates a statefulSyncrecord through a multi-step pipeline. - The
Balance::Materializerexecutes the core calculation logic within a database transaction, using either forward (manual accounts) or reverse (linked/Plaid accounts) strategies to generate daily balances. - Post-sync hooks automatically match transfers between family accounts and broadcast completion events, while comprehensive error handling ensures failures are logged to Sentry without corrupting the balance data.
Frequently Asked Questions
How do I manually trigger account balance synchronization for a specific account?
You can enqueue a sync manually through the Rails console by creating a Sync record linked to the account and passing it to SyncJob:
account = Account.find(42)
sync = Sync.create!(syncable: account)
SyncJob.perform_later(sync)
This follows the exact same code path as automated syncs, ensuring consistency between manual and background operations.
What is the difference between forward and reverse balance calculation strategies?
The forward strategy is used for manual or cash-only accounts, calculating balances chronologically from the oldest transaction forward and updating the account's cached balance and cash_balance fields. The reverse strategy is used for linked accounts (e.g., Plaid integrations), walking the ledger backwards from the most recent externally-provided balance to derive historical values. The strategy is determined in Account::Syncer based on whether account.linked? returns true.
How does Maybe Finance handle errors during balance synchronization?
The entire synchronization runs inside a database transaction, ensuring atomic updates—either all balance changes persist or none do. If an exception occurs during syncable.perform_sync, the Sync model transitions to the failed state, logs the error, and reports it to Sentry via report_error without breaking the overall sync chain. The system also attempts to finalize child syncs so that failures cascade correctly through dependent records.
What happens after account balances are successfully synchronized?
After the Balance::Materializer completes and the sync status transitions to completed, the Sync#perform_post_sync method executes two critical operations: it calls account.family.auto_match_transfers! to automatically reconcile transfer transactions between family accounts, and it broadcasts a SyncCompleteEvent to notify the UI and other subscribers that the account balance synchronization has finished.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →