Skip to content

Best Practices ​

Master the art of specification-driven development with industry best practices, design principles, and proven patterns. Build maintainable, scalable applications that stand the test of time.

Design Principles ​

Single Responsibility Principle ​

Each specification should have one clear, well-defined responsibility:

php
// Good: Single, clear responsibility
class PremiumUserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->subscription_type === 'premium'
            && $candidate->subscription_expires_at > now();
    }
}

// Bad: Multiple responsibilities mixed together
class UserBusinessRulesSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        // Premium check
        $isPremium = $candidate->subscription_type === 'premium';
        
        // Age verification
        $isOldEnough = $candidate->age >= 18;
        
        // Location check
        $isValidLocation = in_array($candidate->country, ['US', 'CA', 'UK']);
        
        // Email validation
        $hasValidEmail = filter_var($candidate->email, FILTER_VALIDATE_EMAIL);
        
        return $isPremium && $isOldEnough && $isValidLocation && $hasValidEmail;
    }
}

// Better: Split into focused specifications
class PremiumUserSpecification extends AbstractSpecification { /* Premium logic */ }
class AgeVerifiedUserSpecification extends AbstractSpecification { /* Age logic */ }
class ValidLocationSpecification extends AbstractSpecification { /* Location logic */ }
class ValidEmailSpecification extends AbstractSpecification { /* Email logic */ }

// Compose when needed
$eligibilitySpec = (new PremiumUserSpecification())
    ->and(new AgeVerifiedUserSpecification())
    ->and(new ValidLocationSpecification())
    ->and(new ValidEmailSpecification());

Open/Closed Principle ​

Specifications should be open for extension, closed for modification:

php
// Good: Extensible base specification
abstract class DiscountEligibilitySpecification extends AbstractSpecification
{
    abstract protected function getMinimumRequirements(): array;
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        foreach ($this->getMinimumRequirements() as $requirement) {
            if (!$requirement->isSatisfiedBy($candidate)) {
                return false;
            }
        }
        return true;
    }
}

// Extend without modifying the base
class VipDiscountEligibilitySpecification extends DiscountEligibilitySpecification
{
    protected function getMinimumRequirements(): array
    {
        return [
            new PremiumUserSpecification(),
            new MinimumPurchaseSpecification(5000),
            new LoyaltyMemberSpecification(),
        ];
    }
}

class StudentDiscountEligibilitySpecification extends DiscountEligibilitySpecification
{
    protected function getMinimumRequirements(): array
    {
        return [
            new VerifiedStudentSpecification(),
            new ActiveUserSpecification(),
        ];
    }
}

Dependency Inversion Principle ​

Depend on abstractions, not concretions:

php
// Good: Depends on interface
class OrderProcessingService
{
    public function __construct(
        private readonly SpecificationInterface $eligibilitySpec,
        private readonly SpecificationInterface $validationSpec
    ) {}
    
    public function processOrder(Order $order): bool
    {
        if (!$this->eligibilitySpec->isSatisfiedBy($order->customer)) {
            return false;
        }
        
        if (!$this->validationSpec->isSatisfiedBy($order)) {
            return false;
        }
        
        return $this->executeOrderProcessing($order);
    }
}

// Bad: Tightly coupled to concrete implementations
class BadOrderProcessingService
{
    public function processOrder(Order $order): bool
    {
        $premiumSpec = new PremiumUserSpecification();
        $validOrderSpec = new ValidOrderSpecification();
        
        // Tightly coupled - hard to test and extend
        if (!$premiumSpec->isSatisfiedBy($order->customer)) {
            return false;
        }
        
        return true;
    }
}

Naming Conventions ​

Descriptive, Business-Focused Names ​

Use names that clearly express business intent:

php
// Good: Clear business intent
class EligibleForPremiumDiscountSpecification extends AbstractSpecification {}
class ActiveSubscriptionSpecification extends AbstractSpecification {}
class HighValueCustomerSpecification extends AbstractSpecification {}
class ContentModerationApprovedSpecification extends AbstractSpecification {}

// Bad: Technical or vague names
class UserSpec extends AbstractSpecification {}
class CheckConditionSpecification extends AbstractSpecification {}
class ValidateDataSpecification extends AbstractSpecification {}
class BusinessRulesSpecification extends AbstractSpecification {}

