Skip to main content

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:

  1. Troubleshooting - Solve common issues
  2. Implementation Guide - Review implementation details
  3. Field Protection - Learn about cache field protection