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:
- A model with a JSON
cachecolumn in the database - Business events that should trigger cache updates
- 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:
<?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:
<?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:
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:
<?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:
<?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:
<?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:
<?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:
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
cacheJSON column to the database table - Implemented
CacheableModelinterface in the model - Added
HasCacheFieldtrait 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:
- Best Practices - Follow recommended patterns and approaches
- Troubleshooting - Solve common issues
- Performance Monitoring - Monitor and optimize cache performance