Building a Production-Grade E-Commerce Platform with Laravel 12, Stripe, and Kubernetes - Part 1: Architecture, Setup & Foundations - NextGenBeing Building a Production-Grade E-Commerce Platform with Laravel 12, Stripe, and Kubernetes - Part 1: Architecture, Setup & Foundations - NextGenBeing
Back to discoveries
Part 1 of 8

Building a Production-Grade E-Commerce Platform with Laravel 12, Stripe, and Kubernetes - Part 1: Architecture, Setup & Foundations

**Estimated Reading Time:** 22 minutes | **Difficulty:** Intermediate to Advanced...

Comprehensive Tutorials 6 min read
Daniel Hartwell

Daniel Hartwell

May 12, 2026 4 views
Building a Production-Grade E-Commerce Platform with Laravel 12, Stripe, and Kubernetes - Part 1: Architecture, Setup & Foundations
Size:
Height:
📖 6 min read 📝 11,569 words 👁 Focus mode: ✨ Eye care:

Listen to Article

Loading...
0:00 / 0:00
0:00 0:00
Low High
0% 100%
⏸ Paused ▶️ Now playing... Ready to play ✓ Finished

Building a Production-Grade E-Commerce Platform with Laravel 12, Stripe, and Kubernetes - Part 1: Architecture, Setup & Foundations

Estimated Reading Time: 22 minutes | Difficulty: Intermediate to Advanced


Table of Contents

  1. Why Build E-Commerce on Kubernetes?
  2. Architecture Overview
  3. Technology Stack Decisions
  4. Local Development Environment Setup
  5. Project Scaffolding and Structure
  6. Database Architecture and Migrations
  7. Docker Configuration for Local Development
  8. First Working API Endpoint
  9. Common Setup Mistakes
  10. What's Next

Why Build E-Commerce on Kubernetes?

After deploying e-commerce platforms that handle hundreds of thousands of transactions monthly, I've learned that the technology choices you make on day one determine your operational overhead for years. Let's be clear: Kubernetes is not for every project. If you're building an MVP or expect fewer than 1,000 orders per day, a simple Laravel Forge deployment with a managed database will serve you better.

However, if you're targeting:

  • Multi-region deployment for global latency optimization
  • Horizontal scaling during traffic spikes (Black Friday, flash sales)
  • Zero-downtime deployments with canary releases
  • Service isolation where payment processing, inventory, and order management scale independently

Then Kubernetes becomes the foundation that prevents costly rewrites later.

The Real-World Context

In 2023, I worked on migrating an e-commerce platform from a monolithic Laravel deployment on AWS EC2 to a Kubernetes-based architecture. The system handled approximately 15,000 orders daily with peaks reaching 3,000 orders per hour during promotional campaigns. The monolithic approach required vertical scaling (upgrading to larger EC2 instances), which meant:

  1. Downtime during instance resizing
  2. Over-provisioning for peak capacity (paying for unused resources 90% of the time)
  3. Database connection pool exhaustion when traffic spiked
  4. No ability to scale individual services (image processing always bottlenecked checkout)

The Kubernetes migration solved these issues, but it introduced operational complexity. This series documents the production-ready patterns we developed.


Architecture Overview

Our e-commerce platform follows a modular monolith approach—a single Laravel application organized into domain-driven modules that can later be extracted into microservices if needed. This is the sweet spot for most teams: you get the simplicity of a monolith with the organizational clarity of services.

High-Level Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                     Cloudflare CDN                          │
│              (Static Assets + DDoS Protection)              │
└───────────────────────┬─────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────────────┐
│              Kubernetes Ingress Controller                  │
│                  (NGINX Ingress)                            │
└───────────┬─────────────────────────────────┬───────────────┘
            │                                 │
            ▼                                 ▼
┌───────────────────────┐         ┌───────────────────────┐
│   Laravel API Pods    │         │   Laravel Queue       │
│   (Stateless)         │         │   Workers (Stateless) │
│   • Product Catalog   │         │   • Order Processing  │
│   • Cart Management   │         │   • Email Dispatch    │
│   • Checkout API      │         │   • Payment Webhooks  │
└───────┬───────────────┘         └───────────┬───────────┘
        │                                     │
        └─────────────────┬───────────────────┘
                          │
        ┌─────────────────┼─────────────────┬──────────────┐
        │                 │                 │              │
        ▼                 ▼                 ▼              ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────┐
│  PostgreSQL  │  │    Redis     │  │   Stripe     │  │   S3     │
│  (Primary +  │  │  (Sessions,  │  │   Payment    │  │  Product │
│   Replicas)  │  │   Cache,     │  │   Gateway    │  │  Images  │
│              │  │   Queues)    │  │              │  │          │
└──────────────┘  └──────────────┘  └──────────────┘  └──────────┘

Service Breakdown

API Pods (Horizontal Scaling: 2-20 instances)

  • Handles HTTP requests for browsing, cart, checkout
  • Stateless design: no session data stored locally
  • Autoscales based on CPU and request latency

Queue Worker Pods (Horizontal Scaling: 1-10 instances)

  • Processes asynchronous jobs (order confirmation emails, inventory updates)
  • Consumes Redis-backed Laravel queues
  • Isolated from web traffic to prevent job delays during traffic spikes

Database Layer

  • PostgreSQL for transactional data (orders, users, products)
  • Read replicas for product catalog queries
  • Redis for session storage, cache, and queue backend

External Services

  • Stripe for payment processing (PCI compliance offloaded)
  • S3 for product images and invoices
  • Cloudflare for CDN and DDoS protection

Technology Stack Decisions

Here's what we're building with and why these specific choices matter:

Component Technology Version Why This Choice
Framework Laravel 12.x Best-in-class ORM, queue system, and ecosystem. Version 12 includes native async support.
Language PHP 8.3+ Required for Laravel 12. Opcache + JIT provides 15-30% performance gains over PHP 8.1.
Database PostgreSQL 16.x JSONB support for flexible product attributes, superior concurrency control vs MySQL for high-write workloads.
Cache/Queue Redis 7.2+ Persistence for queue jobs, sub-millisecond cache reads.
Payment Stripe API 2024-04-10 Industry-leading developer experience, handles PCI compliance, supports 135+ currencies.
Container Runtime Docker 24.x+ Development parity with production, layer caching speeds up builds.
Orchestration Kubernetes 1.28+ EKS/GKE support, mature autoscaling, service mesh integration.
HTTP Server PHP-FPM + NGINX - FPM process management prevents memory leaks, NGINX serves static assets directly.

Alternative Approaches Considered

Why Not Node.js/NestJS? Laravel's Eloquent ORM and built-in payment abstractions would take months to replicate in Node. For teams already proficient in PHP, Laravel's productivity advantage is significant.

Why Not MySQL? PostgreSQL's JSONB columns and partial indexes are critical for our product catalog, which needs flexible attributes (clothing has sizes, electronics have specs) without EAV anti-patterns.

Why Not Serverless (Lambda/Cloud Run)? Cold start latency (200-500ms) is unacceptable for checkout flows. Kubernetes provides predictable performance with warm instances.


Local Development Environment Setup

We'll create a development environment that mirrors production as closely as possible using Docker and Kubernetes (via Minikube or Docker Desktop's Kubernetes).

Prerequisites

Ensure you have these installed (check with the commands shown):

$ php --version
PHP 8.3.2 (cli) (built: Jan 16 2024 13:46:41) (NTS)

$ composer --version
Composer version 2.7.1 2024-02-09 15:26:28

$ docker --version
Docker version 24.0.7, build afdd53b

$ kubectl version --client
Client Version: v1.28.4

If any are missing:

# macOS
brew install php@8.3 composer docker docker-compose kubectl minikube

# Ubuntu/Debian
sudo apt update
sudo apt install -y php8.3-cli php8.3-fpm php8.3-pgsql php8.3-redis \
  php8.3-mbstring php8.3-xml php8.3-curl composer docker.io

# Install kubectl
curl -LO "https://dl.k8s.io/release/v1.28.4/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

Creating the Laravel Project

We'll start with a fresh Laravel 12 installation. Important: Do not use Laravel Sail for this project—we're building custom Docker configurations optimized for production deployment.

# Create project directory
mkdir laravel-k8s-ecommerce && cd laravel-k8s-ecommerce

# Install Laravel 12 with Composer
composer create-project laravel/laravel:^12.0 .

# Verify installation
php artisan --version
# Output: Laravel Framework 12.0.0

Project Scaffolding and Structure

We'll organize code using a domain-driven structure that groups related functionality. This differs from Laravel's default structure and prevents the common "fat models, anemic controllers" anti-pattern.

Directory Structure

Create this structure in your Laravel project:

app/
├── Domain/
│   ├── Product/
│   │   ├── Models/
│   │   │   ├── Product.php
│   │   │   └── ProductVariant.php
│   │   ├── Services/
│   │   │   └── ProductCatalogService.php
│   │   ├── Repositories/
│   │   │   └── ProductRepository.php
│   │   └── Events/
│   │       └── ProductCreated.php
│   ├── Order/
│   │   ├── Models/
│   │   │   ├── Order.php
│   │   │   └── OrderItem.php
│   │   ├── Services/
│   │   │   ├── OrderService.php
│   │   │   └── PaymentService.php
│   │   └── Jobs/
│   │       └── ProcessOrderPayment.php
│   ├── Cart/
│   │   ├── Models/
│   │   │   └── Cart.php
│   │   └── Services/
│   │       └── CartService.php
│   └── User/
│       ├── Models/
│       │   └── User.php
│       └── Services/
│           └── CustomerService.php
├── Http/
│   └── Controllers/
│       └── Api/
│           ├── ProductController.php
│           ├── CartController.php
│           └── OrderController.php
└── Infrastructure/
    ├── Stripe/
    │   └── StripePaymentGateway.php
    └── Cache/
        └── ProductCacheWarmer.php

Create the directory structure:

mkdir -p app/Domain/{Product,Order,Cart,User}/{Models,Services,Repositories,Events,Jobs}
mkdir -p app/Http/Controllers/Api
mkdir -p app/Infrastructure/{Stripe,Cache}

Configuration Management

Laravel's .env file is insufficient for production. We'll use a hybrid approach:

