Performance & Caching
Optimize your specifications for maximum performance. Learn caching strategies, query optimization techniques, and how to build specifications that scale from hundreds to millions of records.
Performance Fundamentals
Specifications operate in two contexts with different performance characteristics:
- Memory Operations: Fast for small datasets, memory-bound for large ones
- Database Queries: Scalable for large datasets, network/disk I/O bound
Understanding when to use each approach is crucial for optimal performance.
Query Optimization
Index-Aware Specification Design
Design specifications that leverage database indexes:
// Good: Uses indexed columns efficiently
class OptimizedUserSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
return $query->where('status', 'active') // Indexed column first
->where('created_at', '>', now()->subYear()) // Compound index
->orderBy('updated_at', 'desc'); // Additional indexed column
}
}
// Bad: Prevents index usage
class UnoptimizedUserSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
return $query->whereRaw('UPPER(email) LIKE ?', ['%@EXAMPLE.COM'])
->orWhere('status', 'active'); // OR conditions can prevent index usage
}
}
// Better: Index-friendly alternative
class ImprovedUserSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
return $query->where('email_domain', 'example.com') // Pre-computed indexed column
->where('status', 'active'); // Compound index with email_domain
}
}
Selective Query Building
Put most selective conditions first:
class SelectiveSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
return $query
// Most selective first (eliminates 95% of records)
->where('subscription_type', 'premium_enterprise')
// Moderately selective (filters remaining records)
->where('status', 'active')
// Least selective last (fine-tunes final results)
->where('created_at', '>', now()->subMonth());
}
public function isSatisfiedBy(mixed $candidate): bool
{
// Same order for consistency and short-circuiting
return $candidate->subscription_type === 'premium_enterprise'
&& $candidate->status === 'active'
&& $candidate->created_at > now()->subMonth();
}
}
Efficient Relationship Queries
Optimize specifications that work with relationships:
class EfficientRelationshipSpecification extends AbstractSpecification
{
public function __construct(
private readonly int $minimumOrders = 1
) {}
public function toQuery($query): Builder
{
if ($this->minimumOrders === 1) {
// Use exists for simple "has relationship" checks
return $query->whereHas('orders');
}
// Use withCount for counting relationships
return $query->withCount('orders')
->having('orders_count', '>=', $this->minimumOrders);
}
public function isSatisfiedBy(mixed $candidate): bool
{
// Leverage eager-loaded counts when available
if (isset($candidate->orders_count)) {
return $candidate->orders_count >= $this->minimumOrders;
}
// Fallback to relationship check
if ($candidate->relationLoaded('orders')) {
return $candidate->orders->count() >= $this->minimumOrders;
}
// Last resort: database query
return $candidate->orders()->count() >= $this->minimumOrders;
}
}
Caching Strategies
Basic Specification Caching
Cache expensive specification evaluations:
class CachedSpecification extends AbstractSpecification
{
public function __construct(
private readonly SpecificationInterface $innerSpec,
private readonly int $ttlSeconds = 3600,
private readonly ?string $cacheKey = null
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
$key = $this->cacheKey ?? $this->generateCacheKey($candidate);
return Cache::remember($key, $this->ttlSeconds, function () use ($candidate) {
return $this->innerSpec->isSatisfiedBy($candidate);
});
}
public function toQuery($query): Builder
{
// Query operations typically don't need caching at specification level
return $this->innerSpec->toQuery($query);
}
private function generateCacheKey(mixed $candidate): string
{
$specClass = get_class($this->innerSpec);
$candidateKey = $this->getCandidateKey($candidate);
return "spec.{$specClass}.{$candidateKey}";
}
private function getCandidateKey(mixed $candidate): string
{
if (method_exists($candidate, 'getCacheKey')) {
return $candidate->getCacheKey();
}
if (isset($candidate->id)) {
return get_class($candidate) . '.' . $candidate->id;
}
return md5(serialize($candidate));
}
}
// Usage
$cachedSpec = new CachedSpecification(
new ComplexBusinessRuleSpecification(),
ttlSeconds: 1800 // 30 minutes
);
if ($cachedSpec->isSatisfiedBy($user)) {
// Result cached for 30 minutes
}
Smart Cache Invalidation
Implement intelligent cache clearing:
class SmartCachedSpecification extends AbstractSpecification
{
use InteractsWithCache;
public function __construct(
private readonly SpecificationInterface $innerSpec,
private readonly array $invalidationEvents = []
) {}
public function boot(): void
{
// Register cache invalidation listeners
foreach ($this->invalidationEvents as $event) {
Event::listen($event, function ($eventInstance) {
$this->invalidateCache($eventInstance);
});
}
}
public function isSatisfiedBy(mixed $candidate): bool
{
$key = $this->getCacheKey($candidate);
return Cache::tags($this->getCacheTags($candidate))
->remember($key, 3600, function () use ($candidate) {
return $this->innerSpec->isSatisfiedBy($candidate);
});
}
private function invalidateCache($event): void
{
$tags = $this->getInvalidationTags($event);
Cache::tags($tags)->flush();
}
private function getCacheTags(mixed $candidate): array
{
return [
'specifications',
get_class($this->innerSpec),
get_class($candidate),
'entity.' . ($candidate->id ?? 'unknown')
];
}
private function getInvalidationTags($event): array
{
if (method_exists($event, 'getCacheInvalidationTags')) {
return $event->getCacheInvalidationTags();
}
return ['specifications'];
}
}
// Usage with automatic cache invalidation
$smartCached = new SmartCachedSpecification(
new UserEligibilitySpecification(),
invalidationEvents: [
UserUpdated::class,
UserStatusChanged::class,
UserSubscriptionChanged::class,
]
);
Query Result Caching
Cache expensive database queries:
class QueryCachedSpecification extends AbstractSpecification
{
public function __construct(
private readonly SpecificationInterface $innerSpec,
private readonly int $cacheTtl = 600 // 10 minutes
) {}
public function toQuery($query): Builder
{
$innerQuery = $this->innerSpec->toQuery($query);
// Add query caching
return $innerQuery->remember($this->cacheTtl);
}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->innerSpec->isSatisfiedBy($candidate);
}
}
// Advanced query caching with custom keys
class AdvancedQueryCachedSpecification extends AbstractSpecification
{
public function toQuery($query): Builder
{
$baseQuery = $this->innerSpec->toQuery($query);
// Generate cache key from query SQL and bindings
$cacheKey = $this->generateQueryCacheKey($baseQuery);
return $baseQuery->remember($this->cacheTtl, $cacheKey);
}
private function generateQueryCacheKey(Builder $query): string
{
$sql = $query->toSql();
$bindings = $query->getBindings();
return 'query.' . md5($sql . serialize($bindings));
}
}
Memory Optimization
Lazy Evaluation
Implement lazy evaluation for expensive operations:
class LazySpecification extends AbstractSpecification
{
private ?bool $cachedResult = null;
private mixed $lastCandidate = null;
public function __construct(
private readonly SpecificationInterface $innerSpec
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
// Only evaluate if candidate changed
if ($this->lastCandidate !== $candidate) {
$this->cachedResult = $this->innerSpec->isSatisfiedBy($candidate);
$this->lastCandidate = $candidate;
}
return $this->cachedResult;
}
public function toQuery($query): Builder
{
return $this->innerSpec->toQuery($query);
}
public function resetCache(): void
{
$this->cachedResult = null;
$this->lastCandidate = null;
}
}
Batch Processing
Process large datasets efficiently:
class BatchProcessingSpecification
{
public function __construct(
private readonly SpecificationInterface $spec,
private readonly int $batchSize = 1000
) {}
public function filterLargeCollection(Collection $collection): Collection
{
return $collection->lazy()
->filter(fn($item) => $this->spec->isSatisfiedBy($item))
->collect();
}
public function processLargeQuery(Builder $query): void
{
$query->whereSpecification($this->spec)
->chunk($this->batchSize, function ($batch) {
// Process each batch
foreach ($batch as $item) {
$this->processItem($item);
}
});
}
private function processItem($item): void
{
// Your processing logic here
}
}
Profiling and Monitoring
Performance Profiling
Track specification performance:
class ProfiledSpecification extends AbstractSpecification
{
private array $metrics = [];
public function __construct(
private readonly SpecificationInterface $innerSpec,
private readonly bool $enabled = true
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$this->enabled) {
return $this->innerSpec->isSatisfiedBy($candidate);
}
$start = microtime(true);
$result = $this->innerSpec->isSatisfiedBy($candidate);
$duration = microtime(true) - $start;
$this->recordMetric('isSatisfiedBy', $duration);
return $result;
}
public function toQuery($query): Builder
{
if (!$this->enabled) {
return $this->innerSpec->toQuery($query);
}
$start = microtime(true);
$result = $this->innerSpec->toQuery($query);
$duration = microtime(true) - $start;
$this->recordMetric('toQuery', $duration);
return $result;
}
private function recordMetric(string $method, float $duration): void
{
$this->metrics[] = [
'method' => $method,
'duration' => $duration,
'timestamp' => now(),
'specification' => get_class($this->innerSpec),
];
// Log slow operations
if ($duration > 0.1) { // 100ms threshold
Log::warning('Slow specification operation', [
'specification' => get_class($this->innerSpec),
'method' => $method,
'duration' => $duration,
]);
}
}
public function getMetrics(): array
{
return $this->metrics;
}
public function getAverageExecutionTime(string $method = null): float
{
$filteredMetrics = $method
? array_filter($this->metrics, fn($m) => $m['method'] === $method)
: $this->metrics;
if (empty($filteredMetrics)) {
return 0.0;
}
$totalDuration = array_sum(array_column($filteredMetrics, 'duration'));
return $totalDuration / count($filteredMetrics);
}
}
Query Analysis
Analyze specification-generated queries:
class QueryAnalyzer
{
public function analyzeSpecification(SpecificationInterface $spec): array
{
DB::listen(function ($query) use (&$queryLog) {
$queryLog[] = [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time,
];
});
$queryLog = [];
// Execute specification query
$query = User::whereSpecification($spec);
$results = $query->get();
return [
'queries' => $queryLog,
'total_time' => array_sum(array_column($queryLog, 'time')),
'query_count' => count($queryLog),
'result_count' => $results->count(),
'memory_usage' => memory_get_peak_usage(true),
];
}
public function explainQuery(SpecificationInterface $spec): array
{
$query = User::whereSpecification($spec);
$explainSql = 'EXPLAIN ' . $query->toSql();
$explanation = DB::select($explainSql, $query->getBindings());
return [
'sql' => $query->toSql(),
'bindings' => $query->getBindings(),
'explanation' => $explanation,
'recommendations' => $this->analyzeExplanation($explanation),
];
}
private function analyzeExplanation(array $explanation): array
{
$recommendations = [];
foreach ($explanation as $row) {
// Check for full table scans
if (str_contains($row->Extra ?? '', 'Using filesort')) {
$recommendations[] = 'Consider adding index for ORDER BY clause';
}
if (str_contains($row->Extra ?? '', 'Using temporary')) {
$recommendations[] = 'Query uses temporary table - consider optimization';
}
if ($row->type === 'ALL') {
$recommendations[] = 'Full table scan detected - add appropriate indexes';
}
}
return $recommendations;
}
}
Real-World Optimization Examples
E-commerce Product Filtering
class OptimizedProductFilterSpecification extends AbstractSpecification
{
public function __construct(
private readonly array $filters,
private readonly bool $useCache = true
) {}
public function toQuery($query): Builder
{
$optimizedQuery = $query;
// Apply most selective filters first
if (!empty($this->filters['category'])) {
// Category is usually very selective
$optimizedQuery = $optimizedQuery->where('category_id', $this->filters['category']);
}
if (!empty($this->filters['in_stock']) && $this->filters['in_stock']) {
// Stock check is moderately selective
$optimizedQuery = $optimizedQuery->where('stock_quantity', '>', 0);
}
if (!empty($this->filters['price_range'])) {
// Price range is less selective but uses index
[$min, $max] = $this->filters['price_range'];
$optimizedQuery = $optimizedQuery->whereBetween('price', [$min * 100, $max * 100]);
}
if (!empty($this->filters['brand'])) {
// Brand filter with index
$optimizedQuery = $optimizedQuery->where('brand_id', $this->filters['brand']);
}
// Add caching if enabled
if ($this->useCache) {
$cacheKey = 'products.' . md5(serialize($this->filters));
$optimizedQuery = $optimizedQuery->remember(600, $cacheKey);
}
return $optimizedQuery;
}
public function isSatisfiedBy(mixed $candidate): bool
{
// Use caching for memory operations too
if ($this->useCache) {
$cacheKey = "product_filter.{$candidate->id}." . md5(serialize($this->filters));
return Cache::remember($cacheKey, 300, function () use ($candidate) {
return $this->evaluateProduct($candidate);
});
}
return $this->evaluateProduct($candidate);
}
private function evaluateProduct($product): bool
{
// Same order as query for consistency
if (!empty($this->filters['category'])) {
if ($product->category_id !== $this->filters['category']) {
return false;
}
}
if (!empty($this->filters['in_stock']) && $this->filters['in_stock']) {
if ($product->stock_quantity <= 0) {
return false;
}
}
if (!empty($this->filters['price_range'])) {
[$min, $max] = $this->filters['price_range'];
$price = $product->price / 100; // Convert from cents
if ($price < $min || $price > $max) {
return false;
}
}
if (!empty($this->filters['brand'])) {
if ($product->brand_id !== $this->filters['brand']) {
return false;
}
}
return true;
}
}
User Analytics Specification
class HighPerformanceUserAnalyticsSpecification extends AbstractSpecification
{
public function __construct(
private readonly Carbon $startDate,
private readonly Carbon $endDate
) {}
public function toQuery($query): Builder
{
return $query
// Use partitioned table if available
->whereDate('created_at', '>=', $this->startDate)
->whereDate('created_at', '<=', $this->endDate)
// Eager load for analytics to prevent N+1
->with([
'orders' => fn($q) => $q->whereBetween('created_at', [
$this->startDate,
$this->endDate
]),
'orders.items'
])
// Use raw SQL for performance-critical calculations
->selectRaw('
users.*,
(
SELECT COUNT(*)
FROM orders
WHERE orders.user_id = users.id
AND orders.created_at BETWEEN ? AND ?
) as orders_count,
(
SELECT COALESCE(SUM(total_amount), 0)
FROM orders
WHERE orders.user_id = users.id
AND orders.created_at BETWEEN ? AND ?
) as total_spent
', [
$this->startDate, $this->endDate,
$this->startDate, $this->endDate
])
// Cache for analytics queries
->remember(1800); // 30 minutes
}
public function isSatisfiedBy(mixed $candidate): bool
{
// Use computed values if available
if (isset($candidate->orders_count, $candidate->total_spent)) {
return $this->evaluateFromComputedValues($candidate);
}
// Fallback to relationship evaluation
return $this->evaluateFromRelationships($candidate);
}
private function evaluateFromComputedValues($user): bool
{
return $user->orders_count > 0 && $user->total_spent > 0;
}
private function evaluateFromRelationships($user): bool
{
if (!$user->relationLoaded('orders')) {
// Load orders efficiently
$user->load([
'orders' => fn($q) => $q->whereBetween('created_at', [
$this->startDate,
$this->endDate
])
]);
}
return $user->orders->isNotEmpty() &&
$user->orders->sum('total_amount') > 0;
}
}
Performance Testing
Benchmark Specifications
Create performance benchmarks:
class SpecificationBenchmark
{
public function benchmarkSpecification(
SpecificationInterface $spec,
array $testData,
int $iterations = 1000
): array {
// Memory benchmark
$memoryStart = memory_get_usage(true);
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
foreach ($testData as $candidate) {
$spec->isSatisfiedBy($candidate);
}
}
$memoryEnd = memory_get_usage(true);
$end = microtime(true);
return [
'total_time' => $end - $start,
'average_time' => ($end - $start) / ($iterations * count($testData)),
'memory_used' => $memoryEnd - $memoryStart,
'operations_per_second' => ($iterations * count($testData)) / ($end - $start),
];
}
public function benchmarkQuery(
SpecificationInterface $spec,
int $iterations = 100
): array {
$times = [];
for ($i = 0; $i < $iterations; $i++) {
$start = microtime(true);
User::whereSpecification($spec)->get();
$times[] = microtime(true) - $start;
}
return [
'min_time' => min($times),
'max_time' => max($times),
'average_time' => array_sum($times) / count($times),
'median_time' => $this->median($times),
];
}
private function median(array $values): float
{
sort($values);
$count = count($values);
$middle = floor($count / 2);
if ($count % 2) {
return $values[$middle];
}
return ($values[$middle - 1] + $values[$middle]) / 2;
}
}
// Usage
$benchmark = new SpecificationBenchmark();
$testUsers = User::factory()->count(1000)->make();
$results = $benchmark->benchmarkSpecification(
new ActiveUserSpecification(),
$testUsers,
1000
);
dump($results);
Performance Guidelines
Best Practices Summary
- Database Indexes: Ensure specifications use indexed columns
- Query Order: Put most selective conditions first
- Caching Strategy: Cache expensive operations appropriately
- Relationship Loading: Eager load when needed, avoid N+1 queries
- Memory Management: Use lazy evaluation for large datasets
- Profiling: Monitor and profile specification performance
When to Cache
- ✅ Cache When: Complex calculations, external API calls, expensive database operations
- ❌ Don't Cache When: Simple comparisons, rapidly changing data, memory-constrained environments
Performance Red Flags
Watch out for these performance issues:
// ❌ Bad: N+1 query problem
foreach ($users as $user) {
if ((new UserWithOrdersSpecification())->isSatisfiedBy($user)) {
// Each iteration triggers a database query
}
}
// ✅ Good: Single query solution
$usersWithOrders = User::whereSpecification(
new UserWithOrdersSpecification()
)->get();
// ❌ Bad: Inefficient OR conditions
$query->where('status', 'active')
->orWhere('status', 'premium')
->orWhere('status', 'trial');
// ✅ Good: Use whereIn for multiple values
$query->whereIn('status', ['active', 'premium', 'trial']);
// ❌ Bad: Function in WHERE clause prevents index usage
$query->whereRaw('DATE(created_at) = ?', [today()]);
// ✅ Good: Range query uses index
$query->whereBetween('created_at', [today(), today()->addDay()]);
Next Steps
With performance optimization mastered, learn how to integrate specifications throughout your Laravel application:
- Laravel Integration → - Framework-specific patterns
- Best Practices → - Professional development approaches
Performance Mastery
Great performance comes from understanding both your data access patterns and your application's constraints. Profile first, optimize second, and always measure the impact of your changes.