Skip to content

Performance Tips

Braden Keith edited this page Sep 20, 2025 · 1 revision

Performance Tips

Optimization strategies for high-performance availability checking.

Query Optimization

Eager Loading Rules

// Bad: N+1 problem
$resources = Resource::all();
foreach ($resources as $resource) {
    $isAvailable = $engine->isAvailable($resource, now());
    // Each call loads rules separately
}

// Good: Eager load rules
$resources = Resource::with('availabilityRules')->get();
foreach ($resources as $resource) {
    $isAvailable = $engine->isAvailable($resource, now());
}

Selective Loading

// Load only what you need
$rules = $resource->availabilityRules()
    ->where('enabled', true)
    ->where(function ($query) {
        $query->whereNull('expires_at')
              ->orWhere('expires_at', '>', now());
    })
    ->select(['type', 'config', 'effect', 'priority'])
    ->orderBy('priority')
    ->get();

Database Indexing

// Migration with optimized indexes
Schema::table('availability_rules', function (Blueprint $table) {
    // Composite index for common queries
    $table->index(['subject_type', 'subject_id', 'enabled', 'priority']);
    
    // Index for rule type filtering
    $table->index('type');
    
    // Index for expiration checks
    $table->index('expires_at');
});

Caching Strategies

Simple Caching

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class CachedAvailabilityService
{
    private $engine;
    
    public function isAvailable($resource, $moment)
    {
        $cacheKey = $this->getCacheKey($resource, $moment);
        
        return Cache::remember($cacheKey, 300, function () use ($resource, $moment) {
            return $this->engine->isAvailable($resource, $moment);
        });
    }
    
    private function getCacheKey($resource, $moment)
    {
        // Round to 5-minute intervals for better cache hits
        $rounded = $moment->copy()
            ->minute(floor($moment->minute / 5) * 5)
            ->second(0);
            
        return sprintf(
            'availability:%s:%d:%s',
            $resource->getMorphClass(),
            $resource->id,
            $rounded->timestamp
        );
    }
}

Tagged Caching

class TaggedCacheService
{
    public function cacheAvailability($resource, $moment, $result)
    {
        $tags = [
            'availability',
            "resource-{$resource->id}",
            "type-{$resource->getMorphClass()}",
        ];
        
        Cache::tags($tags)->put(
            $this->getCacheKey($resource, $moment),
            $result,
            $this->getCacheTtl($moment)
        );
    }
    
    public function invalidateResource($resource)
    {
        Cache::tags(["resource-{$resource->id}"])->flush();
    }
    
    private function getCacheTtl($moment)
    {
        $hoursAhead = now()->diffInHours($moment);
        
        return match(true) {
            $hoursAhead <= 1 => 60,      // 1 minute
            $hoursAhead <= 24 => 300,    // 5 minutes
            $hoursAhead <= 168 => 1800,  // 30 minutes
            default => 3600,              // 1 hour
        };
    }
}

Pre-warming Cache

namespace App\Jobs;

class PreWarmAvailabilityCache extends Job
{
    public function handle()
    {
        $resources = Resource::active()->get();
        $moments = $this->getCheckPoints();
        
        foreach ($resources as $resource) {
            foreach ($moments as $moment) {
                dispatch(new CacheAvailability($resource, $moment))
                    ->onQueue('low');
            }
        }
    }
    
    private function getCheckPoints()
    {
        $points = [];
        $current = now()->startOfHour();
        
        // Next 24 hours, every hour
        for ($i = 0; $i < 24; $i++) {
            $points[] = $current->copy()->addHours($i);
        }
        
        // Next 7 days, every 6 hours
        for ($i = 1; $i <= 7; $i++) {
            $points[] = $current->copy()->addDays($i)->setTime(0, 0);
            $points[] = $current->copy()->addDays($i)->setTime(6, 0);
            $points[] = $current->copy()->addDays($i)->setTime(12, 0);
            $points[] = $current->copy()->addDays($i)->setTime(18, 0);
        }
        
        return $points;
    }
}

Batch Processing

Bulk Availability Checks

class BulkAvailabilityService
{
    public function checkMultiple(array $resources, $moment)
    {
        $results = [];
        $uncached = [];
        
        // Check cache first
        foreach ($resources as $resource) {
            $cacheKey = $this->getCacheKey($resource, $moment);
            $cached = Cache::get($cacheKey);
            
            if ($cached !== null) {
                $results[$resource->id] = $cached;
            } else {
                $uncached[] = $resource;
            }
        }
        
        // Process uncached in batches
        $chunks = array_chunk($uncached, 10);
        
        foreach ($chunks as $chunk) {
            $this->processChunk($chunk, $moment, $results);
        }
        
        return $results;
    }
    
    private function processChunk($resources, $moment, &$results)
    {
        // Load all rules at once
        $resourceIds = array_map(fn($r) => $r->id, $resources);
        $resourceClass = get_class($resources[0]);
        
        $allRules = AvailabilityRule::where('subject_type', $resourceClass)
            ->whereIn('subject_id', $resourceIds)
            ->where('enabled', true)
            ->orderBy('priority')
            ->get()
            ->groupBy('subject_id');
        
        foreach ($resources as $resource) {
            $rules = $allRules[$resource->id] ?? collect();
            $resource->setRelation('availabilityRules', $rules);
            
            $result = $this->engine->isAvailable($resource, $moment);
            $results[$resource->id] = $result;
            
            // Cache the result
            Cache::put(
                $this->getCacheKey($resource, $moment),
                $result,
                300
            );
        }
    }
}

Parallel Processing

use Illuminate\Support\Facades\Parallel;

