E-Commerce & Integrations

Building a Scalable E-Commerce Platform with Laravel 12 and Stripe Integration

Build a scalable e-commerce platform with Laravel 12 and Stripe. Complete guide covering product catalog, shopping cart, payment processing, order management, and scalability.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
03-Nov-2025
12 min read
Building a Scalable E-Commerce Platform with Laravel 12 and Stripe Integration

Introduction

Building a robust e-commerce platform requires careful architecture, secure payment processing, and scalability from day one. Laravel 12 provides the perfect foundation with its elegant syntax, built-in features, and extensive ecosystem, while Stripe offers enterprise-grade payment processing with comprehensive API support.

Whether you're building a small online store or a large-scale marketplace, Laravel's modular architecture combined with Stripe's powerful payment infrastructure enables rapid development without compromising security or performance.

In this comprehensive guide, we'll build a production-ready e-commerce platform with Laravel 12 and Stripe, covering product management, shopping cart functionality, secure checkout, payment processing, order management, webhooks, and scalability considerations.


E-Commerce Architecture Overview

Core Components

  1. Product Catalog – Products, categories, variants, inventory
  2. Shopping Cart – Session/database-based cart management
  3. Checkout Flow – Multi-step checkout with validation
  4. Payment Processing – Stripe integration with 3D Secure
  5. Order Management – Order tracking, status updates
  6. Customer Area – Order history, profile management
  7. Admin Dashboard – Inventory, orders, analytics

Technology Stack

  • Backend: Laravel 12, MySQL, Redis
  • Payment: Stripe API, Stripe Webhooks
  • Frontend: Livewire/Inertia.js + Vue/React
  • Queue: Laravel Horizon for async processing
  • Storage: S3 for product images

Database Schema Design

1. Core Tables Migration

// Products table
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description');
    $table->decimal('price', 10, 2);
    $table->decimal('compare_price', 10, 2)->nullable();
    $table->integer('stock_quantity')->default(0);
    $table->string('sku')->unique();
    $table->boolean('is_active')->default(true);
    $table->json('images')->nullable();
    $table->json('metadata')->nullable();
    $table->timestamps();
    $table->softDeletes();
    
    $table->index(['slug', 'is_active']);
});

// Categories table
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->foreignId('parent_id')->nullable()->constrained('categories');
    $table->integer('order')->default(0);
    $table->timestamps();
});

// Product categories pivot
Schema::create('category_product', function (Blueprint $table) {
    $table->foreignId('category_id')->constrained()->cascadeOnDelete();
    $table->foreignId('product_id')->constrained()->cascadeOnDelete();
    $table->primary(['category_id', 'product_id']);
});

// Carts table
Schema::create('carts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->string('session_id')->nullable();
    $table->decimal('subtotal', 10, 2)->default(0);
    $table->decimal('tax', 10, 2)->default(0);
    $table->decimal('total', 10, 2)->default(0);
    $table->timestamps();
    
    $table->index(['user_id', 'session_id']);
});

// Cart items table
Schema::create('cart_items', function (Blueprint $table) {
    $table->id();
    $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
    $table->foreignId('product_id')->constrained();
    $table->integer('quantity')->default(1);
    $table->decimal('price', 10, 2);
    $table->timestamps();
});

// Orders table
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->string('order_number')->unique();
    $table->foreignId('user_id')->constrained();
    $table->enum('status', ['pending', 'processing', 'completed', 'cancelled', 'refunded']);
    $table->decimal('subtotal', 10, 2);
    $table->decimal('tax', 10, 2);
    $table->decimal('shipping', 10, 2);
    $table->decimal('total', 10, 2);
    $table->string('stripe_payment_intent_id')->nullable();
    $table->json('shipping_address');
    $table->json('billing_address');
    $table->text('notes')->nullable();
    $table->timestamps();
    
    $table->index(['user_id', 'status', 'order_number']);
});

// Order items table
Schema::create('order_items', function (Blueprint $table) {
    $table->id();
    $table->foreignId('order_id')->constrained()->cascadeOnDelete();
    $table->foreignId('product_id')->constrained();
    $table->string('product_name');
    $table->string('sku');
    $table->integer('quantity');
    $table->decimal('price', 10, 2);
    $table->decimal('total', 10, 2);
    $table->timestamps();
});

// Payments table
Schema::create('payments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('order_id')->constrained();
    $table->string('stripe_payment_id');
    $table->string('payment_method');
    $table->decimal('amount', 10, 2);
    $table->string('currency', 3)->default('usd');
    $table->enum('status', ['pending', 'succeeded', 'failed', 'refunded']);
    $table->json('metadata')->nullable();
    $table->timestamps();
});

Product Management

1. Product Model

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes;
    
    protected $fillable = [
        'name', 'slug', 'description', 'price', 'compare_price',
        'stock_quantity', 'sku', 'is_active', 'images', 'metadata'
    ];
    
    protected $casts = [
        'price' => 'decimal:2',
        'compare_price' => 'decimal:2',
        'is_active' => 'boolean',
        'images' => 'array',
        'metadata' => 'array',
    ];
    
    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class);
    }
    
    public function scopeActive($query)
    {
        return $query->where('is_active', true)->where('stock_quantity', '>', 0);
    }
    
    public function scopeInStock($query)
    {
        return $query->where('stock_quantity', '>', 0);
    }
    
    public function isInStock(): bool
    {
        return $this->stock_quantity > 0;
    }
    
    public function decrementStock(int $quantity): void
    {
        $this->decrement('stock_quantity', $quantity);
    }
    
    public function getDiscountPercentAttribute(): ?float
    {
        if (!$this->compare_price) {
            return null;
        }
        
        return round((($this->compare_price - $this->price) / $this->compare_price) * 100);
    }
}

2. Product Controller

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        $products = Product::with('categories')
            ->active()
            ->when($request->category, function ($query, $category) {
                $query->whereHas('categories', function ($q) use ($category) {
                    $q->where('slug', $category);
                });
            })
            ->when($request->search, function ($query, $search) {
                $query->where(function ($q) use ($search) {
                    $q->where('name', 'like', "%{$search}%")
                      ->orWhere('description', 'like', "%{$search}%");
                });
            })
            ->orderBy($request->sort ?? 'created_at', $request->order ?? 'desc')
            ->paginate(24);
        
        return view('products.index', compact('products'));
    }
    
    public function show(string $slug)
    {
        $product = Product::with('categories')
            ->where('slug', $slug)
            ->active()
            ->firstOrFail();
        
        // Get related products
        $relatedProducts = Product::active()
            ->whereHas('categories', function ($query) use ($product) {
                $query->whereIn('categories.id', $product->categories->pluck('id'));
            })
            ->where('id', '!=', $product->id)
            ->inRandomOrder()
            ->limit(4)
            ->get();
        
        return view('products.show', compact('product', 'relatedProducts'));
    }
}

Shopping Cart Implementation

1. Cart Service

namespace App\Services;

use App\Models\Cart;
use App\Models\CartItem;
use App\Models\Product;
use Illuminate\Support\Facades\Session;

class CartService
{
    public function getCart()
    {
        if (auth()->check()) {
            return Cart::with('items.product')
                ->firstOrCreate(['user_id' => auth()->id()]);
        }
        
        return Cart::with('items.product')
            ->firstOrCreate(['session_id' => Session::getId()]);
    }
    
    public function addItem(Product $product, int $quantity = 1)
    {
        $cart = $this->getCart();
        
        if (!$product->isInStock()) {
            throw new \Exception('Product is out of stock');
        }
        
        $cartItem = $cart->items()
            ->where('product_id', $product->id)
            ->first();
        
        if ($cartItem) {
            $newQuantity = $cartItem->quantity + $quantity;
            
            if ($newQuantity > $product->stock_quantity) {
                throw new \Exception('Not enough stock available');
            }
            
            $cartItem->update(['quantity' => $newQuantity]);
        } else {
            $cart->items()->create([
                'product_id' => $product->id,
                'quantity' => $quantity,
                'price' => $product->price,
            ]);
        }
        
        $this->recalculateCart($cart);
        
        return $cart;
    }
    
    public function updateQuantity(CartItem $item, int $quantity)
    {
        if ($quantity <= 0) {
            return $this->removeItem($item);
        }
        
        if ($quantity > $item->product->stock_quantity) {
            throw new \Exception('Not enough stock available');
        }
        
        $item->update(['quantity' => $quantity]);
        $this->recalculateCart($item->cart);
        
        return $item->cart;
    }
    
    public function removeItem(CartItem $item)
    {
        $cart = $item->cart;
        $item->delete();
        $this->recalculateCart($cart);
        
        return $cart;
    }
    
    public function clearCart(Cart $cart)
    {
        $cart->items()->delete();
        $this->recalculateCart($cart);
    }
    
    protected function recalculateCart(Cart $cart)
    {
        $cart->load('items.product');
        
        $subtotal = $cart->items->sum(function ($item) {
            return $item->price * $item->quantity;
        });
        
        $tax = $subtotal * 0.1; // 10% tax
        $total = $subtotal + $tax;
        
        $cart->update([
            'subtotal' => $subtotal,
            'tax' => $tax,
            'total' => $total,
        ]);
    }
}

2. Cart Controller

namespace App\Http\Controllers;

use App\Models\Product;
use App\Services\CartService;
use Illuminate\Http\Request;

class CartController extends Controller
{
    public function __construct(
        private CartService $cartService
    ) {}
    
    public function index()
    {
        $cart = $this->cartService->getCart();
        
        return view('cart.index', compact('cart'));
    }
    
    public function add(Request $request, Product $product)
    {
        $request->validate([
            'quantity' => 'required|integer|min:1',
        ]);
        
        try {
            $this->cartService->addItem($product, $request->quantity);
            
            return back()->with('success', 'Product added to cart');
        } catch (\Exception $e) {
            return back()->withErrors(['error' => $e->getMessage()]);
        }
    }
    
    public function update(Request $request, CartItem $item)
    {
        try {
            $this->cartService->updateQuantity($item, $request->quantity);
            
            return back()->with('success', 'Cart updated');
        } catch (\Exception $e) {
            return back()->withErrors(['error' => $e->getMessage()]);
        }
    }
    
    public function remove(CartItem $item)
    {
        $this->cartService->removeItem($item);
        
        return back()->with('success', 'Item removed from cart');
    }
}

Stripe Integration

1. Install Stripe SDK

composer require stripe/stripe-php

2. Stripe Configuration

Add to .env:

STRIPE_KEY=pk_test_your_publishable_key
STRIPE_SECRET=sk_test_your_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret

Configure in config/services.php:

