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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping multi-step writes in a transaction ensures the order, stock, and coupon changes all succeed or all roll back together.
  2. 2Locking rows for update prevents two concurrent orders from overselling the same product or coupon.
  3. 3Firing a domain event after the commit-worthy work lets side effects like emails stay decoupled from the core logic.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Placing an order atomically in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code