class ParallelAvailabilityService
{
    public function checkAvailabilityParallel(array $resources, $moment)
    {
        $chunks = array_chunk($resources, 50);
        
        $results = Parallel::map($chunks, function ($chunk) use ($moment) {
            $engine = app(AvailabilityEngine::class);
            $chunkResults = [];
            
            foreach ($chunk as $resource) {
                $chunkResults[$resource->id] = $engine->isAvailable($resource, $moment);
            }
            
            return $chunkResults;
        });
        
        return array_merge(...$results);
    }
}

Memory Optimization

Rule Compilation

class CompiledRuleService
{
    private array $compiledRules = [];
    
    public function compileRules($resource)
    {
        $key = $resource->getMorphClass() . ':' . $resource->id;
        
        if (isset($this->compiledRules[$key])) {
            return $this->compiledRules[$key];
        }
        
        $rules = $resource->availabilityRules()
            ->where('enabled', true)
            ->orderBy('priority')
            ->get(['type', 'config', 'effect', 'priority'])
            ->map(function ($rule) {
                return [
                    'type' => $rule->type,
                    'config' => $rule->config,
                    'effect' => $rule->effect === Effect::Allow,
                    'priority' => $rule->priority,
                ];
            })
            ->toArray();
        
        $this->compiledRules[$key] = $rules;
        
        return $rules;
    }
}

Lazy Loading Large Configs

class LazyConfigEvaluator implements RuleEvaluator
{
    public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
    {
        // Don't load large data unless needed
        if (!$this->quickCheck($config, $moment)) {
            return false;
        }
        
        // Now load the full data
        $fullData = $this->loadFullData($config['data_key']);
        
        return $this->detailedCheck($fullData, $moment, $subject);
    }
    
    private function quickCheck($config, $moment)
    {
        // Quick elimination checks
        return $moment->hour >= 8 && $moment->hour <= 20;
    }
}

Database Optimization

Partitioning Rules Table

// For very large rule sets, partition by subject type
Schema::create('availability_rules_products', function (Blueprint $table) {
    // Same structure as main table
});

Schema::create('availability_rules_rooms', function (Blueprint $table) {
    // Same structure as main table
});

// Custom model to use partitioned tables
class PartitionedAvailabilityRule extends Model
{
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
        
        if (isset($attributes['subject_type'])) {
            $this->setTable($this->getTableForType($attributes['subject_type']));
        }
    }
    
    private function getTableForType($type)
    {
        $map = [
            'App\\Models\\Product' => 'availability_rules_products',
            'App\\Models\\Room' => 'availability_rules_rooms',
        ];
        
        return $map[$type] ?? 'availability_rules';
    }
}

Read Replicas

class ReadOptimizedAvailability
{
    public function loadRules($resource)
    {
        return $resource->availabilityRules()
            ->on('read-replica')  // Use read replica
            ->where('enabled', true)
            ->orderBy('priority')
            ->get();
    }
}

Monitoring and Profiling

Performance Metrics

class MonitoredAvailabilityEngine
{
    private $engine;
    private $metrics;
    
    public function isAvailable($resource, $moment)
    {
        $start = microtime(true);
        
        try {
            $result = $this->engine->isAvailable($resource, $moment);
            
            $this->recordMetrics($resource, microtime(true) - $start, 'success');
            
            return $result;
        } catch (\Exception $e) {
            $this->recordMetrics($resource, microtime(true) - $start, 'error');
            throw $e;
        }
    }
    
    private function recordMetrics($resource, $duration, $status)
    {
        $this->metrics->record([
            'type' => 'availability_check',
            'resource_type' => $resource->getMorphClass(),
            'duration' => $duration,
            'status' => $status,
            'rule_count' => $resource->availabilityRules()->count(),
        ]);
        
        // Alert if slow
        if ($duration > 0.5) {
            Log::warning('Slow availability check', [
                'resource' => $resource->id,
                'duration' => $duration,
            ]);
        }
    }
}

Query Logging

class DebugAvailabilityService
{
    public function debugCheck($resource, $moment)
    {
        DB::enableQueryLog();
        
        $result = $this->engine->isAvailable($resource, $moment);
        
        $queries = DB::getQueryLog();
        
        Log::debug('Availability check queries', [
            'count' => count($queries),
            'total_time' => array_sum(array_column($queries, 'time')),
            'queries' => $queries,
        ]);
        
        return $result;
    }
}

Best Practices Summary

Do's

  • ✅ Cache frequently checked availability
  • ✅ Use eager loading for multiple resources
  • ✅ Index database columns used in queries
  • ✅ Round timestamps for better cache hits
  • ✅ Batch process bulk checks
  • ✅ Use read replicas for high traffic
  • ✅ Monitor performance metrics

Don'ts

  • ❌ Load all rules when only checking one type
  • ❌ Cache results for too long (data becomes stale)
  • ❌ Make synchronous API calls in evaluators
  • ❌ Store large configs in rule table
  • ❌ Process rules one by one in loops
  • ❌ Ignore slow query warnings

Next Steps

Getting Started

Installation
Set up the package in your Laravel app

Quick Start
Get running in 5 minutes

Basic Usage
Common patterns and examples


Core Concepts

How It Works
Understanding the evaluation engine

Rule Types
Available rule types and configurations

Priority System
How rule priority affects evaluation


Advanced Topics

Inventory Gates
Dynamic availability based on stock

Custom Evaluators
Build your own rule types

Complex Scenarios
Real-world implementation patterns

Performance Tips
Optimization strategies


API Reference

Configuration
Package configuration options

Models & Traits
Available models and traits

Testing
Testing your availability rules

Clone this wiki locally