config/ecommerce.php (version-controlled defaults):

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | E-Commerce Platform Configuration
    |--------------------------------------------------------------------------
    |
    | Core business logic configuration that should be version-controlled.
    | Sensitive values (API keys) are still pulled from environment variables.
    |
    */

    'currency' => [
        'default' => env('ECOMMERCE_CURRENCY', 'USD'),
        'supported' => ['USD', 'EUR', 'GBP', 'CAD'],
    ],

    'inventory' => [
        // Reserve inventory when added to cart (prevents overselling)
        'reserve_on_cart_add' => env('INVENTORY_RESERVE_ON_ADD', true),
        
        // Minutes to hold inventory reservation
        'reservation_ttl' => env('INVENTORY_RESERVATION_TTL', 15),
        
        // Enable real-time inventory sync with external warehouse
        'enable_warehouse_sync' => env('INVENTORY_WAREHOUSE_SYNC', false),
    ],

    'checkout' => [
        // Redirect to this URL after successful payment
        'success_url' => env('CHECKOUT_SUCCESS_URL', '/order/confirmation'),
        
        // Redirect to this URL on payment failure
        'cancel_url' => env('CHECKOUT_CANCEL_URL', '/cart'),
        
        // Maximum time (seconds) to complete checkout before session expires
        'session_timeout' => env('CHECKOUT_SESSION_TIMEOUT', 1800),
    ],

    'stripe' => [
        'api_key' => env('STRIPE_SECRET_KEY'),
        'publishable_key' => env('STRIPE_PUBLISHABLE_KEY'),
        'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
        
        // Use Stripe test mode in non-production environments
        'test_mode' => env('STRIPE_TEST_MODE', !app()->environment('production')),
        
        // API version pinning for predictable behavior
        'api_version' => '2024-04-10',
    ],

    'performance' => [
        // Cache product catalog queries (seconds)
        'catalog_cache_ttl' => env('CATALOG_CACHE_TTL', 3600),
        
        // Enable Redis-based session storage
        'redis_sessions' => env('SESSION_DRIVER', 'redis') === 'redis',
        
        // Maximum products per catalog page (prevent memory exhaustion)
        'max_products_per_page' => 100,
    ],
];

Why this structure?

  • Business logic defaults (currency, inventory rules) are version-controlled so all developers share the same config
  • Sensitive values (API keys) still come from environment variables
  • Configuration is typed and documented with comments
  • Easy to override via environment variables in Kubernetes ConfigMaps

Database Architecture and Migrations

E-commerce databases have specific challenges: high-read product catalogs and high-write order processing. Our schema optimizes for both.

Environment Configuration

Update .env to use PostgreSQL:

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=laravel_ecommerce
DB_USERNAME=laravel
DB_PASSWORD=secret

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

Core Database Migrations

Products Migration (database/migrations/2024_01_01_000001_create_products_table.php):

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * Why these design choices:
     * - JSONB for attributes: Flexible schema for different product types
     * - Partial index on active products: 90% of queries filter by status
     * - GIN index on attributes: Fast JSONB queries like "color = 'red'"
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('sku', 100)->unique();
            $table->string('name');
            $table->text('description')->nullable();
            $table->string('slug')->unique();
            
            // Pricing stored in cents to avoid floating-point errors
            // Example: $19.99 stored as 1999
            $table->integer('price_cents')->unsigned();
            $table->string('currency', 3)->default('USD');
            
            // JSONB for flexible product attributes
            // Example: {"color": "red", "size": "XL", "material": "cotton"}
            $table->jsonb('attributes')->nullable();
            
            // Inventory tracking
            $table->integer('stock_quantity')->default(0);
            $table->integer('reserved_quantity')->default(0);
            
            // Soft deletes for order history preservation
            $table->enum('status', ['draft', 'active', 'discontinued'])->default('draft');
            
            $table->timestamps();
            $table->softDeletes();
            
            // Indexes
            $table->index('status');
            $table->index('slug');
        });

        // Create partial index for active products (PostgreSQL-specific)
        // This index is much smaller and faster than indexing all products
        DB::statement('CREATE INDEX products_active_idx ON products (status) WHERE status = \'active\'');
        
        // GIN index for JSONB attribute queries
        DB::statement('CREATE INDEX products_attributes_idx ON products USING GIN (attributes)');
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Orders Migration (database/migrations/2024_01_01_000002_create_orders_table.php):

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * Design notes:
     * - Denormalized customer data: Prevents issues if user updates profile after order
     * - Stripe payment intent stored: Enables refunds and payment tracking
     * - Order items in separate table: Enables order-level promotions
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->string('order_number', 20)->unique();
            
            // User relationship (nullable for guest checkout)
            $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
            
            // Denormalized customer data (snapshot at order time)
            $table->string('customer_email');
            $table->string('customer_name');
            $table->jsonb('shipping_address');
            $table->jsonb('billing_address');
            
            // Order totals (stored in cents)
            $table->integer('subtotal_cents')->unsigned();
            $table->integer('tax_cents')->unsigned()->default(0);
            $table->integer('shipping_cents')->unsigned()->default(0);
            $table->integer('discount_cents')->unsigned()->default(0);
            $table->integer('total_cents')->unsigned();
            $table->string('currency', 3);
            
            // Payment tracking
            $table->string('stripe_payment_intent_id')->nullable()->unique();
            $table->enum('payment_status', [
                'pending',
                'processing',
                'completed',
                'failed',
                'refunded'
            ])->default('pending');
            
            // Order lifecycle
            $table->enum('fulfillment_status', [
                'unfulfilled',
                'partially_fulfilled',
                'fulfilled',
                'shipped',
                'delivered'
            ])->default('unfulfilled');
            
            $table->timestamp('paid_at')->nullable();
            $table->timestamp('fulfilled_at')->nullable();
            $table->timestamp('shipped_at')->nullable();
            $table->timestamps();
            
            // Indexes for common queries
            $table->index('order_number');
            $table->index('customer_email');
            $table->index('payment_status');
            $table->index(['created_at', 'payment_status']); // Composite index for reporting
        });

        Schema::create('order_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->restrictOnDelete();
            
            // Snapshot product data at order time (price may change later)
            $table->string('product_name');
            $table->string('product_sku');
            $table->jsonb('product_attributes')->nullable(); // Size, color, etc.
            
            $table->integer('quantity')->unsigned();
            $table->integer('unit_price_cents')->unsigned();
            $table->integer('total_price_cents')->unsigned();
            
            $table->timestamps();
            
            $table->index('order_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('order_items');
        Schema::dropIfExists('orders');
    }
};

