java 90 lines · 3 tabs

Roll Up Daily Sales Totals With a Scheduled Spring Batch Upsert via JdbcTemplate

Shared by codesnips Jul 2026
3 tabs
@Component
public class SalesRollupScheduler {

    private static final Logger log = LoggerFactory.getLogger(SalesRollupScheduler.class);
    private static final int ROLLUP_WINDOW_DAYS = 3;

    private final SalesRollupRepository repository;

    public SalesRollupScheduler(SalesRollupRepository repository) {
        this.repository = repository;
    }

    @Scheduled(cron = "0 15 1 * * *", zone = "UTC")
    @SchedulerLock(name = "dailySalesRollup", lockAtMostFor = "10m", lockAtLeastFor = "1m")
    public void runDailyRollup() {
        LocalDate today = LocalDate.now(ZoneOffset.UTC);
        LocalDate from = today.minusDays(ROLLUP_WINDOW_DAYS);

        List<DailySalesTotal> totals = repository.aggregateDailyTotals(from, today);
        int written = repository.upsertDailyTotals(totals);

        log.info("Sales rollup complete: {} days aggregated between {} and {}", written, from, today);
    }
}
3 files · java Explain with highlit

This snippet shows a common reporting pattern: a scheduled background job that aggregates raw transactional rows into a pre-computed summary table so dashboards can read fast, denormalized totals instead of scanning the whole orders table on every request. The work is split across three collaborating files that tell one story — a Spring @Configuration that defines the periodic trigger, the repository that performs the aggregate query and the batch upsert, and a lightweight DTO that carries the rolled-up figures between them.

In SalesRollupScheduler, @Scheduled fires runDailyRollup on a cron expression, and @SchedulerLock (from ShedLock) guarantees only one node in a clustered deployment executes the job at a time — without it, every instance would race to recompute the same days. The scheduler asks SalesRollupRepository for a window of recent days rather than the whole history, because re-aggregating only the trailing few days cheaply corrects late-arriving or amended orders while keeping the job bounded.

SalesRollupRepository is where the interesting SQL lives. aggregateDailyTotals runs a GROUP BY over orders to produce one DailySalesTotal per day, and upsertDailyTotals writes them back using batchUpdate. The write is an idempotent INSERT ... ON CONFLICT (sale_date) DO UPDATE, which is the key design choice: because the daily rollup is derived data, re-running the job must never create duplicates or drift. The unique constraint on sale_date makes the upsert the single source of truth, so the job is safe to retry, replay, or overlap-recover after a crash.

Using JdbcTemplate.batchUpdate sends all rows in one round trip with a BatchPreparedStatementSetter, which matters when a wide backfill window produces many rows — per-row INSERTs would multiply network latency. The whole runDailyRollup method is wrapped in @Transactional so the delete-and-repopulate style upsert commits atomically.

DailySalesTotal is an immutable carrier holding the saleDate, orderCount, grossCents, and netCents, keeping money as integer cents to avoid floating-point rounding errors. A trade-off worth noting: this trailing-window approach assumes corrections land within the recomputed range, so a very old amended order outside the window would require a manual wider backfill.


Related snips

Share this code

Here's the card — post it anywhere.

Roll Up Daily Sales Totals With a Scheduled Spring Batch Upsert via JdbcTemplate — share card
Link copied