Consistent Naming Patterns ​

Establish and follow consistent naming patterns:

php
// Pattern: [Subject][Condition]Specification
class UserActiveSpecification extends AbstractSpecification {}
class ProductInStockSpecification extends AbstractSpecification {}
class OrderShippableSpecification extends AbstractSpecification {}

// Pattern: [Action][Subject]Specification  
class CanEditProductSpecification extends AbstractSpecification {}
class CanViewUserSpecification extends AbstractSpecification {}
class CanDeleteOrderSpecification extends AbstractSpecification {}

// Pattern: [Subject]Eligible[ForWhat]Specification
class UserEligibleForDiscountSpecification extends AbstractSpecification {}
class CustomerEligibleForUpgradeSpecification extends AbstractSpecification {}
class ProductEligibleForPromotionSpecification extends AbstractSpecification {}

Organization and Structure ​

Domain-Based Organization ​

Organize specifications by domain, not by technical concerns:

app/
β”œβ”€β”€ Specifications/
β”‚   β”œβ”€β”€ User/
β”‚   β”‚   β”œβ”€β”€ ActiveUserSpecification.php
β”‚   β”‚   β”œβ”€β”€ PremiumUserSpecification.php
β”‚   β”‚   β”œβ”€β”€ VerifiedUserSpecification.php
β”‚   β”‚   └── UserEligibilitySpecification.php
β”‚   β”œβ”€β”€ Product/
β”‚   β”‚   β”œβ”€β”€ InStockProductSpecification.php
β”‚   β”‚   β”œβ”€β”€ FeaturedProductSpecification.php
β”‚   β”‚   └── ProductVisibilitySpecification.php
β”‚   β”œβ”€β”€ Order/
β”‚   β”‚   β”œβ”€β”€ ShippableOrderSpecification.php
β”‚   β”‚   β”œβ”€β”€ CancellableOrderSpecification.php
β”‚   β”‚   └── RefundableOrderSpecification.php
β”‚   └── Shared/
β”‚       β”œβ”€β”€ ActiveEntitySpecification.php
β”‚       β”œβ”€β”€ DateRangeSpecification.php
β”‚       └── StatusSpecification.php

Base Classes and Traits ​

Create reusable base classes and traits:

php
// Base specification for common patterns
abstract class EntityStatusSpecification extends AbstractSpecification
{
    protected abstract function getRequiredStatus(): string;
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        return $candidate->status === $this->getRequiredStatus();
    }
    
    public function toQuery($query): Builder
    {
        return $query->where('status', $this->getRequiredStatus());
    }
}

// Specific implementations
class ActiveUserSpecification extends EntityStatusSpecification
{
    protected function getRequiredStatus(): string
    {
        return 'active';
    }
}

class PublishedProductSpecification extends EntityStatusSpecification
{
    protected function getRequiredStatus(): string
    {
        return 'published';
    }
}

// Traits for common functionality
trait CacheableSpecification
{
    private ?string $cacheKey = null;
    private int $cacheTtl = 3600;
    
    public function withCache(string $key, int $ttl = 3600): self
    {
        $this->cacheKey = $key;
        $this->cacheTtl = $ttl;
        return $this;
    }
    
    protected function cached(callable $callback)
    {
        if (!$this->cacheKey) {
            return $callback();
        }
        
        return Cache::remember($this->cacheKey, $this->cacheTtl, $callback);
    }
}

Error Handling ​

Graceful Degradation ​

Handle errors gracefully without breaking the application:

php
class SafeSpecification extends AbstractSpecification
{
    public function __construct(
        private readonly SpecificationInterface $innerSpec,
        private readonly bool $defaultOnError = false
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        try {
            return $this->innerSpec->isSatisfiedBy($candidate);
        } catch (Throwable $e) {
            Log::warning('Specification evaluation failed', [
                'specification' => get_class($this->innerSpec),
                'error' => $e->getMessage(),
                'candidate' => $this->getCandidateInfo($candidate),
            ]);
            
            return $this->defaultOnError;
        }
    }
    