Cart Migration (database/migrations/2024_01_01_000003_create_carts_table.php):

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Cart implementation strategy:
     * - Redis for active sessions (fast, auto-expiration)
     * - PostgreSQL for persistent carts (logged-in users, cart recovery)
     * 
     * This migration creates the PostgreSQL persistent cart.
     * Active session carts are stored in Redis with TTL.
     */
    public function up(): void
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
            $table->string('session_id')->nullable()->index();
            
            // Cart metadata
            $table->timestamp('last_activity_at')->useCurrent();
            $table->timestamp('expires_at')->nullable();
            
            $table->timestamps();
            
            // Ensure one cart per user or session
            $table->unique(['user_id']);
            $table->unique(['session_id']);
        });

        Schema::create('cart_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            
            $table->integer('quantity')->unsigned()->default(1);
            
            // Store selected variant attributes (size, color, etc.)
            $table->jsonb('selected_attributes')->nullable();
            
            // Inventory reservation tracking
            $table->string('reservation_id')->nullable()->unique();
            $table->timestamp('reserved_until')->nullable();
            
            $table->timestamps();
            
            // Prevent duplicate products in same cart
            $table->unique(['cart_id', 'product_id', 'selected_attributes']);
            
            $table->index('reservation_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('cart_items');
        Schema::dropIfExists('carts');
    }
};

Running Migrations

Start a local PostgreSQL instance with Docker and run migrations:

# Start PostgreSQL
docker run --name postgres-dev \
  -e POSTGRES_DB=laravel_ecommerce \
  -e POSTGRES_USER=laravel \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 \
  -d postgres:16

# Start Redis
docker run --name redis-dev \
  -p 6379:6379 \
  -d redis:7.2

# Run migrations
php artisan migrate

# Expected output:
# 2024_01_01_000001_create_products_table .............. 142ms DONE
# 2024_01_01_000002_create_orders_table ................ 98ms DONE
# 2024_01_01_000003_create_carts_table ................. 76ms DONE

Verify the schema:

# Connect to PostgreSQL
docker exec -it postgres-dev psql -U laravel -d laravel_ecommerce

# List tables
\dt

# Output:
#  Schema |       Name        | Type  |  Owner  
# --------+-------------------+-------+---------
#  public | carts             | table | laravel
#  public | cart_items        | table | laravel
#  public | orders            | table | laravel
#  public | order_items       | table | laravel
#  public | products          | table | laravel

# Inspect indexes
\d products

# Verify partial index exists
SELECT indexdef FROM pg_indexes WHERE tablename = 'products' AND indexname = 'products_active_idx';

Docker Configuration for Local Development

We'll create a multi-stage Dockerfile that optimizes for both development (fast rebuilds) and production (minimal image size).

Project Dockerfile

Dockerfile (place in project root):

# syntax=docker/dockerfile:1.4

# ============================================================================
# Base Stage: Shared dependencies for all stages
# ============================================================================
FROM php:8.3-fpm-alpine AS base

# Install system dependencies and PHP extensions
# Why alpine? 5x smaller than debian-based images (40MB vs 200MB)
RUN apk add --no-cache \
    postgresql-dev \
    libzip-dev \
    libpng-dev \
    libjpeg-turbo-dev \
    freetype-dev \
    icu-dev \
    oniguruma-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) \
        pdo_pgsql \
        pgsql \
        zip \
        gd \
        intl \
        mbstring \
        opcache \
        pcntl

# Install Redis extension via PECL
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
    && pecl install redis-6.0.2 \
    && docker-php-ext-enable redis \
    && apk del .build-deps

# Install Composer from official image
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

# ============================================================================
# Development Stage: Hot-reloading, debugging tools
# ============================================================================
FROM base AS development

# Install Xdebug for step-debugging
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
    && pecl install xdebug-3.3.1 \
    && docker-php-ext-enable xdebug \
    && apk del .build-deps

# PHP configuration for development
COPY <<EOF /usr/local/etc/php/conf.d/99-development.ini
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
xdebug.mode = debug
xdebug.start_with_request = yes
xdebug.client_host = host.docker.internal
xdebug.client_port = 9003
opcache.enable = 0
EOF

# Copy application code (will be overridden by volume mount in docker-compose)
COPY . .

# Install Composer dependencies with dev packages
RUN composer install --no-interaction --no-progress --optimize-autoloader

# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 9000

CMD ["php-fpm"]

# ============================================================================
# Production Dependencies Stage: Optimized vendor folder
# ============================================================================
FROM base AS production-dependencies

COPY composer.json composer.lock ./

# Install production dependencies only (no dev packages, optimized autoloader)
# --no-scripts prevents post-install scripts that require full app
RUN composer install \
    --no-dev \
    --no-interaction \
    --no-progress \
    --no-scripts \
    --prefer-dist \
    --optimize-autoloader \
    --classmap-authoritative

# ============================================================================
# Production Assets Stage: Frontend build
# ============================================================================
FROM node:20-alpine AS production-assets

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --production=false

COPY resources ./resources
COPY vite.config.js tailwind.config.js postcss.config.js ./

RUN npm run build

# ============================================================================
# Production Stage: Minimal runtime image
# ============================================================================
FROM base AS production

# PHP configuration optimized for production
COPY <<EOF /usr/local/etc/php/conf.d/99-production.ini
; Error handling - log to stderr for container log aggregation
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /proc/self/fd/2

; Opcache configuration for maximum performance
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.fast_shutdown = 1

; Realpath cache (reduces filesystem lookups)
realpath_cache_size = 4096k
realpath_cache_ttl = 600

; Session configuration
session.save_handler = redis
session.save_path = "tcp://redis:6379"

; Security hardening
expose_php = Off
allow_url_fopen = Off
EOF

