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:

  1. Starts the sync (start!)
  2. Delegates to the syncable's perform_sync method
  3. Rescues and reports errors to Sentry without breaking the chain
  4. Finalizes child syncs
  5. 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:

  1. Imports any missing market data required for valuation
  2. Invokes the Balance::Materializer to 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:

  1. materialize_holdings – Delegates to Holding::Materializer to generate the holdings required for balance calculations
  2. calculate_balances – Builds a list of Balance objects using either Balance::ForwardCalculator or Balance::ReverseCalculator depending on the strategy
  3. persist_balances – Bulk upserts the new balance rows into the database
  4. purge_stale_balances – Removes out-of-range balance records that no longer align with the current sync window
  5. update_account_info – For forward syncs, writes the latest aggregate balance onto the accounts table's balance and cash_balance fields

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_sync triggers the fail! state transition
  • Errors are logged and reported to Sentry via report_error without 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

File Role
[app/jobs/sync_job.rb](https://github.com/maybe-finance/maybe/blob/main/app/jobs/sync_job.rb) Sidekiq entry point that invokes Sync#perform.
[app/models/sync.rb](https://github.com/maybe-finance/maybe/blob/main/app/models/sync.rb) State machine orchestration, error handling, and post-sync callbacks.
[app/models/account/syncer.rb](https://github.com/maybe-finance/maybe/blob/main/app/models/account/syncer.rb) Account-specific sync logic including market data import and strategy selection.
[app/models/balance/materializer.rb](https://github.com/maybe-finance/maybe/blob/main/app/models/balance/materializer.rb) Core algorithm for building, persisting, and cleaning up balance records.
[app/models/holding/materializer.rb](https://github.com/maybe-finance/maybe/blob/main/app/models/holding/materializer.rb) Generates holdings required for balance calculations.
[app/models/balance/forward_calculator.rb](https://github.com/maybe-finance/maybe/blob/main/app/models/balance/forward_calculator.rb) Implements forward calculation strategy for manual accounts.
[app/models/balance/reverse_calculator.rb](https://github.com/maybe-finance/maybe/blob/main/app/models/balance/reverse_calculator.rb) Implements reverse calculation strategy for linked accounts.

Summary

  • Account balance synchronization in Maybe Finance runs as a Sidekiq job (SyncJob) that orchestrates a stateful Sync record through a multi-step pipeline.
  • The Balance::Materializer executes 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →