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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
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:
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:
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:
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:
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:
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:
/**
* 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:
/**
* @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:
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:
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:
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:
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
andtoQuery
methods return consistent results? - [ ] Security: Are inputs validated and sanitized?
Team Guidelines β
Establish team guidelines for specification development:
// 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?
- Deep Dive Section β - Explore theoretical foundations and advanced patterns
- Examples Section β - Real-world case studies and implementations
- API Reference β - Complete technical documentation