'stripe' => [
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
    'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],

3. Payment Service

namespace App\Services;

use App\Models\Order;
use Stripe\StripeClient;
use Stripe\Exception\ApiErrorException;

class PaymentService
{
    private StripeClient $stripe;
    
    public function __construct()
    {
        $this->stripe = new StripeClient(config('services.stripe.secret'));
    }
    
    public function createPaymentIntent(Order $order): array
    {
        try {
            $paymentIntent = $this->stripe->paymentIntents->create([
                'amount' => $order->total * 100, // Convert to cents
                'currency' => 'usd',
                'metadata' => [
                    'order_id' => $order->id,
                    'order_number' => $order->order_number,
                ],
                'automatic_payment_methods' => [
                    'enabled' => true,
                ],
            ]);
            
            $order->update([
                'stripe_payment_intent_id' => $paymentIntent->id,
            ]);
            
            return [
                'clientSecret' => $paymentIntent->client_secret,
                'paymentIntentId' => $paymentIntent->id,
            ];
        } catch (ApiErrorException $e) {
            throw new \Exception('Payment initialization failed: ' . $e->getMessage());
        }
    }
    
    public function confirmPayment(string $paymentIntentId): bool
    {
        try {
            $paymentIntent = $this->stripe->paymentIntents->retrieve($paymentIntentId);
            
            return $paymentIntent->status === 'succeeded';
        } catch (ApiErrorException $e) {
            return false;
        }
    }
    
    public function refundPayment(Order $order, float $amount = null): bool
    {
        try {
            $refund = $this->stripe->refunds->create([
                'payment_intent' => $order->stripe_payment_intent_id,
                'amount' => $amount ? $amount * 100 : null,
            ]);
            
            return $refund->status === 'succeeded';
        } catch (ApiErrorException $e) {
            throw new \Exception('Refund failed: ' . $e->getMessage());
        }
    }
}

Checkout Process

1. Order Service

namespace App\Services;

use App\Models\Cart;
use App\Models\Order;
use Illuminate\Support\Str;

class OrderService
{
    public function createFromCart(Cart $cart, array $shippingAddress, array $billingAddress): Order
    {
        $order = Order::create([
            'order_number' => $this->generateOrderNumber(),
            'user_id' => auth()->id(),
            'status' => 'pending',
            'subtotal' => $cart->subtotal,
            'tax' => $cart->tax,
            'shipping' => 0, // Calculate shipping
            'total' => $cart->total,
            'shipping_address' => $shippingAddress,
            'billing_address' => $billingAddress,
        ]);
        
        foreach ($cart->items as $item) {
            $order->items()->create([
                'product_id' => $item->product_id,
                'product_name' => $item->product->name,
                'sku' => $item->product->sku,
                'quantity' => $item->quantity,
                'price' => $item->price,
                'total' => $item->price * $item->quantity,
            ]);
            
            // Decrement stock
            $item->product->decrementStock($item->quantity);
        }
        
        return $order;
    }
    
    protected function generateOrderNumber(): string
    {
        return 'ORD-' . strtoupper(Str::random(10));
    }
    
    public function updateStatus(Order $order, string $status): void
    {
        $order->update(['status' => $status]);
        
        // Send notification
        $order->user->notify(new OrderStatusUpdated($order));
    }
}

2. Checkout Controller

namespace App\Http\Controllers;

use App\Services\{CartService, OrderService, PaymentService};
use Illuminate\Http\Request;

class CheckoutController extends Controller
{
    public function __construct(
        private CartService $cartService,
        private OrderService $orderService,
        private PaymentService $paymentService
    ) {}
    
    public function index()
    {
        $cart = $this->cartService->getCart();
        
        if ($cart->items->isEmpty()) {
            return redirect()->route('cart.index')
                ->with('error', 'Your cart is empty');
        }
        
        return view('checkout.index', compact('cart'));
    }
    
    public function process(Request $request)
    {
        $validated = $request->validate([
            'shipping_address' => 'required|array',
            'billing_address' => 'required|array',
        ]);
        
        $cart = $this->cartService->getCart();
        
        // Create order
        $order = $this->orderService->createFromCart(
            $cart,
            $validated['shipping_address'],
            $validated['billing_address']
        );
        
        // Create payment intent
        $paymentIntent = $this->paymentService->createPaymentIntent($order);
        
        // Clear cart
        $this->cartService->clearCart($cart);
        
        return view('checkout.payment', [
            'order' => $order,
            'clientSecret' => $paymentIntent['clientSecret'],
        ]);
    }
    
    public function success(Order $order)
    {
        // Verify payment
        if ($this->paymentService->confirmPayment($order->stripe_payment_intent_id)) {
            $this->orderService->updateStatus($order, 'processing');
            
            return view('checkout.success', compact('order'));
        }
        
        return redirect()->route('checkout.index')
            ->with('error', 'Payment verification failed');
    }
}

Stripe Webhooks

1. Webhook Controller

namespace App\Http\Controllers;

use App\Models\Order;
use App\Services\OrderService;
use Illuminate\Http\Request;
use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;

class StripeWebhookController extends Controller
{
    public function __construct(
        private OrderService $orderService
    ) {}
    
    public function handle(Request $request)
    {
        $payload = $request->getContent();
        $signature = $request->header('Stripe-Signature');
        
        try {
            $event = Webhook::constructEvent(
                $payload,
                $signature,
                config('services.stripe.webhook_secret')
            );
        } catch (SignatureVerificationException $e) {
            return response()->json(['error' => 'Invalid signature'], 400);
        }
        
        switch ($event->type) {
            case 'payment_intent.succeeded':
                $this->handlePaymentSucceeded($event->data->object);
                break;
                
            case 'payment_intent.payment_failed':
                $this->handlePaymentFailed($event->data->object);
                break;
                
            case 'charge.refunded':
                $this->handleRefund($event->data->object);
                break;
        }
        
        return response()->json(['status' => 'success']);
    }
    
    protected function handlePaymentSucceeded($paymentIntent)
    {
        $order = Order::where('stripe_payment_intent_id', $paymentIntent->id)->first();
        
        if ($order) {
            $order->payments()->create([
                'stripe_payment_id' => $paymentIntent->id,
                'payment_method' => $paymentIntent->payment_method,
                'amount' => $paymentIntent->amount / 100,
                'currency' => $paymentIntent->currency,
                'status' => 'succeeded',
            ]);
            
            $this->orderService->updateStatus($order, 'processing');
        }
    }
    
    protected function handlePaymentFailed($paymentIntent)
    {
        $order = Order::where('stripe_payment_intent_id', $paymentIntent->id)->first();
        
        if ($order) {
            $this->orderService->updateStatus($order, 'cancelled');
        }
    }
    
    protected function handleRefund($charge)
    {
        $order = Order::where('stripe_payment_intent_id', $charge->payment_intent)->first();
        
        if ($order) {
            $this->orderService->updateStatus($order, 'refunded');
        }
    }
}

2. Register Webhook Route

// routes/web.php
Route::post('/stripe/webhook', [StripeWebhookController::class, 'handle'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

Scalability Considerations

1. Queue Jobs for Processing

// Process order asynchronously
dispatch(new ProcessOrder($order));

// Send order confirmation email
dispatch(new SendOrderConfirmation($order));

// Update inventory
dispatch(new UpdateInventory($orderItems));

2. Database Optimization

// Add indexes for frequently queried columns
Schema::table('products', function (Blueprint $table) {
    $table->index(['slug', 'is_active']);
    $table->index('stock_quantity');
});

Schema::table('orders', function (Blueprint $table) {
    $table->index(['user_id', 'status']);
    $table->index('order_number');
});

3. Cache Product Catalog

$products = Cache::remember('products:featured', 3600, function () {
    return Product::active()
        ->with('categories')
        ->limit(12)
        ->get();
});

4. Load Balancing

upstream laravel_backend {
    least_conn;
    server app1.example.com:9000;
    server app2.example.com:9000;
    server app3.example.com:9000;
}

Conclusion

Building a scalable e-commerce platform with Laravel 12 and Stripe provides enterprise-grade payment processing, robust order management, and the flexibility to scale as your business grows. By implementing proper database design, service-oriented architecture, asynchronous processing, and following Laravel best practices, you create a foundation for sustainable e-commerce growth.

Key takeaways:

  • Design normalized database schema for products and orders
  • Implement service layer for business logic separation
  • Integrate Stripe for secure payment processing
  • Handle webhooks for payment status updates
  • Use queues for asynchronous order processing
  • Implement caching for improved performance
  • Plan for horizontal scaling from the start

Need help building your e-commerce platform? NeedLaravelSite specializes in custom e-commerce development with Laravel. Contact us for expert Laravel development services.


Related Resources:


Article Tags

Laravel e-commerce Laravel Stripe integration Laravel 12 e-commerce Laravel payment gateway Laravel shopping cart Laravel online store Laravel checkout Stripe payment Laravel Laravel e-commerce platform Laravel order management Scalable e-commerce Laravel

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products