Skip to main content

Cache System Implementation Guide

This guide provides step-by-step instructions for implementing the cache system in your models and modules.

Prerequisites

Before implementing the cache system, ensure you have:

  1. A model with a JSON cache column in the database
  2. Business events that should trigger cache updates
  3. Clear understanding of what data should be cached

Step 1: Prepare Your Model

Add the Required Column

If your model doesn't already have a cache column, add it via migration:

database/migrations/xxxx_xx_xx_add_cache_to_products_table.php
<?php

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

return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->json('cache')->nullable();
});
}

public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('cache');
});
}
};

Implement Required Interfaces and Traits

Update your model to implement the CacheableModel interface and use the HasCacheField trait:

app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Services\Cache\Contracts\CacheableModel;
use App\Services\Cache\Traits\HasCacheField;
use App\Services\Cache\DTOs\CacheConfigCollection;
use App\Services\Cache\DTOs\CacheConfigDTO;
use App\Services\Cache\Enums\CacheModeEnum;
use App\Services\Cache\Enums\CacheCastEnum;

class Product extends Model implements CacheableModel
{
use HasCacheField;

protected $fillable = [
'name',
'description',
'price',
// Do NOT include 'cache' here
];

protected $casts = [
// Do NOT include 'cache' here
];

// Implementation of CacheableModel interface
public function getCacheConfig(): CacheConfigCollection
{
return new CacheConfigCollection([
// Cache configurations will go here
]);
}
}

Step 2: Define Cache Configurations

Add cache configurations to your model's getCacheConfig() method:

app/Models/Product.php
public function getCacheConfig(): CacheConfigCollection
{
return new CacheConfigCollection([
new CacheConfigDTO(
key: 'related_products',
relatedEvents: [
ProductUpdatedEvent::class,
ProductCategoryChangedEvent::class,
],
initializerEvents: [
ProductCreatedEvent::class,
],
sourceModule: 'Product',
sourceEntity: Product::class,
mode: CacheModeEnum::SYNC,
cast: CacheCastEnum::ARRAY,
),
new CacheConfigDTO(
key: 'view_statistics',
relatedEvents: [
ProductViewedEvent::class,
],
sourceModule: 'Analytics',
sourceEntity: Product::class,
mode: CacheModeEnum::ASYNC,
cast: CacheCastEnum::OBJECT,
),
]);
}

Step 3: Create Cache Handlers

Create a cache handler for each cache key:

app/Modules/Product/Services/CacheHandlers/RelatedProductsCacheHandler.php
<?php

namespace App\Modules\Product\Services\CacheHandlers;

use App\Services\Cache\Contracts\CacheHandler;
use App\Services\Cache\DTOs\CacheEventDTO;
use App\Services\Cache\DTOs\CacheHandlerResult;
use App\Services\Cache\DTOs\CacheHandlerResultCollection;
use App\Models\Product;
use Psr\Log\LoggerInterface;

final readonly class RelatedProductsCacheHandler implements CacheHandler
{
public function __construct(
private LoggerInterface $logger
) {}

public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
$product = Product::find($productId);

if (!$product) {
return new CacheHandlerResultCollection();
}

// Get related products logic
$relatedProductIds = $this->getRelatedProductIds($product);

// Return the result
return new CacheHandlerResultCollection([
new CacheHandlerResult(
$productId,
$relatedProductIds
)
]);
}

private function getRelatedProductIds(Product $product): array
{
// Implementation of related products logic
// This is just an example
return Product::where('category_id', $product->category_id)
->where('id', '!=', $product->id)
->limit(5)
->pluck('id')
->toArray();
}
}

Step 4: Create Cache Initializers (Optional)

If you need to initialize cache data for new entities, create initializers:

app/Modules/Product/Services/CacheHandlers/RelatedProductsInitializer.php
<?php

namespace App\Modules\Product\Services\CacheHandlers;

use App\Services\Cache\Contracts\CacheInitializerInterface;
use App\Services\Cache\DTOs\CacheEventDTO;
use App\Services\Cache\DTOs\CacheHandlerResult;
use App\Services\Cache\DTOs\CacheHandlerResultCollection;
use App\Models\Product;
use Psr\Log\LoggerInterface;

final readonly class RelatedProductsInitializer implements CacheInitializerInterface
{
public function __construct(
private LoggerInterface $logger
) {}

public function initialize(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
$product = Product::find($productId);

if (!$product) {
return new CacheHandlerResultCollection();
}

// Get related products logic (same as handler)
$relatedProductIds = $this->getRelatedProductIds($product);

// Return the result
return new CacheHandlerResultCollection([
new CacheHandlerResult(
$productId,
$relatedProductIds
)
]);
}

private function getRelatedProductIds(Product $product): array
{
// Implementation of related products logic
return Product::where('category_id', $product->category_id)
->where('id', '!=', $product->id)
->limit(5)
->pluck('id')
->toArray();
}
}

Step 5: Register Services in Service Provider

Register your cache handlers and initializers in a service provider:

app/Providers/CacheServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\Cache\Contracts\CacheHandler;
use App\Services\Cache\Contracts\CacheInitializerInterface;
use App\Modules\Product\Services\CacheHandlers\RelatedProductsCacheHandler;
use App\Modules\Product\Services\CacheHandlers\RelatedProductsInitializer;

class CacheServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register cache handlers
$this->app->tag([
RelatedProductsCacheHandler::class,
], 'cache.handlers');

// Register cache initializers
$this->app->tag([
RelatedProductsInitializer::class,
], 'cache.initializers');
}
}

Step 6: Create Business Events

Create business events that will trigger cache updates:

app/Events/ProductUpdatedEvent.php
<?php

namespace App\Events;

use App\Models\Product;
use App\Services\Cache\Contracts\BusinessEvent;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProductUpdatedEvent implements BusinessEvent
{
use Dispatchable, SerializesModels;

public function __construct(
private readonly Product $product
) {}

public function getEntityId(): int|string
{
return $this->product->id;
}

public function getEventName(): string
{
return self::class;
}

public function getEventId(): string
{
return (string) $this->product->id . '_' . time();
}

public function getProduct(): Product
{
return $this->product;
}
}

Step 7: Dispatch Events

Dispatch events when relevant actions occur:

app/Http/Controllers/ProductController.php
public function update(UpdateProductRequest $request, Product $product)
{
$validated = $request->validated();

$product->update([
'name' => $validated['name'],
'description' => $validated['description'],
'price' => $validated['price'],
'category_id' => $validated['category_id'],
]);

// Dispatch event to trigger cache update
event(new ProductUpdatedEvent($product));

return response()->json([
'message' => 'Product updated successfully',
'product' => $product
]);
}

Step 8: Access Cached Data

Access cached data using the automatic accessors:

// Controller or service
public function getProductDetails(int $productId)
{
$product = Product::find($productId);

if (!$product) {
return response()->json(['error' => 'Product not found'], 404);
}

// Access cached data using automatic accessor
$relatedProducts = $product->related_products;
$viewStatistics = $product->view_statistics;

return response()->json([
'product' => $product,
'related_products' => $relatedProducts,
'statistics' => $viewStatistics,
]);
}

Step 9: Handle Missing Cache Data

Implement graceful handling for missing cache data:

public function getProductDetails(int $productId)
{
$product = Product::find($productId);

if (!$product) {
return response()->json(['error' => 'Product not found'], 404);
}

// Check if cache data exists
if (!$product->hasCacheData('related_products')) {
// Trigger cache initialization
event(new ProductCreatedEvent($product));

// Return empty array for now
$relatedProducts = [];
} else {
$relatedProducts = $product->related_products;
}

return response()->json([
'product' => $product,
'related_products' => $relatedProducts,
]);
}

Implementation Checklist

Use this checklist to ensure you've completed all necessary steps:

  • Added cache JSON column to the database table
  • Implemented CacheableModel interface in the model
  • Added HasCacheField trait to the model
  • Defined cache configurations in getCacheConfig()
  • Created cache handlers for each cache key
  • Created initializers for new entity cache (if needed)
  • Registered handlers and initializers in service provider
  • Created business events that implement BusinessEvent
  • Added event dispatching in appropriate places
  • Implemented graceful handling for missing cache data

Common Implementation Patterns

Pattern 1: Simple Cache Handler

For straightforward data caching:

final readonly class SimpleStatsCacheHandler implements CacheHandler
{
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();

// Simple data calculation
$stats = [
'view_count' => rand(100, 1000),
'last_viewed_at' => now()->toIso8601String(),
];

return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $stats)
]);
}
}

Pattern 2: Multi-Entity Cache Handler

For updating cache on multiple related entities:

final readonly class CategoryProductsCacheHandler implements CacheHandler
{
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
$product = Product::find($productId);

if (!$product || !$product->category_id) {
return new CacheHandlerResultCollection();
}

// Get all products in the same category
$categoryProducts = Product::where('category_id', $product->category_id)
->pluck('id')
->toArray();

// Create result for the category
$categoryResult = new CacheHandlerResult(
$product->category_id,
[
'product_count' => count($categoryProducts),
'product_ids' => $categoryProducts,
'updated_at' => now()->toIso8601String(),
]
);

return new CacheHandlerResultCollection([$categoryResult]);
}
}

Pattern 3: Async Heavy Processing

For computationally intensive operations:

// In model configuration
new CacheConfigDTO(
key: 'product_recommendations',
relatedEvents: [ProductViewedEvent::class],
sourceModule: 'Recommendations',
sourceEntity: Product::class,
mode: CacheModeEnum::ASYNC, // Process asynchronously
cast: CacheCastEnum::ARRAY,
)

// In cache handler
final readonly class ProductRecommendationsCacheHandler implements CacheHandler
{
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();

// Heavy computation that will run in a queue
$recommendations = $this->recommendationService->generateRecommendations($productId);

return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $recommendations)
]);
}
}

Advanced Implementation

Cross-Module Cache Updates

For cache that depends on data from multiple modules:

final readonly class ProductFullDetailsCacheHandler implements CacheHandler
{
public function __construct(
private QueryBusInterface $queryBus
) {}

public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();

// Get product details from Product module
$productQuery = new GetProductDetailsQuery($productId);
$productResponse = $this->queryBus->dispatch($productQuery);

if (!$productResponse->isSuccess()) {
return new CacheHandlerResultCollection();
}

// Get inventory details from Inventory module
$inventoryQuery = new GetProductInventoryQuery($productId);
$inventoryResponse = $this->queryBus->dispatch($inventoryQuery);

// Get pricing details from Pricing module
$pricingQuery = new GetProductPricingQuery($productId);
$pricingResponse = $this->queryBus->dispatch($pricingQuery);

// Combine all data
$fullDetails = [
'basic' => $productResponse->getData(),
'inventory' => $inventoryResponse->isSuccess() ? $inventoryResponse->getData() : null,
'pricing' => $pricingResponse->isSuccess() ? $pricingResponse->getData() : null,
];

return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $fullDetails)
]);
}
}

Conditional Cache Updates

For optimizing cache updates based on conditions:

final readonly class ConditionalCacheHandler implements CacheHandler
{
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
$product = Product::find($productId);

if (!$product) {
return new CacheHandlerResultCollection();
}

// Check if the event is relevant for this cache
$event = $eventDto->getEvent();

if ($event instanceof ProductPriceChangedEvent) {
// Only update price-related cache data
return $this->handlePriceChange($product);
}

if ($event instanceof ProductStockChangedEvent) {
// Only update stock-related cache data
return $this->handleStockChange($product);
}

// Default: full update
return $this->handleFullUpdate($product);
}

private function handlePriceChange(Product $product): CacheHandlerResultCollection
{
// Price-specific update logic
}

private function handleStockChange(Product $product): CacheHandlerResultCollection
{
// Stock-specific update logic
}

private function handleFullUpdate(Product $product): CacheHandlerResultCollection
{
// Full update logic
}
}

Next Steps

After implementing the cache system, consider:

  1. Best Practices - Follow recommended patterns and approaches
  2. Troubleshooting - Solve common issues
  3. Performance Monitoring - Monitor and optimize cache performance