Appearance
Tier 2b · Chapter 5: Laravel Async Patterns
PHP/Laravel Track · ~30 min read + exercise
A note on sources: The async, reliability, and idempotency principles come from Foundations Chapters 4 and 5 (and the source books); the Laravel queue, job, and event mechanics come from Laravel's documentation, flagged where relevant.
Why this chapter exists
Foundations Chapter 5 made the case for asynchronous work and Chapter 4 covered the reliability patterns that make it safe; the JS/TS track built this with queues and workers in Node. This chapter does the same in Laravel, which has excellent built-in async tooling — queues, jobs, events, scheduled tasks — that makes dispatching background work a one-liner. And as in the Node track, the one-liner is the easy part; the architecture is in making the deferred work reliable, because Laravel's ergonomics make it just as easy to ship async work that silently loses jobs or duplicates side effects.
The throughline is identical to the Node async chapter, because the underlying truth is framework-independent: a queue moves work off the request, and everything that matters is making the deferred work reliable — retries, idempotency, dead-letter handling. Laravel gives you the dispatch and the worker infrastructure for free; what it can't give you for free is the judgment about idempotency (because Laravel queues, like all queues, deliver at-least-once) and the failed-job handling that keeps work from vanishing. This chapter is the Laravel realization of that, and it deliberately echoes the Node track — the patterns transfer, the mechanics differ.
What this chapter covers
- Queues and jobs in Laravel — the framework's async tooling and how it maps to the foundations.
- Reliability for Laravel jobs — retries, backoff, and the at-least-once idempotency requirement.
- Failed jobs — Laravel's dead-letter equivalent and why it needs monitoring.
- Events and listeners — Laravel's mechanism for decoupled, choreographed reactions.
Queues and jobs in Laravel
Laravel's queue system is the framework's realization of the producer/queue/worker architecture from Foundations Chapter 5. You define a job (a class encapsulating work to do), dispatch it (the producer, usually from a controller handling a request), it lands on a queue (backed by Redis, a database table, SQS, or others), and a queue worker process (php artisan queue:work) pulls and processes jobs independently of the request that created them. The request returns at dispatch; the work happens later in the worker.
php
// Dispatch from a request — returns immediately, work happens later in a worker.
SendTicketConfirmation::dispatch($ticket);
// The job class — runs in the worker process.
class SendTicketConfirmation implements ShouldQueue
{
use Queueable; // serializes the Eloquent model by ID, re-fetching when the job runs
public function __construct(public Ticket $ticket) {}
public function handle(Mailer $mailer): void
{
// ...send the email
}
}This buys exactly what Foundations Chapter 5 promised — fast response (return at dispatch), decoupling (the mail provider can be down without breaking the request, the job waits), and burst absorption (a flood of dispatches queues and drains at the worker's pace). The ticket-confirmation example from the foundations is this, in Laravel. (Flagged: ShouldQueue, dispatch, queue:work are Laravel mechanics; the producer/queue/worker architecture is from the foundations.)
The architectural point that carries from the Node track: workers are separate processes from your web app. Laravel's queue:work runs in its own process (or many — you scale workers independently of web servers), which means heavy background work doesn't compete with request serving, and you scale the worker fleet to the job backlog and the web fleet to request load, independently. The queue is the decoupling seam between them. (PHP isn't single-threaded the way Node is, so the "don't block the event loop" framing differs — but the architectural reason to separate workers from web is the same: isolation of heavy work and independent scaling.)
A Laravel-specific caution worth flagging: jobs serialize their data to sit on the queue, and Laravel serializes Eloquent models by storing the ID and re-fetching when the job runs (the Queueable trait, which bundles SerializesModels). This is usually what you want (the job gets fresh data), but it means the model might have changed or been deleted between dispatch and execution — the job must handle the model no longer existing or being in a different state than at dispatch. Passing whole model state versus re-fetching is a small consistency decision the framework makes for you by default, and worth being aware of.
Reliability for Laravel jobs
This is where the chapter earns itself, identically to the Node track, because the reliability truths are framework-independent. Dispatching is trivial; reliable jobs require the Foundations Chapter 4 patterns, and Laravel supports them as job configuration — but the decisions are yours.
Retries with backoff. Jobs fail transiently (a downstream blips), and Laravel will retry a failed job up to a configured number of attempts ($tries), with a configurable backoff between attempts ($backoff) — which should be exponential with some spacing, per Chapter 4's prescription, to avoid the retry storm that hammers a struggling dependency. These are properties you set on the job; the default is a single attempt — no retry unless you set $tries (or --tries on the worker) — which is rarely what reliability wants, so setting it deliberately is the architectural act.
At-least-once delivery makes idempotency mandatory. The crucial, framework-independent fact, exactly as in the Node track: Laravel queues deliver at-least-once, not exactly-once. A worker can process a job, then crash before marking it complete, and the job will be retried — so your jobs will sometimes run more than once on the same data, and must be safe when they do. A job that sends a confirmation email must not send two; a job that charges a card must not double-charge. This is Chapter 4's idempotency keystone, made non-negotiable by the delivery model. Laravel offers helpers (a ShouldBeUnique job to prevent duplicate dispatch, and WithoutOverlapping middleware), but those address dispatch-time and overlap, not the at-least-once redelivery of an already-running job — so the handler itself must be idempotent:
php
public function handle(Mailer $mailer): void
{
// Idempotency guard: has this confirmation already been sent? Recorded durably.
if ($this->ticket->confirmationSent()) {
return; // duplicate execution — safely skip
}
$mailer->send(/* ... */);
$this->ticket->markConfirmationSent(); // record, ideally in the same transaction
}The same habit as the Node track: write every job as if it will run twice, because eventually it will. (Flagged: ShouldBeUnique, WithoutOverlapping, $tries, $backoff are Laravel mechanics; the idempotency requirement is from the foundations and applies to any queue.)
Failed jobs
When a Laravel job exhausts its retries, it doesn't vanish — it goes to the failed_jobs table (or configured failed-job store), which is Laravel's realization of the dead-letter queue from the Node async chapter and Foundations Chapter 5. This is the difference between "the Tuesday-afternoon confirmation emails silently never sent" and "forty failed jobs are recorded with their exceptions; we fixed the mail config and retried them with queue:retry."
The disciplines are exactly the foundations' DLQ disciplines, in Laravel form:
A failed-jobs table nobody watches is a black hole. The whole value is visibility, which requires someone (or something) to look. A growing failed_jobs table means work is failing systematically and someone needs to know — so it should be monitored and alerted on (Laravel Horizon provides dashboards and metrics for Redis queues; alerting on failed-job count is the operational hookup). An unwatched failed-jobs table is a slower way to lose work, the same warning as the Node track.
Retrying failed jobs requires idempotency. php artisan queue:retry re-runs failed jobs, which runs the handler again — so the same idempotency that protects against at-least-once redelivery protects against manual retry. A job you can't safely retry is a job you can't safely recover, undermining the failed-jobs table's purpose. Idempotency is what makes the recovery tool safe to use.
Distinguish what's worth retrying. Some failures are permanent (the customer was deleted, the payload is malformed) and retrying wastes attempts; Laravel lets a job fail() early or limit attempts, and you can handle permanent failures distinctly from transient ones where the failure is distinguishable. The failed() method on a job runs when it ultimately fails, which is the hook for cleanup or alerting on a specific job's permanent failure.
The reliability chain is identical to the Node track because it's a property of queues, not frameworks: retries with backoff handle transient failures, idempotency makes those retries and the at-least-once delivery and the manual retries all safe, and the failed-jobs table catches the unprocessable so it's visible and recoverable — provided it's monitored. Laravel gives you the infrastructure; you supply the idempotency and the monitoring.
Events and listeners
Laravel's event and listener system is the framework's realization of the decoupled, choreographed communication from Foundations Chapter 5, and it connects directly to the domain events from this track's domain-modeling chapter. An event represents that something happened (TicketResolved); listeners react to it, each independently, without the event-raising code knowing or caring about them. Dispatch the event when something significant occurs, and the listeners — notify the customer, schedule the satisfaction survey, record an analytics event — each handle their piece:
php
// The domain raises an event; it doesn't know or care who reacts.
TicketResolved::dispatch($ticket);
// Independent listeners react — add or remove them without touching the resolve logic.
// NotifyCustomer, ScheduleSatisfactionSurvey, RecordAnalyticsEvent — each its own class.This is choreography (Foundations Chapter 5): no central coordinator, each component reacting to events, the workflow emerging from the reactions. Its strength is the foundations' decoupling — you can add a new reaction to TicketResolved (a new listener) without touching the ticket logic, and the resolve code stays ignorant of the growing list of consequences. Its cost is the foundations' choreography cost — the full consequence of resolving a ticket isn't visible in one place; it's scattered across listeners, which can make the end-to-end flow harder to trace. The foundations' guidance applies: choreography (events) for the peripheral reactions where decoupling matters, and a more orchestrated approach (an explicit service coordinating the steps) for the critical, complex workflows where you need to see and control the whole process.
Queued listeners tie the two halves of this chapter together: a listener can implement ShouldQueue, so the reaction happens asynchronously in a worker rather than synchronously in the request that dispatched the event. This is the common, powerful pattern — raise a domain event synchronously, let queued listeners handle the slow reactions (email, analytics) in the background — combining the decoupling of events with the off-the-request-path benefit of queues. And because queued listeners are jobs, they inherit everything from the reliability section: at-least-once delivery, the need for idempotency, retries, and failed-job handling. A queued listener that sends an email must be idempotent for the same reason any job must. (Flagged: Laravel's event/listener/queued-listener system is the framework mechanism; the choreography and domain-event concepts are from the foundations and the DDD book.)
That's async in Laravel: queues and separate workers move work off the request and scale independently, jobs need the foundations' reliability (retries, mandatory idempotency, monitored failed-jobs), and events/listeners provide decoupled choreography — with queued listeners combining both, and inheriting the same reliability requirements. The mechanics are Laravel's; the architecture is the foundations', and it transfers stack to stack.
Practice
Budget about ninety minutes.
Exercise 1: Find work to queue (20 min)
In a Laravel app you work on, find synchronous work in a request that doesn't need to block the response (the audit from Foundations Chapter 5, in Laravel). For one piece, sketch it as a queued job: what dispatches it, what data it carries (and whether that data could go stale between dispatch and execution), what the worker does.
Exercise 2: Make a job idempotent (35 min)
Take a job with a side effect and, given at-least-once delivery, trace how it could run twice harmfully. Redesign the handler to be safe — a durable guard checking whether the work was already done, recorded with the side effect. Then set its retry policy ($tries, $backoff) deliberately rather than by default.
If you use an AI assistant to draft the idempotency guard, interrogate the race: under concurrent redelivery, can two copies both pass the "already done?" check before either records completion? Closing that with a transaction or unique constraint (this track's consistency chapter) is the part that's easy to get subtly wrong.
Exercise 3: Check the failure path (35 min)
Find out what currently happens to a job in your app that exhausts its retries — does it reach failed_jobs, and is anyone watching that table? Sketch the monitoring/alerting you'd add (failed-job count, Horizon dashboards if you're on Redis), and confirm the jobs are idempotent enough that queue:retry is safe to run on them.
Deliverable
A short async design for one Laravel workflow: the job (dispatcher, payload, worker), its idempotency approach and why at-least-once requires it, its retry policy, and the failed-job monitoring plan. If it's a multi-reaction flow, take a multi-listener event (or design one), decide which listeners should be queued, and make the explicit call on whether the overall flow is better as choreography (events and independent listeners) or orchestration (a coordinating service) — with the reasoning for the choice.
Check yourself
- Draw the Laravel queue/job/worker architecture and explain what dispatching to a queue buys.
- Explain why at-least-once delivery makes idempotent job handlers mandatory in Laravel.
- Explain what the
failed_jobstable is for and why it's worthless without monitoring. - Explain how events/listeners implement choreography, the trade-off versus orchestration, and what queued listeners combine.
What a strong answer demonstrates: you can state the reliability chain as a property of queues, not Laravel — at-least-once delivery makes idempotent handlers mandatory, retries-with-backoff handle the transient failures, and a monitored dead-letter store catches the unprocessable — and explain why each link depends on the others (an unwatched failed_jobs table is just a slower way to lose work; queue:retry is only safe if jobs are idempotent). A strong answer also makes the choreography-vs-orchestration call as a trade-off (decoupling vs. traceability), not a default, and notes that queued listeners inherit every job-reliability requirement.
Going deeper (optional)
- Software Architecture: The Hard Parts (Ford, Richards, Sadalage & Dehghani, O'Reilly, 2021) — the distributed-workflow and saga chapters, for keeping multi-step async workflows consistent (which the capstone returns to).
- Building Microservices (Newman, 2nd ed., O'Reilly, 2021), the workflow chapter, for orchestration vs. choreography in more depth. The Laravel documentation on queues, jobs, events, and Horizon for the framework mechanics. (Flagged: all Laravel queue/event mechanics are from Laravel docs; the async, reliability, and choreography principles are from the foundations.)
- Primary source: Gregor Hohpe & Bobby Woolf, Enterprise Integration Patterns (Addison-Wesley, 2003) — the canonical catalog of messaging patterns behind this chapter: Idempotent Receiver (why at-least-once forces idempotent handlers), Dead Letter Channel (the
failed_jobstable's seminal form), and Guaranteed Delivery and Competing Consumers (the queue/worker model). - Primary source: Michael Nygard, Release It!: Design and Deploy Production-Ready Software (Pragmatic Bookshelf; 1st ed. 2007, 2nd ed. 2018) — the original treatment of the stability patterns the retry/backoff discipline rests on: Circuit Breaker, Timeouts, and the cascading-failure failure mode a naive retry storm produces.
Key takeaways
- Laravel queues + separate worker processes move work off the request (fast responses, decoupling, burst absorption) and scale independently of the web tier.
- At-least-once delivery makes idempotent jobs mandatory — handlers will run more than once, so they must be safe to; pair with deliberate retries + backoff.
- The
failed_jobstable is Laravel's dead-letter queue — catches the unprocessable for visibility and recovery, but only if monitored, and only safe to retry if jobs are idempotent. - Events and listeners give decoupled choreography (great for peripheral reactions, harder to trace than orchestration); queued listeners combine event decoupling with off-the-request-path execution — and inherit all the job reliability requirements.
Next: Security & Validation — authentication, authorization policies, and request validation in Laravel, where the foundations' security attribute becomes concrete.