php
63 lines · 8 steps
Placing an order atomically in Laravel
A service wraps order creation, stock deduction, and coupon redemption in one retryable database transaction.
Explained by
highlit
1<?php
2
3namespace App\Services;
4
5use App\Models\Order;
6use App\Models\Product;
7use App\Exceptions\InsufficientStockException;
8use App\Events\OrderPlaced;
9use Illuminate\Support\Facades\DB;
10
11class OrderCreator
12{
13 public function place(array $items, int $customerId, string $couponCode = null): Order
14 {
15 return DB::transaction(function () use ($items, $customerId, $couponCode) {
16 $order = Order::create([
17 'customer_id' => $customerId,
18 'status' => 'pending',
19 'total' => 0,
20 ]);
21
22 $total = 0;
23
24 foreach ($items as $item) {
25 $product = Product::lockForUpdate()->findOrFail($item['product_id']);
26
27 if ($product->stock < $item['quantity']) {
28 throw new InsufficientStockException($product->id);
29 }
30
31 $product->decrement('stock', $item['quantity']);
32
33 $order->lines()->create([
34 'product_id' => $product->id,
35 'quantity' => $item['quantity'],
36 'unit_price' => $product->price,
37 ]);
38
39 $total += $product->price * $item['quantity'];
40 }
41
42 if ($couponCode) {
43 $coupon = $this->redeemCoupon($couponCode, $order);
44 $total -= $coupon->discountFor($total);
45 }
46
47 $order->update(['total' => $total, 'status' => 'confirmed']);
48
49 event(new OrderPlaced($order));
50
51 return $order;
52 }, 3);
53 }
54
55 protected function redeemCoupon(string $code, Order $order): Coupon
56 {
57 $coupon = Coupon::where('code', $code)->active()->lockForUpdate()->firstOrFail();
58 $coupon->decrement('remaining');
59 $coupon->redemptions()->create(['order_id' => $order->id]);
60
61 return $coupon;
62 }
63}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping multi-step writes in a transaction ensures the order, stock, and coupon changes all succeed or all roll back together.
- 2Locking rows for update prevents two concurrent orders from overselling the same product or coupon.
- 3Firing a domain event after the commit-worthy work lets side effects like emails stay decoupled from the core logic.
Related explainers
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
php
<?php namespace App\Http\Controllers;
Streaming a filtered CSV export in Laravel
streaming
csv-export
query-builder
Intermediate
9 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/placing-an-order-atomically-in-laravel-explained-php-1586/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.