# Copy optimized dependencies from build stage
COPY --chown=www-data:www-data --from=production-dependencies /var/www/html/vendor ./vendor

# Copy application code
COPY --chown=www-data:www-data . .

# Copy built frontend assets
COPY --chown=www-data:www-data --from=production-assets /app/public/build ./public/build

# Generate optimized configuration cache
# Why: Reduces app boot time by ~30% in production
RUN php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

# Security: Run as non-root user
USER www-data

EXPOSE 9000

# Use exec form to ensure proper signal handling
CMD ["php-fpm"]

NGINX Configuration for Serving Laravel

docker/nginx/default.conf:

# nginx configuration optimized for Laravel + Kubernetes
server {
    listen 80;
    server_name _;
    root /var/www/html/public;

    index index.php;

    charset utf-8;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Kubernetes health check endpoint
    # Responds instantly without hitting PHP-FPM
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }

    # Serve static assets directly (bypass PHP-FPM)
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Laravel application
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM processing
    location ~ \.php$ {
        fastcgi_pass laravel-app:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        
        # Prevent caching of PHP responses
        fastcgi_param HTTP_CACHE_CONTROL "no-store, no-cache, must-revalidate";
        
        # Increase timeouts for long-running operations
        fastcgi_read_timeout 300s;
        
        # Hide PHP version in response headers
        fastcgi_hide_header X-Powered-By;
    }

    # Block access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Custom error pages
    error_page 404 /index.php;
}

Docker Compose for Local Development

docker-compose.yml:

version: '3.8'

services:
  # Laravel PHP-FPM application
  app:
    build:
      context: .
      target: development
      dockerfile: Dockerfile
    container_name: laravel-app
    volumes:
      # Mount source code for hot-reloading
      - .:/var/www/html
      # Exclude vendor (use container's version for speed)
      - /var/www/html/vendor
      # Named volume for storage (persists uploads)
      - storage-data:/var/www/html/storage/app
    environment:
      - APP_ENV=local
      - APP_DEBUG=true
      - DB_CONNECTION=pgsql
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_DATABASE=laravel_ecommerce
      - DB_USERNAME=laravel
      - DB_PASSWORD=secret
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - CACHE_DRIVER=redis
      - QUEUE_CONNECTION=redis
      - SESSION_DRIVER=redis
    depends_on:
      - postgres
      - redis
    networks:
      - ecommerce-network

  # NGINX web server
  nginx:
    image: nginx:1.25-alpine
    container_name: laravel-nginx
    ports:
      - "8000:80"
    volumes:
      - .:/var/www/html:ro
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
    networks:
      - ecommerce-network

  # PostgreSQL database
  postgres:
    image: postgres:16-alpine
    container_name: laravel-postgres
    environment:
      POSTGRES_DB: laravel_ecommerce
      POSTGRES_USER: laravel
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - ecommerce-network

  # Redis cache and queue
  redis:
    image: redis:7.2-alpine
    container_name: laravel-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - ecommerce-network

  # Queue worker for background jobs
  queue-worker:
    build:
      context: .
      target: development
      dockerfile: Dockerfile
    container_name: laravel-queue
    command: php artisan queue:work --tries=3 --timeout=90 --sleep=3
    volumes:
      - .:/var/www/html
      - /var/www/html/vendor
    environment:
      - APP_ENV=local
      - DB_CONNECTION=pgsql
      - DB_HOST=postgres
      - REDIS_HOST=redis
      - QUEUE_CONNECTION=redis
    depends_on:
      - postgres
      - redis
    networks:
      - ecommerce-network
    restart: unless-stopped

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local
  storage-data:
    driver: local

networks:
  ecommerce-network:
    driver: bridge

Starting the Development Environment

# Build and start all services
docker-compose up -d --build

# Expected output:
# [+] Building 34.2s (19/19) FINISHED
# [+] Running 6/6
#  ✔ Network laravel-k8s-ecommerce_ecommerce-network  Created
#  ✔ Container laravel-postgres                       Started
#  ✔ Container laravel-redis                          Started
#  ✔ Container laravel-app                            Started
#  ✔ Container laravel-nginx                          Started
#  ✔ Container laravel-queue                          Started

# Run migrations
docker-compose exec app php artisan migrate

# Check service status
docker-compose ps

# Output:
# NAME               COMMAND                  SERVICE   STATUS    PORTS
# laravel-app        "docker-php-entrypoi…"   app       running   9000/tcp
# laravel-nginx      "/docker-entrypoint.…"   nginx     running   0.0.0.0:8000->80/tcp
# laravel-postgres   "docker-entrypoint.s…"   postgres  running   0.0.0.0:5432->5432/tcp
# laravel-queue      "docker-php-entrypoi…"   queue     running   9000/tcp
# laravel-redis      "docker-entrypoint.s…"   redis     running   0.0.0.0:6379->6379/tcp

# Test the application
curl http://localhost:8000/health

# Expected output:
# healthy

First Working API Endpoint

Let's build a complete, production-ready endpoint for product catalog retrieval. This will demonstrate the entire stack working together.

Product Model

app/Domain/Product/Models/Product.php:

<?php

