skip to content

In a Laravel checkout, when should placing an order dispatch an event to listeners rather than call a service directly or dispatch a job?

level: seniorimportance: should knowfreq 45%

answer

  1. who needs the result
  2. must its failure fail checkout
  3. independent reactions owned elsewhere
  4. sync listeners run in the request
  5. hidden flow: check event:list

basics

~20 s

Call a service directly when checkout needs its result or must fail with it; dispatch a job for one background task checkout owns; fire an event when several independent reactions, owned elsewhere, follow the order and can each be queued.

solid answer

~50 s

Use a **direct call** for steps that are part of placing the order: charging payment or reserving stock when the order is invalid without it, because the caller needs the outcome and a failure must abort checkout. Use a **job** when checkout itself wants one piece of work done later, such as generating an invoice PDF, and should say so explicitly. Use an **event** when several reactions follow the fact that an order was placed (confirmation email, analytics, loyalty points), none of which the checkout needs to know about; a new reaction is a new listener, not a checkout edit. The costs are Laravel-specific: synchronous listeners run inside the request, and an exception aborts it and skips the rest; a sync listener returning `false` stops propagation; each queued listener is its own job with its own retries; the flow is only visible through `event:list`; and transaction timing needs `ShouldDispatchAfterCommit`.

go deeper

for a junior

Recall that an event lets many listeners react to one fact, while a direct call or a job is a single, explicit piece of work.

for a middle

Explain how synchronous and queued listeners differ at runtime, including exceptions, propagation and ordering.

for a senior

Justify, reaction by reaction, which checkout steps are direct calls, jobs or listeners, and name the failure modes of each choice.

for a principal

Set the team's rule for when domain events are allowed, balancing decoupling against traceability and the cost of indirect control flow.

## Three ways to trigger work When an order is placed, the checkout action can trigger the follow-up work in three ways: - **Direct call** — `$this->inventory->reserve($order)`: the code runs now, in the request, and the caller sees its return value and exceptions. - **Job** — `GenerateInvoice::dispatch($order)`: the caller explicitly asks for one unit of background work. - **Event** — `OrderPlaced::dispatch($order)`: the caller announces a fact; whatever listeners exist react to it, synchronously or through the queue, and the caller does not know who they are. ## Deciding for each reaction Ask these questions about each piece of follow-up work, in order: 1. **Does checkout need the result, or must a failure abort the order?** Then it is a direct call. Payment capture and, when overselling is unacceptable, stock reservation belong here. 2. **Is it one task that checkout itself owns and wants done later?** Then dispatch a job. Naming the job in checkout keeps the dependency visible. 3. **Is it a reaction that other parts of the system own, and would checkout not care if it were added or removed?** Then it is a listener on an event. The confirmation email, the analytics row and the loyalty points are typical. | Question | Direct call | Job | Event with listeners | |---|---|---|---| | Caller needs a result | yes | no | no | | Failure aborts the checkout | yes | no | only for sync listeners | | Number of reactions | one | one | any | | Caller knows the handler | yes | yes | no | | Runs in background | no | yes | per listener | | Adding a reaction | edit checkout | edit checkout | add a listener | ## What Laravel's dispatcher actually does The choice has concrete consequences in Laravel, and interviewers probe them: - **Synchronous listeners run inside the request**, one after another in registration order. An exception from one propagates out of `dispatch()`, fails the request and skips the listeners after it. - **Returning `false`** from a synchronous listener stops propagation to later listeners; a queued listener's return value is ignored. - **Order is not a contract.** With discovery, registration order follows the file scan, so listeners must not depend on each other; if one must run first, it is really part of a sequence and belongs in a direct call or a chained job. - **Each queued listener becomes its own job**, so the email can retry without re-running analytics. That independence is the main operational win over one big job that does everything. - **Timing against transactions matters.** An event fired inside `DB::transaction()` reaches listeners before commit unless the event implements `ShouldDispatchAfterCommit`. - **The control flow is indirect.** Reading the checkout no longer tells you what happens; `php artisan event:list` does, and teams should expect to use it. ## A worked split for the order fan-out Applied to a real checkout, the rules give a split like this: 1. **Payment capture** — direct call; no order without it. 2. **Stock reservation** — direct call when overselling is unacceptable; otherwise a synchronous listener that updates a stock projection. 3. **Invoice PDF** — a job dispatched by checkout, because checkout owns the invoice and wants it done later. 4. **Confirmation email** — queued listener on `OrderPlaced`, so a mail outage never fails checkout. 5. **Analytics row and loyalty points** — queued listeners, each retried on its own. The event carries only the order, and the listeners that need more load it themselves. ## Common anti-patterns - firing an event and then relying on a listener's side effect later in the same request, which couples the caller to a handler it supposedly does not know - putting the payment capture in a listener, so a failure surfaces as a half-completed order - one listener per event that does everything, which is a job with extra indirection - chains of events whose listeners fire further events, making failures hard to trace ## Summary Events are for **fan-out to independent reactions**. When the caller depends on the outcome, call it; when the caller wants one known task done later, dispatch a job; when many parts of the system want to know that an order was placed, fire `OrderPlaced` and let each listener decide whether to run now or on the queue.

  • A synchronous listener on OrderPlaced throws; what does the customer see, and what happens to the other listeners?
    The exception propagates out of `dispatch()` into the checkout action, so unless it is caught the request fails with an error response. Listeners registered after the failing one are never called. If the event was fired inside a transaction, the transaction rolls back too. That is why slow or fragile reactions are queued.
  • Two listeners on OrderPlaced must run in a fixed order; is registration order a safe way to guarantee it?
    No. With discovery, order follows the directory scan, and a manual `Event::listen` elsewhere can reorder or duplicate entries. A hard ordering dependency means the steps are one sequence: call them directly in order, or dispatch a job chain, and keep listeners independent.

Placing the order is like a restaurant kitchen ringing the bell when a dish is ready: the chef does not phone each waiter, dessert station and stock clerk, but anything the chef needs before plating, like the right ingredients, is fetched directly and not announced with the bell.

saying these in an interview costs you the question

  • Listeners always run in the background, so firing an event never slows the request.
  • Payment capture belongs in an OrderPlaced listener to keep the checkout thin.
  • Listener execution order is guaranteed, so later listeners can rely on earlier ones.
  • Returning false from a queued listener stops the remaining listeners.
  • An exception in one synchronous listener is caught so the others still run.