Cache System Best Practices
This guide outlines recommended practices for effectively using the cache system in your application.
Cache Key Naming
Naming Conventions
Follow these naming conventions for cache keys:
// ✅ Good: Descriptive and consistent
'user_profile_data'
'product_recommendations'
'order_statistics'
'category_hierarchy'
// ❌ Bad: Vague or inconsistent
'data'
'info'
'userdata' // Inconsistent with snake_case pattern
'ProductInfo' // Inconsistent with snake_case pattern
Namespace by Module
Prefix cache keys with the module name to avoid collisions:
// ✅ Good: Namespaced by module
'user_profile_data'
'user_activity_metrics'
'product_related_items'
'product_pricing_tiers'
// ❌ Bad: No module context
'profile'
'metrics'
'related'
'pricing'
Versioning
Include version information in cache keys when making breaking changes:
// ✅ Good: Version included in key
'user_profile_data_v2'
'product_recommendations_v3'
// Alternative approach: Version in metadata
$resultData = [
'data' => $transformedData,
'metadata' => [
'version' => '2.0',
'last_updated' => now()->toIso8601String(),
]
];
Cache Configuration
Event Selection
Be specific about which events should trigger cache updates:
// ✅ Good: Specific events
new CacheConfigDTO(
key: 'product_recommendations',
relatedEvents: [
ProductUpdatedEvent::class,
ProductCategoryChangedEvent::class,
ProductTagsChangedEvent::class,
],
// Other config...
)
// ❌ Bad: Too broad or missing important events
new CacheConfigDTO(
key: 'product_recommendations',
relatedEvents: [
ProductUpdatedEvent::class, // Missing category and tag events
],
// Other config...
)
Processing Mode Selection
Choose the appropriate processing mode based on the nature of the cache:
@tab Synchronous Cache
// Use SYNC mode when:
// - Cache is critical for immediate user experience
// - Cache generation is fast (< 100ms)
// - Data consistency is critical
new CacheConfigDTO(
key: 'product_basic_info',
relatedEvents: [ProductUpdatedEvent::class],
sourceModule: 'Product',
sourceEntity: Product::class,
mode: CacheModeEnum::SYNC, // Immediate update
cast: CacheCastEnum::OBJECT,
)
@tab Asynchronous Cache
// Use ASYNC mode when:
// - Cache generation is computationally expensive
// - Cache is not critical for immediate user experience
// - Some delay in cache updates is acceptable
new CacheConfigDTO(
key: 'product_recommendations',
relatedEvents: [ProductViewedEvent::class],
sourceModule: 'Recommendations',
sourceEntity: Product::class,
mode: CacheModeEnum::ASYNC, // Process in background
cast: CacheCastEnum::ARRAY,
)
Cast Type Selection
Choose the most appropriate cast type for your data:
// ✅ Good: Appropriate cast types
new CacheConfigDTO(
key: 'is_featured',
// other config...
cast: CacheCastEnum::BOOLEAN, // For boolean flags
)
new CacheConfigDTO(
key: 'view_count',
// other config...
cast: CacheCastEnum::INTEGER, // For numeric counts
)
new CacheConfigDTO(
key: 'last_activity',
// other config...
cast: CacheCastEnum::DATETIME, // For dates and times
)
// ❌ Bad: Inappropriate cast types
new CacheConfigDTO(
key: 'is_featured',
// other config...
cast: CacheCastEnum::STRING, // Should be BOOLEAN
)
Cache Handler Implementation
Single Responsibility
Each cache handler should focus on a single cache key:
// ✅ Good: Single responsibility
final readonly class RelatedProductsCacheHandler implements CacheHandler
{
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
// Only handles 'related_products' cache key
}
}
// ❌ Bad: Multiple responsibilities
final readonly class ProductCacheHandler implements CacheHandler
{
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
// Handles multiple cache keys
if ($eventDto->getCacheKey() === 'related_products') {
// Handle related products
} elseif ($eventDto->getCacheKey() === 'product_statistics') {
// Handle statistics
}
}
}
Error Handling
Implement robust error handling in cache handlers:
// ✅ Good: Comprehensive error handling
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
try {
$productId = $eventDto->getEntityId();
$product = Product::find($productId);
if (!$product) {
$this->logger->warning('Product not found', [
'product_id' => $productId,
'cache_key' => 'related_products'
]);
return new CacheHandlerResultCollection();
}
// Process cache data
$relatedProducts = $this->getRelatedProducts($product);
return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $relatedProducts)
]);
} catch (Exception $e) {
$this->logger->error('Failed to generate cache', [
'product_id' => $productId ?? null,
'cache_key' => 'related_products',
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Return empty collection on error
return new CacheHandlerResultCollection();
}
}
Performance Optimization
Optimize cache handlers for performance:
// ✅ Good: Performance optimized
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
// Use efficient queries
$relatedProducts = Product::select(['id', 'name', 'price'])
->where('category_id', function ($query) use ($productId) {
$query->select('category_id')
->from('products')
->where('id', $productId);
})
->where('id', '!=', $productId)
->limit(5)
->get();
// Transform only necessary data
$result = $relatedProducts->map(function ($product) {
return [
'id' => $product->id,
'name' => $product->name,
'price' => $product->price,
];
})->toArray();
return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $result)
]);
}
Data Structure
Consistent Format
Maintain a consistent data structure across cache handlers:
// ✅ Good: Consistent structure with metadata
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
// Process data...
$result = [
'data' => $processedData,
'metadata' => [
'count' => count($processedData),
'generated_at' => now()->toIso8601String(),
'source' => 'ProductModule',
'version' => '1.0',
]
];
return new CacheHandlerResultCollection([
new CacheHandlerResult($entityId, $result)
]);
}
Data Normalization
Normalize data before caching to ensure consistency:
// ✅ Good: Normalized data
private function normalizeProductData(Product $product): array
{
return [
'id' => (int) $product->id,
'name' => (string) $product->name,
'price' => (float) $product->price,
'is_active' => (bool) $product->is_active,
'created_at' => $product->created_at->toIso8601String(),
];
}
Cache Access Patterns
Graceful Degradation
Implement graceful degradation when cache data is missing:
// ✅ Good: Graceful degradation
public function getProductWithRelated(int $productId)
{
$product = Product::find($productId);
if (!$product) {
return null;
}
// Check if cache exists
if (!$product->hasCacheData('related_products')) {
// Trigger cache generation
event(new ProductCacheRequested($product));
// Return empty array for now
$relatedProducts = [];
} else {
$relatedProducts = $product->related_products;
}
return [
'product' => $product,
'related_products' => $relatedProducts,
];
}
Defensive Access
Use defensive programming when accessing cache data:
// ✅ Good: Defensive access
public function getUserStats(User $user)
{
// Check if cache exists
if (!$user->hasCacheData('activity_metrics')) {
return [
'post_count' => 0,
'comment_count' => 0,
'like_count' => 0,
];
}
$metrics = $user->activity_metrics;
return [
'post_count' => $metrics['post_count'] ?? 0,
'comment_count' => $metrics['comment_count'] ?? 0,
'like_count' => $metrics['like_count'] ?? 0,
];
}
Type Safety
Ensure type safety when accessing cache data:
// ✅ Good: Type-safe access
public function getUserLastLogin(User $user)
{
if (!$user->hasCacheData('last_login')) {
return null;
}
$lastLogin = $user->last_login; // Cast to Carbon via DATETIME cast
if (!$lastLogin instanceof Carbon) {
return null;
}
return $lastLogin->diffForHumans();
}
Testing
Unit Testing Cache Handlers
Write unit tests for cache handlers:
// ✅ Good: Comprehensive unit test
public function test_related_products_cache_handler()
{
// Arrange
$product = Product::factory()->create();
$relatedProducts = Product::factory()->count(3)->create([
'category_id' => $product->category_id
]);
$eventDto = new CacheEventDTO(
entityId: $product->id,
event: new ProductUpdatedEvent($product),
cacheKey: 'related_products'
);
$handler = app(RelatedProductsCacheHandler::class);
// Act
$result = $handler->handle($eventDto);
// Assert
$this->assertInstanceOf(CacheHandlerResultCollection::class, $result);
$this->assertCount(1, $result);
$this->assertEquals($product->id, $result->first()->getEntityId());
$data = $result->first()->getData();
$this->assertIsArray($data);
$this->assertArrayHasKey('data', $data);
$this->assertCount(3, $data['data']);
}
Integration Testing
Test the complete cache flow:
// ✅ Good: Integration test
public function test_cache_updates_on_product_update()
{
// Arrange
$product = Product::factory()->create();
$relatedProducts = Product::factory()->count(3)->create([
'category_id' => $product->category_id
]);
// Act
event(new ProductUpdatedEvent($product));
// Assert
$product->refresh();
$this->assertTrue($product->hasCacheData('related_products'));
$this->assertCount(3, $product->related_products);
}
Security
Data Filtering
Filter sensitive data before caching:
// ✅ Good: Filtering sensitive data
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$userId = $eventDto->getEntityId();
$user = User::find($userId);
if (!$user) {
return new CacheHandlerResultCollection();
}
// Filter out sensitive data
$profileData = [
'name' => $user->name,
'username' => $user->username,
'avatar' => $user->avatar_url,
'bio' => $user->bio,
// Do NOT include email, password, etc.
];
return new CacheHandlerResultCollection([
new CacheHandlerResult($userId, $profileData)
]);
}
Authorization Checks
Implement authorization checks in cache handlers:
// ✅ Good: Authorization checks
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
$product = Product::find($productId);
if (!$product) {
return new CacheHandlerResultCollection();
}
// Check if product is public or user has access
if (!$product->is_public && !$this->authService->canAccessProduct($product)) {
$this->logger->warning('Unauthorized cache generation attempt', [
'product_id' => $productId,
'user_id' => $this->authService->getCurrentUserId()
]);
return new CacheHandlerResultCollection();
}
// Continue with cache generation
}
Performance
Selective Updates
Update only what's necessary:
// ✅ Good: Selective updates
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$event = $eventDto->getEvent();
$productId = $eventDto->getEntityId();
// Only update price-related cache if price changed
if ($event instanceof ProductPriceChangedEvent) {
return $this->handlePriceChange($productId);
}
// Only update stock-related cache if stock changed
if ($event instanceof ProductStockChangedEvent) {
return $this->handleStockChange($productId);
}
// Full update for other events
return $this->handleFullUpdate($productId);
}
Batch Processing
Use batch processing for multiple entities:
// ✅ Good: Batch processing
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$categoryId = $eventDto->getEntityId();
// Get all products in category
$productIds = Product::where('category_id', $categoryId)
->pluck('id')
->toArray();
// Process in chunks to avoid memory issues
$results = new CacheHandlerResultCollection();
foreach (array_chunk($productIds, 100) as $chunk) {
$chunkResults = $this->processProductChunk($chunk);
$results = $results->merge($chunkResults);
}
return $results;
}
Caching the Cache
Consider caching expensive operations within cache handlers:
// ✅ Good: Caching expensive operations
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
// Use Laravel's cache for expensive operations
$recommendations = Cache::remember(
"product_recommendations_raw_{$productId}",
now()->addHours(1),
function () use ($productId) {
return $this->recommendationService->getExpensiveRecommendations($productId);
}
);
return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $recommendations)
]);
}
Monitoring and Maintenance
Cache Statistics
Implement cache statistics monitoring:
// ✅ Good: Cache statistics monitoring
public function getCacheHealthReport()
{
$products = Product::take(100)->get();
$stats = [
'total_products' => $products->count(),
'products_with_complete_cache' => 0,
'products_with_partial_cache' => 0,
'products_without_cache' => 0,
'cache_keys' => [
'related_products' => 0,
'pricing_tiers' => 0,
'stock_status' => 0,
],
];
foreach ($products as $product) {
$productStats = $product->getCacheStatistics();
if ($productStats['cached_keys'] === $productStats['total_keys']) {
$stats['products_with_complete_cache']++;
} elseif ($productStats['cached_keys'] === 0) {
$stats['products_without_cache']++;
} else {
$stats['products_with_partial_cache']++;
}
foreach ($productStats['keys_with_data'] as $key) {
if (isset($stats['cache_keys'][$key])) {
$stats['cache_keys'][$key]++;
}
}
}
return $stats;
}
Cache Warming
Implement cache warming for critical data:
// ✅ Good: Cache warming command
class WarmProductCacheCommand extends Command
{
protected $signature = 'cache:warm {--type=featured} {--limit=100}';
public function handle()
{
$query = Product::query();
if ($this->option('type') === 'featured') {
$query->where('is_featured', true);
} elseif ($this->option('type') === 'new') {
$query->where('created_at', '>=', now()->subDays(7));
}
$products = $query->limit($this->option('limit'))->get();
$bar = $this->output->createProgressBar($products->count());
foreach ($products as $product) {
event(new ProductCacheWarmingEvent($product));
$bar->advance();
}
$bar->finish();
$this->info("\nCache warming completed for {$products->count()} products.");
}
}
Cache Cleanup
Implement cache cleanup for stale data:
// ✅ Good: Cache cleanup command
class CleanupStaleCacheCommand extends Command
{
protected $signature = 'cache:cleanup {--days=30}';
public function handle()
{
$staleDate = now()->subDays($this->option('days'));
$products = Product::whereRaw("JSON_EXTRACT(cache, '$.related_products.updated_at') < ?", [
$staleDate->toIso8601String()
])->get();
$this->info("Found {$products->count()} products with stale cache.");
foreach ($products as $product) {
event(new ProductCacheRefreshEvent($product));
}
$this->info("Cache refresh events dispatched.");
}
}
Advanced Patterns
Cache Versioning
Implement cache versioning for breaking changes:
// ✅ Good: Cache versioning
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$productId = $eventDto->getEntityId();
$product = Product::find($productId);
if (!$product) {
return new CacheHandlerResultCollection();
}
$data = $this->generateCacheData($product);
// Add version information
$result = [
'data' => $data,
'metadata' => [
'version' => '2.0', // Increment when structure changes
'generated_at' => now()->toIso8601String(),
]
];
return new CacheHandlerResultCollection([
new CacheHandlerResult($productId, $result)
]);
}
// In consumer code
public function getProductData(Product $product)
{
if (!$product->hasCacheData('product_data')) {
return null;
}
$cache = $product->product_data;
// Check version and handle accordingly
$version = $cache['metadata']['version'] ?? '1.0';
if ($version === '1.0') {
// Handle legacy format
return $this->transformLegacyFormat($cache['data']);
}
// Current version
return $cache['data'];
}
Cache Dependencies
Handle cache dependencies:
// ✅ Good: Cache dependencies
public function handle(CacheEventDTO $eventDto): CacheHandlerResultCollection
{
$categoryId = $eventDto->getEntityId();
// Update category cache
$categoryResult = $this->updateCategoryCache($categoryId);
// Also update all products in this category
$productIds = Product::where('category_id', $categoryId)->pluck('id');
$productResults = new CacheHandlerResultCollection();
foreach ($productIds as $productId) {
$productResults->add(
new CacheHandlerResult(
$productId,
['category_updated_at' => now()->toIso8601String()]
)
);
}
// Combine results
return $categoryResult->merge($productResults);
}
Progressive Enhancement
Implement progressive enhancement for cache data:
// ✅ Good: Progressive enhancement
public function getProductDetails(int $productId)
{
$product = Product::find($productId);
if (!$product) {
return response()->json(['error' => 'Product not found'], 404);
}
// Basic data always available
$response = [
'id' => $product->id,
'name' => $product->name,
'price' => $product->price,
];
// Enhance with cache if available
if ($product->hasCacheData('related_products')) {
$response['related_products'] = $product->related_products;
}
if ($product->hasCacheData('pricing_tiers')) {
$response['pricing_tiers'] = $product->pricing_tiers;
}
return response()->json($response);
}
Next Steps
After implementing these best practices, consider:
- Troubleshooting - Solve common issues
- Implementation Guide - Review implementation details
- Field Protection - Learn about cache field protection