    public function toQuery($query): Builder
    {
        try {
            return $this->innerSpec->toQuery($query);
        } catch (Throwable $e) {
            Log::warning('Specification query generation failed', [
                'specification' => get_class($this->innerSpec),
                'error' => $e->getMessage(),
            ]);
            
            // Return query that matches nothing to fail safely
            return $query->whereRaw('1 = 0');
        }
    }
    
    private function getCandidateInfo(mixed $candidate): array
    {
        if (is_object($candidate)) {
            return [
                'class' => get_class($candidate),
                'id' => $candidate->id ?? null,
            ];
        }
        
        return ['type' => gettype($candidate)];
    }
}

Input Validation ​

Always validate inputs to prevent runtime errors:

php
class RobustSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        $this->validateCandidate($candidate);
        
        return $this->evaluateCandidate($candidate);
    }
    
    private function validateCandidate(mixed $candidate): void
    {
        if (!is_object($candidate)) {
            throw new InvalidArgumentException(
                'Candidate must be an object, ' . gettype($candidate) . ' given'
            );
        }
        
        if (!$candidate instanceof User) {
            throw new InvalidArgumentException(
                'Candidate must be a User instance, ' . get_class($candidate) . ' given'
            );
        }
        
        if (!isset($candidate->status)) {
            throw new InvalidArgumentException(
                'Candidate must have a status property'
            );
        }
    }
    
    private function evaluateCandidate(User $candidate): bool
    {
        return $candidate->status === 'active';
    }
}

Testing Best Practices ​

Test Organization ​

Structure your tests logically and comprehensively:

php
class UserSpecificationTest extends TestCase
{
    private ActiveUserSpecification $specification;
    
    protected function setUp(): void
    {
        parent::setUp();
        $this->specification = new ActiveUserSpecification();
    }
    
    /** @test */
    public function it_is_satisfied_by_active_users(): void
    {
        $user = User::factory()->make(['status' => 'active']);
        
        $this->assertTrue($this->specification->isSatisfiedBy($user));
    }
    
    /** @test */
    public function it_is_not_satisfied_by_inactive_users(): void
    {
        $user = User::factory()->make(['status' => 'inactive']);
        
        $this->assertFalse($this->specification->isSatisfiedBy($user));
    }
    
    /** @test */
    public function it_generates_correct_database_query(): void
    {
        $query = User::whereSpecification($this->specification);
        
        $this->assertEquals(
            'select * from "users" where "status" = ?',
            $query->toSql()
        );
        $this->assertEquals(['active'], $query->getBindings());
    }
    
    /** @test */
    public function it_handles_null_status_gracefully(): void
    {
        $user = User::factory()->make(['status' => null]);
        
        $this->assertFalse($this->specification->isSatisfiedBy($user));
    }
    
    /** @test */
    public function it_throws_exception_for_invalid_candidate(): void
    {
        $this->expectException(InvalidArgumentException::class);
        
        $this->specification->isSatisfiedBy('not a user');
    }
}

Test Data Builders ​

Use the builder pattern for complex test data:

php
class UserTestBuilder
{
    private array $attributes = [];
    
    public static function aUser(): self
    {
        return new self();
    }
    
    public function active(): self
    {
        $this->attributes['status'] = 'active';
        return $this;
    }
    
    public function inactive(): self
    {
        $this->attributes['status'] = 'inactive';
        return $this;
    }
    
    public function verified(): self
    {
        $this->attributes['email_verified_at'] = now();
        return $this;
    }
    
    public function premium(): self
    {
        $this->attributes['subscription_type'] = 'premium';
        $this->attributes['subscription_expires_at'] = now()->addYear();
        return $this;
    }
    
    public function withOrders(int $count): self
    {
        $this->attributes['_orders_count'] = $count;
        return $this;
    }
    
    public function build(): User
    {
        $user = User::factory()->make($this->attributes);
        
        if (isset($this->attributes['_orders_count'])) {
            $user->setRelation('orders', 
                Order::factory()->count($this->attributes['_orders_count'])->make()
            );
        }
        
        return $user;
    }
}

// Usage in tests
public function test_premium_user_specification(): void
{
    $premiumUser = UserTestBuilder::aUser()
        ->active()
        ->verified()
        ->premium()
        ->withOrders(5)
        ->build();
    
    $spec = new PremiumUserSpecification();
    
    $this->assertTrue($spec->isSatisfiedBy($premiumUser));
}

