Laravel modernization: queues, idempotency and safe migration

Modernize a legacy PHP application incrementally with transactional writes, retry-safe jobs and a rehearsed rollback plan.

An application transaction feeding a queue and independent workers

The short answer

Migrate one business capability at a time. Protect database invariants with transactions and constraints, make queued jobs safe to retry, and verify delivery and rollback before moving the next route.

Choose a boundary small enough to verify

An incremental migration can reduce the scope of each release, but it is not risk-free. Start with a bounded workflow whose inputs, outputs and ownership are understood. Map the legacy data model, external side effects and reporting dependencies before moving traffic.

Record baseline error rates, response latency, queue age and business outcomes such as completed orders. Route a controlled set of requests to the new implementation and retain a clear route back. Avoid migrating storage, framework, payments and user interface simultaneously unless the dependencies require it.

Keep the critical write transactional

Validate permissions and input, then persist the minimum valid state transition within a database transaction. Use unique constraints for invariants such as one external reference per order. Row locks or conditional updates can protect concurrent transitions when used correctly.

A Redis lock can coordinate work, but an expired lease, a crash or an incorrectly scoped key can allow overlap. It is not a replacement for database constraints. Document which system owns each invariant and test simultaneous requests against it.

Dispatch only after committed data exists

A worker can run before its originating transaction commits. Laravel provides after-commit dispatch so jobs do not read uncommitted or rolled-back records. In a Laravel 12 application, a basic pattern is:

DB::transaction(function () use ($validated) {
    $order = Order::create($validated);
    ProcessOrder::dispatch($order->id)->afterCommit();
});

This example is deliberately incomplete: authorization, duplicate-request handling and business validation belong around it. After-commit dispatch also does not eliminate the failure window between the database commit and delivery to an external queue. For workflows requiring durable delivery, consider a transactional outbox and a retrying publisher.

Design jobs for at-least-once execution

Assume a job may execute more than once. Persist an idempotency key, track completed operations, and use the external provider’s idempotency mechanism when available. Do not mark a job complete before the external side effect has succeeded. Reconcile uncertain outcomes rather than blindly repeating a charge or shipment.

  • Set timeouts, retry limits and backoff according to the operation.
  • Keep worker timeout shorter than the queue’s retry interval, with room for shutdown, to reduce overlapping attempts.
  • Monitor failed jobs and queue age; an HTTP 202 response means accepted, not completed.
  • Restart long-running workers during releases so they load the intended code.

Rehearse failure and rollback

Test a duplicate webhook, a worker crash after an external call, a database deadlock, a queue outage and a failed deployment. Verify the resulting records and side effects, not just the HTTP response. Add reconciliation for work that was accepted but never completed.

Use backward-compatible schema changes while both applications are active. Define rollback conditions before release and check that the old application can read data written by the new one. Keep a single source of truth for each record until synchronization is explicitly designed.

Frequently asked questions

When should work stay synchronous?

When the caller needs an immediate authoritative result and the work is short enough to meet the response budget. Queues are useful for deferrable work; they introduce delivery, visibility and recovery responsibilities of their own.

Does after-commit dispatch guarantee that a job reaches the queue?

No. It prevents a job from being dispatched before its database transaction commits, but a failure can still occur between the commit and delivery to an external queue. For durable delivery requirements, evaluate a transactional outbox with a retrying publisher and reconciliation.

How do I prevent retries from creating duplicate payments or orders?

Use persistent idempotency keys, database constraints and the external provider’s idempotency mechanism where available. Track outcomes and reconcile uncertain responses. A short-lived lock alone does not cover every crash or retry window.

What makes an incremental migration easier to roll back?

Move a bounded workflow, preserve a clear routing path to the previous implementation and use backward-compatible schema changes. Test whether the old application can read records created by the new one. Define rollback triggers using errors and business outcomes before releasing.

Sources and further reading

Keep exploring

Compare rendering strategies or discuss a Laravel migration.

Paul Edward

Written by Paul Edward

Senior full-stack web developer working with PHP, Laravel, WordPress and AI-assisted web systems.

More about Paul

Leave a Reply

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

Loading a quick check… (this needs JavaScript)

Project brief Step 1 of 2 · The work

What do you want built?

A paragraph is genuinely enough to start. If it isn't work I'm right for, I'll say so and point you somewhere better.

The work

Pick everything that applies.

Platform

No idea is a perfectly good answer.

What are you trying to build, and what does it have to do for the people who use it? Write it the way you'd say it out loud.

0 / 1200

Two steps. Under a minute.