php 122 lines · 3 tabs

Batching Laravel Queue Jobs to Roll Up Daily Order Totals With a Completion Callback

Shared by codesnips Sep 2026
3 tabs
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('daily_order_summaries', function (Blueprint $table) {
            $table->id();
            $table->date('summary_date');
            $table->string('currency', 3);
            $table->unsignedInteger('orders_count')->default(0);
            $table->decimal('gross_total', 15, 2)->default(0);
            $table->timestamps();

            $table->unique(['summary_date', 'currency']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('daily_order_summaries');
    }
};
3 files · php Explain with highlit

This snippet shows how to build a fault-tolerant daily rollup that aggregates raw orders into a daily_order_summaries table using Laravel's job batching feature. The pattern splits one large aggregation into many small, independent jobs — one per date — that run in parallel across queue workers, then fires a completion callback once the whole batch finishes. This scales far better than a single long-running command, which risks memory blowups and timeouts, and gives per-day retry granularity so one bad day does not fail the entire run.

The summaries migration defines the target table. A composite unique index on summary_date keeps the rollup idempotent: re-running a day must not create duplicate rows. The monetary column uses decimal rather than a float to avoid rounding drift when summing thousands of orders, and orders_count lets reports distinguish a zero-revenue day from a missing one.

In RollUpDailyOrders job, each instance owns exactly one Carbon date. The handle method groups that day's orders by currency and writes results with updateOrCreate, keyed on the unique columns, so the job is safe to retry. The WithoutOverlapping middleware keyed on the date prevents two workers from processing the same day concurrently, and $backoff staggers retries. Because each job touches only one date, its working set stays tiny regardless of total volume.

The DispatchDailyRollups command is the orchestrator. It builds the date range, maps each date to a job, and hands the collection to Bus::batch(...). The fluent then, catch, and finally callbacks attach behavior to the batch lifecycle: then runs only when every job succeeds, catch fires on the first failure, and finally always runs for cleanup and metrics. allowFailures() is deliberately omitted so a failed day marks the batch as failed rather than silently skipping data.

A subtle pitfall worth noting: batch callbacks are serialized, so they should close over scalars like $batch->id rather than heavy objects. The onQueue and name calls make the batch observable in Horizon and the job_batches table, which matters when a nightly rollup quietly stops producing numbers. Reach for this approach whenever a periodic aggregation grows too large for one process but decomposes cleanly along a natural key such as a date or tenant.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
go
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"

Retry Postgres serialization failures with bounded attempts

go postgres transactions
by Leah Thompson 1 tab
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
ruby
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]

sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first

SQL injection prevention with unsafe and safe query patterns

sql-injection owasp database
by Kai Nakamura 3 tabs
php
<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

Laravel service container and dependency injection

laravel dependency-injection service-container
by Carlos Mendez 2 tabs
sql
-- EXPLAIN ANALYZE (actual execution statistics)
EXPLAIN ANALYZE
SELECT u.username, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'

Advanced query optimization techniques

database optimization query-performance
by Maria Garcia 2 tabs

Share this code

Here's the card — post it anywhere.

Batching Laravel Queue Jobs to Roll Up Daily Order Totals With a Completion Callback — share card
Link copied