# How Account Balance Synchronization Works in Maybe Finance: Inside the Sync Jobs Pipeline

> Discover how Maybe Finance sync jobs orchestrate account balance synchronization using SyncJob, Account::Syncer, and Balance::Materializer to efficiently calculate and cache daily balances.

- Repository: [Maybe/maybe](https://github.com/maybe-finance/maybe)
- Tags: internals
- Published: 2026-03-07

---

**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`](https://github.com/maybe-finance/maybe/blob/main/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.

```ruby

# 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:

```ruby
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`](https://github.com/maybe-finance/maybe/blob/main/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`](https://github.com/maybe-finance/maybe/blob/main/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:

```ruby

# 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`](https://github.com/maybe-finance/maybe/blob/main/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:

```ruby
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:

```ruby
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)](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)](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)](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)](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)](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)](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)](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`:

```ruby
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.