Make Your Artisan Commands Idempotent

📖 2 minutes read

We recently had to bulk-process a batch of records — update statuses, add notes, trigger some side effects. The kind of thing you write a one-off artisan command for.

The first instinct is to just loop and execute. But what happens when the command fails halfway through? Or the queue worker restarts? You need to run it again, and now half your records get double-processed.

The fix: Make every step check if it’s already been done.

public function handle()
{
    $orders = Order::whereIn('reference', $this->references)->get();

    foreach ($orders as $order) {
        // Step 1: Always safe to repeat
        $order->addNote('Bulk processed on ' . now()->toDateString());

        // Step 2: Skip if already done
        if ($order->status === OrderStatus::CANCELLED) {
            $this->info("Skipping {$order->reference} — already cancelled");
            continue;
        }

        // Step 3: Do the actual work
        $order->cancel();
        $this->info("Cancelled {$order->reference}");
    }
}

Key patterns:

  • Check before acting — If the record is already in the target state, skip it
  • Log what you skip — So you can verify the second run did nothing harmful
  • Add a --dry-run flag — Always. Test the logic before committing to production changes
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Run without making changes');

// In your handle method:
if ($this->option('dry-run')) {
    $this->info("[DRY RUN] Would cancel {$order->reference}");
    continue;
}

The rule is simple: if you can’t safely run it twice, it’s not ready for production.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *