Back to Blog

Apex Cursors for Fintech: A Production Guide

Payment Reconciliation at Scale | Spring '26 Release

The Story: NovaPay's Growing Pains

NovaPay is a Series B fintech startup processing payments for 12,000 small businesses. Their Salesforce org serves as the central ledger, tracking every transaction from multiple payment processors—Stripe, Square, PayPal, and direct ACH feeds.

The Numbers:

The Crisis: Their nightly reconciliation batch job started failing. At 1.8M records, it worked. At 2.1M, it timed out. At 2.3M, it crashed the org's async job queue entirely, blocking other critical processes.

System.AsyncException: Maximum number of batch apex jobs in queue (5) has been reached

The finance team was manually reconciling transactions in spreadsheets. Merchants were calling about delayed payouts. The VP of Engineering called an all-hands.

The Solution: Apex Cursors with Queueable chaining—processing 2.3M transactions in 47 minutes with zero queue contention.

This is that implementation.


Section 1: Why Cursors Beat Batch for Financial Processing

The Fintech Constraint Matrix

Requirement Batch Apex Problem Cursor Solution
Audit Trail State lost on failure Position persisted per-chunk
SLA Compliance Queue delays unpredictable Immediate execution, no queue
Partial Failures Entire job restarts Resume from exact record
Concurrent Jobs 5-job limit blocks payroll, reporting Unlimited queueable chains
Real-time Monitoring Only start/finish events Progress tracking per chunk

Financial Data Processing Patterns

Traditional Batch Flow (Problematic):

[2.3M Records] → Batch Start → ... 6 hours ... → Success/Fail (binary)
                     ↓
              Holds 1 of 5 batch slots
              No visibility for 6 hours
              Failure = restart from zero

Cursor + Queueable Flow (Optimal):

[2.3M Records] → Cursor Created → Queueable 1 (records 0-18K)
                                → Queueable 2 (records 18K-36K)
                                → Queueable 3 (records 36K-54K)
                                → ... continues ...

Each queueable: ~30 seconds
Total time: ~47 minutes
Progress visible in real-time
Failure at record 1.2M? Resume from 1.2M

Critical Architecture Note: Cursor Lifecycle

Cursors are NOT serializable across transactions. When chaining Queueables:

  1. The cursor object cannot be passed between jobs
  2. Each Queueable must recreate the cursor from the original query
  3. Use ORDER BY to ensure deterministic record ordering for position-based resumption
  4. Cursors expire after 15 minutes of inactivity
// WRONG - Cursor won't survive transaction boundary
System.enqueueJob(new MyJob(existingCursor, position));

// CORRECT - Recreate cursor, resume from tracked position
System.enqueueJob(new MyJob(queryString, position));

Continue reading the full tutorial for the complete implementation including data model, reconciliation engine, finalizers, governor limits guide, and testing strategy...