Performance Best Practices ​

Lazy Evaluation ​

Implement lazy evaluation for expensive operations:

php
class LazyCompositeSpecification extends AbstractSpecification
{
    private array $specifications;
    private ?bool $cachedResult = null;
    
    public function __construct(SpecificationInterface ...$specifications)
    {
        $this->specifications = $specifications;
    }
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if ($this->cachedResult !== null) {
            return $this->cachedResult;
        }
        
        // Short-circuit evaluation - stop at first false
        foreach ($this->specifications as $spec) {
            if (!$spec->isSatisfiedBy($candidate)) {
                return $this->cachedResult = false;
            }
        }
        
        return $this->cachedResult = true;
    }
    
    public function resetCache(): void
    {
        $this->cachedResult = null;
    }
}

Query Optimization ​

Always consider database performance:

php
class OptimizedSpecification extends AbstractSpecification
{
    public function toQuery($query): Builder
    {
        // Use indexes effectively
        return $query
            ->where('status', 'active') // Indexed column first
            ->where('created_at', '>', now()->subYear()) // Range on indexed column
            ->with(['profile']) // Eager load to prevent N+1
            ->select(['id', 'name', 'status', 'created_at']); // Only needed columns
    }
}

Documentation Standards ​

Comprehensive Documentation ​

Document specifications thoroughly:

php
/**
 * Determines if a user is eligible for premium features.
 * 
 * A user is eligible for premium features if they:
 * - Have an active subscription
 * - Subscription hasn't expired
 * - Account is in good standing (not suspended)
 * 
 * @example
 * $spec = new PremiumUserSpecification();
 * if ($spec->isSatisfiedBy($user)) {
 *     // Show premium features
 * }
 * 
 * @example Database query
 * User::whereSpecification(new PremiumUserSpecification())->get();
 * 
 * @see SubscriptionSpecification
 * @see AccountStatusSpecification
 */
class PremiumUserSpecification extends AbstractSpecification
{
    /**
     * Check if user satisfies premium eligibility requirements.
     * 
     * @param User $candidate The user to evaluate
     * @return bool True if user is eligible for premium features
     * @throws InvalidArgumentException If candidate is not a User instance
     */
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof User) {
            throw new InvalidArgumentException('Candidate must be a User instance');
        }
        
        return $candidate->subscription_type === 'premium'
            && $candidate->subscription_expires_at > now()
            && $candidate->status !== 'suspended';
    }
    
    /**
     * Generate database query for premium users.
     * 
     * @param Builder $query The query builder instance
     * @return Builder The modified query builder
     */
    public function toQuery($query): Builder
    {
        return $query->where('subscription_type', 'premium')
                    ->where('subscription_expires_at', '>', now())
                    ->where('status', '!=', 'suspended');
    }
}

Usage Examples ​

Always provide clear usage examples:

php
/**
 * @example Basic usage
 * $spec = new ActiveUserSpecification();
 * $isActive = $spec->isSatisfiedBy($user);
 * 
 * @example Database filtering
 * $activeUsers = User::whereSpecification(new ActiveUserSpecification())->get();
 * 
 * @example Composition
 * $eligibleSpec = (new ActiveUserSpecification())
 *     ->and(new VerifiedUserSpecification())
 *     ->and(new PremiumUserSpecification());
 * 
 * @example Collection filtering
 * $activeUsers = $users->filter(fn($user) => 
 *     (new ActiveUserSpecification())->isSatisfiedBy($user)
 * );
 */
class ActiveUserSpecification extends AbstractSpecification
{
    // Implementation...
}

Security Best Practices ​

Input Sanitization ​

Always sanitize inputs, especially in specifications that handle user data:

php
class SecureUserSearchSpecification extends AbstractSpecification
{
    public function __construct(
        private readonly string $searchTerm
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        $sanitizedTerm = $this->sanitizeSearchTerm($this->searchTerm);
        
        return str_contains(
            strtolower($candidate->name),
            strtolower($sanitizedTerm)
        );
    }
    
    public function toQuery($query): Builder
    {
        $sanitizedTerm = $this->sanitizeSearchTerm($this->searchTerm);
        
        return $query->where('name', 'ILIKE', "%{$sanitizedTerm}%");
    }
    