namespace App\Domain\Product\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'sku',
        'name',
        'description',
        'slug',
        'price_cents',
        'currency',
        'attributes',
        'stock_quantity',
        'reserved_quantity',
        'status',
    ];

    protected $casts = [
        'price_cents' => 'integer',
        'stock_quantity' => 'integer',
        'reserved_quantity' => 'integer',
        'attributes' => 'array', // Auto JSON encode/decode
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    /**
     * Get available stock (total - reserved).
     * 
     * Why separate reserved_quantity?
     * Prevents overselling when multiple customers add same item to cart.
     * 
     * @return int
     */
    public function getAvailableStockAttribute(): int
    {
        return max(0, $this->stock_quantity - $this->reserved_quantity);
    }

    /**
     * Get price in decimal format (e.g., 19.99).
     * 
     * Why store as cents?
     * Avoids floating-point precision errors. Never use DECIMAL for money.
     * 
     * @return float
     */
    public function getPriceAttribute(): float
    {
        return $this->price_cents / 100;
    }

    /**
     * Format price with currency symbol.
     * 
     * @return string
     */
    public function getFormattedPriceAttribute(): string
    {
        return match($this->currency) {
            'USD' => '$' . number_format($this->price, 2),
            'EUR' => '€' . number_format($this->price, 2),
            'GBP' => '£' . number_format($this->price, 2),
            default => $this->currency . ' ' . number_format($this->price, 2),
        };
    }

    /**
     * Scope query to only active products.
     * 
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    /**
     * Scope query to products with available stock.
     * 
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeInStock($query)
    {
        return $query->whereRaw('stock_quantity > reserved_quantity');
    }
}

Product Repository

app/Domain/Product/Repositories/ProductRepository.php:

<?php

namespace App\Domain\Product\Repositories;

use App\Domain\Product\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class ProductRepository
{
    /**
     * Get paginated active products with caching.
     * 
     * Cache strategy:
     * - Cache each page independently (cache key includes page number)
     * - TTL from config (default 1 hour)
     * - Cache tags for easy invalidation on product updates
     * 
     * @param int $page
     * @param int $perPage
     * @param array $filters
     * @return LengthAwarePaginator
     */
    public function getPaginatedProducts(
        int $page = 1,
        int $perPage = 24,
        array $filters = []
    ): LengthAwarePaginator {
        $cacheKey = $this->getCacheKey('products.list', $page, $filters);
        $cacheTtl = config('ecommerce.performance.catalog_cache_ttl', 3600);

        return Cache::tags(['products'])->remember(
            $cacheKey,
            $cacheTtl,
            fn() => $this->buildProductQuery($filters)
                ->paginate($perPage, ['*'], 'page', $page)
        );
    }

    /**
     * Build base query with filters.
     * 
     * @param array $filters
     * @return \Illuminate\Database\Eloquent\Builder
     */
    protected function buildProductQuery(array $filters)
    {
        $query = Product::query()
            ->active()
            ->select([
                'id',
                'sku',
                'name',
                'slug',
                'price_cents',
                'currency',
                'attributes',
                'stock_quantity',
                'reserved_quantity',
            ]);

        // Filter by category (stored in attributes JSONB)
        if (isset($filters['category'])) {
            $query->whereRaw("attributes->>'category' = ?", [$filters['category']]);
        }

        // Filter by price range
        if (isset($filters['min_price'])) {
            $query->where('price_cents', '>=', $filters['min_price'] * 100);
        }
        if (isset($filters['max_price'])) {
            $query->where('price_cents', '<=', $filters['max_price'] * 100);
        }

        // Filter by availability
        if (isset($filters['in_stock']) && $filters['in_stock']) {
            $query->inStock();
        }

        // Search by name or description
        if (isset($filters['search'])) {
            $search = $filters['search'];
            $query->where(function($q) use ($search) {
                $q->where('name', 'ILIKE', "%{$search}%")
                  ->orWhere('description', 'ILIKE', "%{$search}%");
            });
        }

        // Sorting
        $sortBy = $filters['sort_by'] ?? 'created_at';
        $sortOrder = $filters['sort_order'] ?? 'desc';
        
        $allowedSorts = ['created_at', 'price_cents', 'name'];
        if (in_array($sortBy, $allowedSorts)) {
            $query->orderBy($sortBy, $sortOrder);
        }

        return $query;
    }

    /**
     * Generate cache key from parameters.
     * 
     * @param string $prefix
     * @param int $page
     * @param array $filters
     * @return string
     */
    protected function getCacheKey(string $prefix, int $page, array $filters): string
    {
        $filterHash = md5(json_encode($filters));
        return "{$prefix}.page_{$page}.{$filterHash}";
    }

    /**
     * Invalidate product cache.
     * Call this when products are created/updated/deleted.
     * 
     * @return bool
     */
    public function clearCache(): bool
    {
        return Cache::tags(['products'])->flush();
    }
}

API Controller

app/Http/Controllers/Api/ProductController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Domain\Product\Repositories\ProductRepository;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;

class ProductController extends Controller
{
    public function __construct(
        protected ProductRepository $productRepository
    ) {}

    /**
     * List products with pagination and filtering.
     * 
     * GET /api/v1/products
     * 
     * Query parameters:
     * - page: Page number (default: 1)
     * - per_page: Items per page (default: 24, max: 100)
     * - category: Filter by category
     * - min_price: Minimum price filter
     * - max_price: Maximum price filter
     * - in_stock: Only show available products (true/false)
     * - search: Search in name and description
     * - sort_by: Sort field (created_at, price_cents, name)
     * - sort_order: Sort direction (asc, desc)
     * 
     * @param Request $request
     * @return JsonResponse
     */
    public function index(Request $request): JsonResponse
    {
        // Validate query parameters
        $validator = Validator::make($request->all(), [
            'page' => 'integer|min:1',
            'per_page' => 'integer|min:1|max:100',
            'category' => 'string|max:50',
            'min_price' => 'numeric|min:0',
            'max_price' => 'numeric|min:0|gte:min_price',
            'in_stock' => 'boolean',
            'search' => 'string|max:200',
            'sort_by' => 'in:created_at,price_cents,name',
            'sort_order' => 'in:asc,desc',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'error' => 'Validation failed',
                'messages' => $validator->errors(),
            ], 422);
        }

