Table of Contents
You’re about to run a one-off script that updates 10,000 database records. You’ve tested it on staging. You’ve code-reviewed it. But you still want to see exactly what it’ll do in production before committing.
Enter: the dry-run transaction pattern.
The Problem with Fake Dry-Runs
Most “dry-run” modes look like this:
if ($dryRun) {
$this->info("Would update order #{$order->id}");
} else {
$order->update(['status' => 'processed']);
}
The issue? You’re not running the real code. If there’s a bug in the actual update logic β a constraint violation, a triggered event that fails, a missing column β you won’t find out until you run it for real.
The Transaction Rollback Trick
Instead, run the actual code path, but wrap it in a transaction and roll it back:
use Illuminate\Support\Facades\DB;
class ProcessOrdersCommand extends Command
{
public function handle()
{
$dryRun = $this->option('dry-run');
if ($dryRun) {
$this->warn('π DRY-RUN MODE β changes will be rolled back');
DB::beginTransaction();
}
try {
$orders = Order::pending()->get();
foreach ($orders as $order) {
$order->update(['status' => 'processed']);
$this->info("β Processed order #{$order->id}");
}
if ($dryRun) {
DB::rollback();
$this->info('β
Dry-run complete β no changes saved');
} else {
$this->info('β
All changes committed');
}
} catch (\Exception $e) {
if ($dryRun) {
DB::rollback();
}
throw $e;
}
}
}
What You Get
With this pattern:
- Real execution: Every query runs, every constraint is checked
- Event triggers fire: Model observers, jobs, notifications β all execute
- Nothing persists: Rollback undoes all database changes
- Safe preview: See exactly what would happen, but commit nothing
Bonus: Track What Changed
Want to see before/after values?
$changes = [];
foreach ($orders as $order) {
$original = $order->toArray();
$order->update(['status' => 'processed']);
$changes[] = [
'id' => $order->id,
'before' => $original['status'],
'after' => $order->status
];
}
if ($dryRun) {
$this->table(['ID', 'Before', 'After'],
array_map(fn($c) => [$c['id'], $c['before'], $c['after']], $changes)
);
}
Now your dry-run shows a summary table of what would change.
When Not to Use This
This pattern doesn’t help with:
- External API calls: Transaction rollback won’t undo HTTP requests
- File operations: Transactions don’t cover filesystem changes
- Email/notifications: Side effects outside the database still fire
For those, you still need conditional logic or feature flags.
The Takeaway
Dry-run via transaction rollback tests your real code path. If it works in dry-run, it works for real. No surprises, no “but it worked in staging” excuses.
Add --dry-run to every risky command. Your production database will thank you.

Leave a Reply