    private function sanitizeSearchTerm(string $term): string
    {
        // Remove potentially dangerous characters
        $term = preg_replace('/[^\w\s-]/', '', $term);
        
        // Limit length to prevent DoS
        return substr(trim($term), 0, 100);
    }
}

Access Control ​

Implement proper access control in specifications:

php
class SecureAccessSpecification extends AbstractSpecification
{
    public function __construct(
        private readonly User $currentUser
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof AccessibleResource) {
            return false;
        }
        
        // Check ownership
        if ($candidate->user_id === $this->currentUser->id) {
            return true;
        }
        
        // Check role-based access
        if ($this->currentUser->hasRole('admin')) {
            return true;
        }
        
        // Check specific permissions
        return $this->currentUser->can('view', $candidate);
    }
}

Maintenance and Evolution ​

Versioning Strategy ​

Plan for specification evolution:

php
namespace App\Specifications\User\V1;

/**
 * @deprecated Use App\Specifications\User\V2\PremiumUserSpecification
 */
class PremiumUserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        // Legacy logic
        return $candidate->subscription_level >= 2;
    }
}

namespace App\Specifications\User\V2;

class PremiumUserSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        // New logic with subscription types
        return in_array($candidate->subscription_type, ['premium', 'enterprise']);
    }
}

Migration Helpers ​

Provide tools to migrate between specification versions:

php
class SpecificationMigrator
{
    public function migrateToV2(SpecificationInterface $oldSpec): SpecificationInterface
    {
        return match (get_class($oldSpec)) {
            V1\PremiumUserSpecification::class => new V2\PremiumUserSpecification(),
            V1\ActiveUserSpecification::class => new V2\ActiveUserSpecification(),
            default => throw new InvalidArgumentException('Unknown specification for migration')
        };
    }
}

Team Development ​

Code Review Checklist ​

Use this checklist for specification code reviews:

  • [ ] Single Responsibility: Does the specification have one clear purpose?
  • [ ] Naming: Is the name descriptive and follows conventions?
  • [ ] Documentation: Are the purpose and usage clearly documented?
  • [ ] Tests: Are there comprehensive unit and integration tests?
  • [ ] Error Handling: Are edge cases and errors handled gracefully?
  • [ ] Performance: Are database queries optimized and indexes considered?
  • [ ] Consistency: Do isSatisfiedBy and toQuery methods return consistent results?
  • [ ] Security: Are inputs validated and sanitized?

Team Guidelines ​

Establish team guidelines for specification development:

php
// Team coding standards example
abstract class TeamSpecificationBase extends AbstractSpecification
{
    /**
     * All team specifications must implement this method
     * to provide human-readable explanation of what they do.
     */
    abstract public function getDescription(): string;
    
    /**
     * All team specifications must provide usage examples.
     */
    abstract public function getExamples(): array;
    
    /**
     * Standard validation for all team specifications.
     */
    protected function validateCandidate(mixed $candidate, string $expectedType): void
    {
        if (!is_object($candidate)) {
            throw new InvalidArgumentException(
                "Candidate must be an object, " . gettype($candidate) . " given"
            );
        }
        
        if (!$candidate instanceof $expectedType) {
            throw new InvalidArgumentException(
                "Candidate must be instance of {$expectedType}, " . get_class($candidate) . " given"
            );
        }
    }
}

Conclusion ​

Following these best practices will help you build:

  • Maintainable specifications that are easy to understand and modify
  • Testable code with comprehensive coverage
  • Performant applications that scale
  • Secure implementations that protect your data
  • Team-friendly code that supports collaboration

Remember: specifications are not just about code organizationβ€”they're about capturing and expressing your business logic in a way that's both human and machine readable.


Best Practices Summary

  • Keep specifications focused and single-purpose
  • Use clear, business-focused naming
  • Organize by domain, not technical concerns
  • Test thoroughly with realistic scenarios
  • Document comprehensively with examples
  • Consider performance and security implications
  • Plan for evolution and maintenance

What's Next? ​

You've completed the Complete Guide! Ready to go deeper?

Explore Deep Dive β†’

Released under the MIT License.