        try {
            $validated = $validator->validated();
            
            $page = $validated['page'] ?? 1;
            $perPage = min($validated['per_page'] ?? 24, 100);
            
            // Extract filters
            $filters = array_intersect_key($validated, array_flip([
                'category',
                'min_price',
                'max_price',
                'in_stock',
                'search',
                'sort_by',
                'sort_order',
            ]));

            $products = $this->productRepository->getPaginatedProducts(
                $page,
                $perPage,
                $filters
            );

            // Transform response
            return response()->json([
                'data' => $products->map(fn($product) => [
                    'id' => $product->id,
                    'sku' => $product->sku,
                    'name' => $product->name,
                    'slug' => $product->slug,
                    'price' => [
                        'amount' => $product->price,
                        'formatted' => $product->formatted_price,
                        'currency' => $product->currency,
                    ],
                    'attributes' => $product->attributes,
                    'availability' => [
                        'in_stock' => $product->available_stock > 0,
                        'quantity' => $product->available_stock,
                    ],
                ]),
                'meta' => [
                    'current_page' => $products->currentPage(),
                    'last_page' => $products->lastPage(),
                    'per_page' => $products->perPage(),
                    'total' => $products->total(),
                ],
                'links' => [
                    'first' => $products->url(1),
                    'last' => $products->url($products->lastPage()),
                    'prev' => $products->previousPageUrl(),
                    'next' => $products->nextPageUrl(),
                ],
            ], 200);

        } catch (\Exception $e) {
            // Log error with context for debugging
            Log::error('Product listing failed', [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
                'filters' => $filters ?? [],
            ]);

            return response()->json([
                'error' => 'Failed to retrieve products',
                'message' => config('app.debug') ? $e->getMessage() : 'Internal server error',
            ], 500);
        }
    }
}

API Routes

routes/api.php:

<?php

use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| API versioning strategy: /api/v1/...
| This allows breaking changes in future versions without affecting existing clients.
|
*/

Route::prefix('v1')->group(function () {
    // Product catalog endpoints
    Route::get('/products', [ProductController::class, 'index'])
        ->name('api.v1.products.index');
    
    // Add rate limiting middleware for production
    // Route::middleware('throttle:60,1')->group(function () {
    //     Route::get('/products', [ProductController::class, 'index']);
    // });
});

Seeding Test Data

database/seeders/ProductSeeder.php:

<?php

namespace Database\Seeders;

use App\Domain\Product\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    /**
     * Seed product catalog with realistic data.
     */
    public function run(): void
    {
        $products = [
            [
                'sku' => 'TSHIRT-BLK-M',
                'name' => 'Premium Cotton T-Shirt - Black',
                'description' => 'Soft, breathable cotton t-shirt perfect for everyday wear.',
                'slug' => 'premium-cotton-tshirt-black',
                'price_cents' => 2999, // $29.99
                'currency' => 'USD',
                'attributes' => [
                    'category' => 'clothing',
                    'color' => 'black',
                    'size' => 'M',
                    'material' => 'cotton',
                ],
                'stock_quantity' => 150,
                'reserved_quantity' => 5,
                'status' => 'active',
            ],
            [
                'sku' => 'JEANS-DNM-32',
                'name' => 'Classic Denim Jeans',
                'description' => 'Timeless denim jeans with a comfortable fit.',
                'slug' => 'classic-denim-jeans',
                'price_cents' => 7999, // $79.99
                'currency' => 'USD',
                'attributes' => [
                    'category' => 'clothing',
                    'color' => 'blue',
                    'size' => '32',
                    'material' => 'denim',
                ],
                'stock_quantity' => 85,
                'reserved_quantity' => 0,
                'status' => 'active',
            ],
            [
                'sku' => 'SNKR-RUN-10',
                'name' => 'Performance Running Shoes',
                'description' => 'Lightweight running shoes designed for speed and comfort.',
                'slug' => 'performance-running-shoes',
                'price_cents' => 12999, // $129.99
                'currency' => 'USD',
                'attributes' => [
                    'category' => 'footwear',
                    'color' => 'white',
                    'size' => '10',
                    'material' => 'mesh',
                ],
                'stock_quantity' => 42,
                'reserved_quantity' => 3,
                'status' => 'active',
            ],
        ];

        foreach ($products as $productData) {
            Product::create($productData);
        }

        $this->command->info('Seeded ' . count($products) . ' products.');
    }
}

Run the seeder:

docker-compose exec app php artisan db:seed --class=ProductSeeder

Testing the API

# Test product listing
curl -X GET "http://localhost:8000/api/v1/products" \
  -H "Accept: application/json" | jq

# Expected output:
# {
#   "data": [
#     {
#       "id": 1,
#       "sku": "TSHIRT-BLK-M",
#       "name": "Premium Cotton T-Shirt - Black",
#       "slug": "premium-cotton-tshirt-black",
#       "price": {
#         "amount": 29.99,
#         "formatted": "$29.99",
#         "currency": "USD"
#       },
#       "attributes": {
#         "category": "clothing",
#         "color": "black",
#         "size": "M",
#         "material": "cotton"
#       },
#       "availability": {
#         "in_stock": true,
#         "quantity": 145
#       }
#     },
#     ...
#   ],
#   "meta": {
#     "current_page": 1,
#     "last_page": 1,
#     "per_page": 24,
#     "total": 3
#   }
# }

# Test filtering by category
curl -X GET "http://localhost:8000/api/v1/products?category=clothing&in_stock=true" \
  -H "Accept: application/json" | jq

# Test price range filtering
curl -X GET "http://localhost:8000/api/v1/products?min_price=50&max_price=100" \
  -H "Accept: application/json" | jq

