<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/stop-reinventing-product-inventory-in-every-laravel-project-4cte18yd7" -->

---
title: Stop Reinventing Product Inventory in Every Laravel Project
description: Discussion about &quot;Stop Reinventing Product Inventory in Every Laravel Project&quot; on daily.dev - join the developer community
canonical: https://daily.dev/posts/stop-reinventing-product-inventory-in-every-laravel-project-4cte18yd7
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Stop Reinventing Product Inventory in Every Laravel Project | daily.dev
og:description: Discussion about &quot;Stop Reinventing Product Inventory in Every Laravel Project&quot; on daily.dev - join the developer community
og:url: https://daily.dev/posts/stop-reinventing-product-inventory-in-every-laravel-project-4cte18yd7
og:image: https://api.daily.dev/og/posts/4CTe18yd7.png
og:image:alt: Stop Reinventing Product Inventory in Every Laravel Project
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Stop Reinventing Product Inventory in Every Laravel Project

**[Laravel Dev](https://daily.dev/sources/laraveldev)** · [@muhanz](https://daily.dev/muhanz) · 1 upvotes · 0 comments

## Content

Every time I started a new Laravel project that needed a product catalog, I ended up writing the same things from scratch: product variants, stock tracking, reservation logic, audit trails. Each time slightly different, each time with bugs I'd already fixed in the previous project.

So I built a package to solve this once.

## The Core Problem with Most Catalog Packages

Most Laravel e-commerce packages are _full stacks_ — they bundle cart, checkout, payment, and orders alongside the catalog. That's fine if you're building a standard storefront, but it becomes a problem when:

- Your order flow lives in a separate service
- You already have an ERP or WMS managing stock
- You need a custom checkout flow the package wasn't designed for
- You just need the catalog layer, not an entire commerce platform

`aliziodev/laravel-product-catalog` does one thing: **product catalog with variant-centric inventory**. You own the order flow.

## Variant-Centric Design

The key architectural decision: `Product` is a **presentation entity**. `ProductVariant` is the **sellable unit**.

```
// Product is what customers browse
$product = Product::create([
    'name' => 'Running Shoes',
    'type' => ProductType::Variable,
]);

// ProductVariant is what they actually buy
$variant = ProductVariant::create([
    'product_id' => $product->id,
    'sku'        => 'RS-AIR-RED-42',
    'price'      => 850000,
]);

// Stock is tracked per variant, not per product
$variant->inventoryItem()->create([
    'quantity' => 100,
    'policy'   => InventoryPolicy::Track,
]);
```

This means price, stock, weight, and dimensions live on the variant — not on the product. A product with Red/S, Red/M, and Blue/S is three variants, each with their own inventory.

## Pluggable Inventory via Driver Pattern

The inventory system is fully swappable. The package ships two built-in drivers:

- `database` — stock tracked in `catalog_inventory_items` (default)
- `null` — always in stock, no DB writes (digital goods, pre-orders)

But if you already have stock in your own table or an external ERP, implement one interface and plug it in:

```
class ErpInventoryProvider implements InventoryProviderInterface
{
    public function getQuantity(ProductVariant $variant): int
    {
        return Http::get("https://erp.internal/stock/{$variant->sku}")
            ->json('quantity', 0);
    }

    public function adjust(ProductVariant $variant, int $delta, string $reason = '', ?Model $reference = null): void
    {
        Http::post("https://erp.internal/stock/{$variant->sku}/adjust", [
            'delta'  => $delta,
            'reason' => $reason,
        ]);
    }

    // reserve(), release(), commit() ...
}

// Register
ProductCatalog::extend('erp', fn () => new ErpInventoryProvider);
```

```
PRODUCT_CATALOG_INVENTORY_DRIVER=erp
```

Your application code never changes. The driver handles the difference.

## Race-Condition Safe by Default

This is where most DIY implementations break under load. Two concurrent requests both check stock, both see 5 units available, and both proceed — now you've oversold.

The `DatabaseInventoryProvider` uses **pessimistic locking** to prevent this:

```
// Inside every write operation (adjust, reserve, commit, etc.)
DB::transaction(function () use ($variant, $callback) {
    $item = InventoryItem::where('variant_id', $variant->getKey())
        ->lockForUpdate()  // SELECT ... FOR UPDATE
        ->first();

    return $callback($item);
});
```

The `getOrCreateItem()` call happens _outside_ the transaction intentionally — running `firstOrCreate` inside a transaction on a potentially non-existent row can escalate to a gap lock and deadlock under concurrent inserts.

## Full Reservation Lifecycle

The package supports the reserve → commit/release pattern that every real e-commerce flow needs:

```
Customer places order  → reserve()   // hold stock, quantity unchanged
Payment confirmed      → commit()    // deduct permanently (both quantity and reserved)
Order cancelled        → release()   // return the hold
```

```
$inventory = ProductCatalog::inventory();

// Order created — soft-hold stock
$inventory->reserve($variant, 3, InventoryReason::ORDER_PLACED, $order);

// Payment confirmed — permanent deduction
$inventory->commit($variant, 3, InventoryReason::ORDER_FULFILLED, $order);

// Or: order cancelled — release the hold
$inventory->release($variant, 3, InventoryReason::ORDER_CANCELLED, $order);
```

Every operation writes an append-only `InventoryMovement` record — a complete audit trail for every stock change.

## Events for the Moments That Matter

```
// Fires when adjust(), set(), or commit() changes total quantity
InventoryAdjusted::class

// Fires when reserve() or release() changes reserved_quantity
InventoryReserved::class

// Fires when available quantity crosses low_stock_threshold (from above)
InventoryLowStock::class  // $event->availableQuantity, $event->threshold

// Fires when available quantity drops to zero
InventoryOutOfStock::class
```

`InventoryLowStock` and `InventoryOutOfStock` only fire on **threshold crossing** — not on every operation below the threshold. When stock hits zero, only `InventoryOutOfStock` fires (not both).

## What the Package Intentionally Leaves Out

**No image gallery.** Use spatie/laravel-medialibrary or whatever your project already uses. The package has `featured_image_path` for a single URL and `meta` JSON for anything else.

**No product attributes table.** Filterable attributes (material, wattage, color family) are domain-specific — a fashion store's schema looks nothing like an electronics store's. The `meta` JSON column handles display-only specs. If you need filterable attributes, add the table yourself at the application level.

**No order, cart, or payment.** You own the transaction flow. The package gives you `reserve()`, `release()`, and `commit()` — wire them into your own order actions.

## Quick Start

```
composer require aliziodev/laravel-product-catalog
php artisan catalog:install
```

```
use Aliziodev\ProductCatalog\Models\Product;
use Aliziodev\ProductCatalog\Enums\ProductType;
use Aliziodev\ProductCatalog\Enums\InventoryPolicy;

$product = Product::create(['name' => 'T-Shirt', 'type' => ProductType::Simple]);
$variant = $product->variants()->create(['sku' => 'TS-WHT-M', 'price' => 150000]);
$variant->inventoryItem()->create(['quantity' => 50, 'policy' => InventoryPolicy::Track]);
$product->publish();

Product::published()->inStock()->with('variants')->get();
```

## Who This Is For

- You need a product catalog layer without a full commerce framework
- Your order flow is custom or lives in a separate service
- Your stock is managed externally (ERP, WMS, your own table)
- You've written the same inventory logic across multiple projects and want to stop

GitHub: [aliziodev/laravel-product-catalog](https://github.com/aliziodev/laravel-product-catalog)

## Similar posts on daily.dev

- [Don’t just attend KubeCon \+ CloudNativeCon, Merge Forward your experience\!](https://daily.dev/posts/don-t-just-attend-kubecon-cloudnativecon-merge-forward-your-experience--l0rpp73x8) · CNCF · 1 upvotes · 0 comments
- [Announcing H2 2026 KCDs](https://daily.dev/posts/announcing-h2-2026-kcds-m96goajm1) · CNCF · 1 upvotes · 0 comments
- [Two months of Open Community Groups](https://daily.dev/posts/two-months-of-open-community-groups-asf52zhbs) · CNCF · 0 upvotes · 0 comments
- [CNCF Unveils Schedule for KubeCon \+ CloudNativeCon Europe 2026](https://daily.dev/posts/cncf-unveils-schedule-for-kubecon-cloudnativecon-europe-2026-ikhcoa5cb) · CNCF · 2 upvotes · 0 comments
- [CNCF Debuts KubeCon \+ CloudNativeCon Japan 2026 Schedule](https://daily.dev/posts/cncf-debuts-kubecon-cloudnativecon-japan-2026-schedule-xp5pyudub) · CNCF · 1 upvotes · 0 comments

---

[View this post on daily.dev](https://daily.dev/posts/stop-reinventing-product-inventory-in-every-laravel-project-4cte18yd7)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"DiscussionForumPosting","mainEntityOfPage":"https://daily.dev/posts/stop-reinventing-product-inventory-in-every-laravel-project-4cte18yd7","headline":"Stop Reinventing Product Inventory in Every Laravel Project","text":"Discussion about \"Stop Reinventing Product Inventory in Every Laravel Project\" on daily.dev - join the developer community","url":"https://daily.dev/posts/stop-reinventing-product-inventory-in-every-laravel-project-4cte18yd7","datePublished":"2026-04-27T05:30:49.462Z","dateModified":"2026-04-27T05:30:49.462Z","author":{"@type":"Person","name":"Muhamad Hanafi","url":"https://daily.dev/muhanz","image":"https://lh3.googleusercontent.com/a/ACg8ocLSWpyFoPdnjldR4k8pT3D9nrqi7E79BhMOj5AO12TqFFLHO6lp=s96-c","interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"EndorseAction"},"userInteractionCount":80}},"image":"https://media.daily.dev/image/upload/s--B7IVLpwf--/f_auto/v1777221057/posts/N92T6wVu4?_a=BAMAMiWQ0","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":1},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"isPartOf":{"@type":"WebPage","url":"https://daily.dev/squads/laraveldev","name":"Laravel Dev"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Laravel Dev","item":"https://daily.dev/squads/laraveldev"},{"@type":"ListItem","position":3,"name":"Stop Reinventing Product Inventory in Every Laravel Project"}]}
```