# Test search
curl -X GET "http://localhost:8000/api/v1/products?search=running" \
  -H "Accept: application/json" | jq

# Test pagination
curl -X GET "http://localhost:8000/api/v1/products?page=1&per_page=2" \
  -H "Accept: application/json" | jq

Verifying Cache Behavior

# Check Redis for cached queries
docker-compose exec redis redis-cli

# List all keys
KEYS *

# Output should include cache keys like:
# laravel_database_products:products.list.page_1.abc123...

# Check cache hit by querying same endpoint twice and comparing response times
time curl -s "http://localhost:8000/api/v1/products" > /dev/null
# First request: ~150ms (database query)

time curl -s "http://localhost:8000/api/v1/products" > /dev/null
# Second request: ~5ms (cache hit)

Common Setup Mistakes and Solutions

1. Using SQLite or MySQL in Development, PostgreSQL in Production

The Problem: Feature parity issues. PostgreSQL's JSONB type, full-text search, and advanced indexing don't have MySQL equivalents.

The Solution: Use PostgreSQL everywhere. Docker makes this trivial.

2. Not Pinning Dependency Versions

The Problem: composer install on different machines pulls different package versions, causing "works on my machine" bugs.

The Solution:

# Always commit composer.lock
git add composer.lock

# Use exact versions in production
composer install --no-dev --optimize-autoloader

3. Storing Money as DECIMAL or FLOAT

The Problem: Floating-point precision errors. 19.99 * 100 may equal 1998.9999999 in some cases.

The Solution: Always store money as integers in the smallest currency unit (cents, pence, etc.). Our price_cents column avoids this entirely.

4. Not Handling Database Connection Failures

The Problem: In Kubernetes, database pods restart during updates. Unhandled connection failures crash your app.

The Solution: Add retry logic in config/database.php:

'pgsql' => [
    'driver' => 'pgsql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '5432'),
    'database' => env('DB_DATABASE', 'forge'),
    'username' => env('DB_USERNAME', 'forge'),
    'password' => env('DB_PASSWORD', ''),
    'charset' => 'utf8',
    'prefix' => '',
    'prefix_indexes' => true,
    'search_path' => 'public',
    'sslmode' => 'prefer',
    
    // Retry failed connections (critical for Kubernetes)
    'options' => [
        PDO::ATTR_TIMEOUT => 5,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_PERSISTENT => false,
    ],
],

5. Ignoring Opcache Configuration

The Problem: Default PHP configuration rechecks file modification times on every request, killing performance.

The Solution: Our production Dockerfile sets opcache.validate_timestamps=0. After deployment, Opcache never checks if files changed (because they won't—containers are immutable).

6. Not Using Cache Tags

The Problem: Cache invalidation is hard. Clearing all cache when one product updates is wasteful.

The Solution: Cache tags (requires Redis or Memcached). Our ProductRepository uses Cache::tags(['products']) so we can invalidate only product-related caches:

// Clear only product caches, leave session and other caches intact
Cache::tags(['products'])->flush();

What's Next in Part 2: Stripe Payment Integration

In the next part of this series, we'll integrate Stripe for payment processing with:

  1. Payment Intent API for secure checkout flows
  2. Webhook handling for asynchronous payment confirmations
  3. Idempotency keys to prevent duplicate charges
  4. 3D Secure (SCA) compliance for European customers
  5. Refund and dispute handling workflows
  6. Testing with Stripe CLI for local webhook simulation

We'll also implement the OrderService that coordinates inventory reservation, payment processing, and order creation in a single atomic transaction.

Preview: OrderService Transaction Pattern

DB::transaction(function () use ($cart, $paymentIntentId) {
    // 1. Convert cart to order
    $order = $this->orderRepository->createFromCart($cart);
    
    // 2. Confirm Stripe payment
    $payment = $this->paymentService->confirmPayment($paymentIntentId);
    
    // 3. Reduce inventory (only if payment succeeds)
    $this->inventoryService->decrementStock($order);
    
    // 4. Clear cart
    $cart->delete();
    
    // 5. Dispatch order confirmation email job
    dispatch(new SendOrderConfirmation($order));
});

All of this with proper error handling, retry logic, and webhook verification.


Summary: What We Built

In this first part, we established the foundation for a production-grade e-commerce platform:

Architecture design that scales horizontally on Kubernetes
Docker multi-stage builds optimized for development and production
PostgreSQL schema with JSONB for flexible product attributes
Repository pattern with Redis caching for performance
Complete API endpoint with filtering, pagination, and validation
Development environment with Docker Compose for team consistency

Key Takeaways:

  1. Start with production architecture from day one. Migrating a monolith to Kubernetes later is painful.
  2. Use PostgreSQL's advanced features. JSONB and partial indexes are game-changers for e-commerce.
  3. Cache aggressively with tags. Product catalogs change infrequently—exploit that.
  4. Store money as integers. Never use floats for currency.
  5. Make development match production. Docker ensures everyone runs the same stack.

Clone the complete code from this tutorial at https://github.com/iBekzod/laravel-k8s-ecommerce and follow along with Part 2!


Read the next article: Part 2: Stripe Payment Integration & Order Processing (coming soon)

Questions or feedback? Open an issue on the GitHub repository or reach out at nextgenbeing.com.

Daniel Hartwell

Daniel Hartwell

Author

Senior backend engineer focused on distributed systems and database performance. Previously at fintech and SaaS scale-ups. Writes about the boring-but-critical infrastructure that keeps systems running.

Never Miss an Article

Get our best content delivered to your inbox weekly. No spam, unsubscribe anytime.

Comments (0)

Please log in to leave a comment.

Log In

Related Articles