Skip to content

Domain-Driven Design ​

Specifications are the perfect embodiment of Domain-Driven Design principles. They transform business language into executable code, creating a bridge between domain experts and developers.

The DDD Revolution ​

Eric Evans' Vision ​

"The heart of software is its ability to solve domain-related problems for its user. All other features, vital though they may be, support this basic purpose."

— Eric Evans, Domain-Driven Design

Specifications make this vision real by:

  • Capturing business rules as first-class objects
  • Speaking the domain language in code
  • Isolating complexity in the right places
  • Making implicit concepts explicit

Ubiquitous Language Through Specifications ​

The Communication Problem ​

Domain Expert says:

"A customer is eligible for our premium discount if they've been with us for over a year, spent more than $10,000, and haven't had any payment issues."

Developer traditionally codes:

php
if ($customer->created_at <= now()->subYear() && 
    $customer->total_spent >= 10000 && 
    !$customer->has_payment_issues) {
    // Apply discount
}

With Specifications, developer codes:

php
class PremiumDiscountEligibleSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $customer): bool
    {
        return $this->isLoyalCustomer($customer)
            ->and($this->isHighValueCustomer($customer))
            ->and($this->hasGoodPaymentHistory($customer))
            ->isSatisfiedBy($customer);
    }
    
    private function isLoyalCustomer($customer): AbstractSpecification
    {
        return new CustomerLoyaltySpecification(minimumYears: 1);
    }
    
    private function isHighValueCustomer($customer): AbstractSpecification
    {
        return new CustomerValueSpecification(minimumSpent: 10000);
    }
    
    private function hasGoodPaymentHistory($customer): AbstractSpecification
    {
        return new GoodPaymentHistorySpecification();
    }
}

The code now speaks the same language as the domain expert!

Building the Language ​

php
namespace Domain\Sales\Specifications;

/**
 * Terms from our sales domain:
 * - "Qualified Lead": Has budget, authority, need, and timeline
 * - "Hot Prospect": Qualified lead with recent engagement
 * - "Champion": Internal advocate at the prospect company
 */

class QualifiedLeadSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $lead): bool
    {
        return $this->hasBudget($lead)
            ->and($this->hasAuthority($lead))
            ->and($this->hasNeed($lead))
            ->and($this->hasTimeline($lead))
            ->isSatisfiedBy($lead);
    }
}

class HotProspectSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $lead): bool
    {
        $qualified = new QualifiedLeadSpecification();
        $recentlyEngaged = new RecentEngagementSpecification(days: 7);
        
        return $qualified->and($recentlyEngaged)->isSatisfiedBy($lead);
    }
}

class ChampionIdentifiedSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $contact): bool
    {
        return $contact->engagement_score >= 80
            && $contact->has_decision_authority
            && $contact->sentiment_score >= 0.7;
    }
}

Bounded Contexts and Specifications ​

Context Boundaries ​

Different contexts have different meanings for the same concept:

php
namespace Contexts\Sales\Specifications;

class CustomerSpecification extends AbstractSpecification
{
    // In Sales context: Anyone who might buy
    public function isSatisfiedBy(mixed $entity): bool
    {
        return $entity instanceof Lead || 
               $entity instanceof Contact ||
               $entity instanceof Account;
    }
}

namespace Contexts\Billing\Specifications;

class CustomerSpecification extends AbstractSpecification
{
    // In Billing context: Only those with payment method
    public function isSatisfiedBy(mixed $entity): bool
    {
        return $entity instanceof Account &&
               $entity->hasPaymentMethod() &&
               $entity->billingAddress !== null;
    }
}

namespace Contexts\Support\Specifications;

class CustomerSpecification extends AbstractSpecification
{
    // In Support context: Anyone with support contract
    public function isSatisfiedBy(mixed $entity): bool
    {
        return $entity->supportContract !== null &&
               $entity->supportContract->isActive();
    }
}

Context Mapping with Specifications ​

php
class ContextTranslator
{
    public function translateSalesToBilling(
        Sales\Customer $salesCustomer
    ): ?Billing\Customer {
        // Use specification to check if translation is valid
        $billingRequirements = new Billing\ValidCustomerSpecification();
        
        if (!$billingRequirements->isSatisfiedBy($salesCustomer)) {
            return null; // Cannot translate - missing billing requirements
        }
        
        return new Billing\Customer([
            'account_id' => $salesCustomer->getAccountId(),
            'payment_method' => $salesCustomer->getPaymentMethod(),
            'billing_address' => $salesCustomer->getBillingAddress(),
        ]);
    }
}

Aggregates and Specifications ​

Protecting Aggregate Invariants ​

php
namespace Domain\Order;

class Order // Aggregate Root
{
    private array $items = [];
    private ?Address $shippingAddress = null;
    private string $status = 'draft';
    
    public function addItem(OrderItem $item): void
    {
        // Specification protects invariant
        $canAddItemSpec = new CanAddItemToOrderSpecification();
        
        if (!$canAddItemSpec->isSatisfiedBy([
            'order' => $this,
            'item' => $item
        ])) {
            throw new DomainException("Cannot add item to order");
        }
        
        $this->items[] = $item;
    }
    
    public function submit(): void
    {
        // Specification ensures order is valid for submission
        $submittableSpec = new OrderSubmittableSpecification();
        
        if (!$submittableSpec->isSatisfiedBy($this)) {
            throw new DomainException(
                "Order does not meet submission requirements: " .
                $submittableSpec->getUnmetRequirements($this)
            );
        }
        
        $this->status = 'submitted';
        $this->raise(new OrderSubmittedEvent($this));
    }
}

class OrderSubmittableSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $order): bool
    {
        return $this->hasItems($order)
            ->and($this->hasValidShippingAddress($order))
            ->and($this->hasValidPaymentMethod($order))
            ->and($this->meetsMinimumValue($order))
            ->isSatisfiedBy($order);
    }
    
    public function getUnmetRequirements(Order $order): string
    {
        $requirements = [];
        
        if (!$this->hasItems($order)->isSatisfiedBy($order)) {
            $requirements[] = "Order must have at least one item";
        }
        
        if (!$this->hasValidShippingAddress($order)->isSatisfiedBy($order)) {
            $requirements[] = "Valid shipping address required";
        }
        
        // ... check other requirements
        
        return implode(", ", $requirements);
    }
}

Aggregate Selection with Specifications ​

php
class OrderRepository
{
    public function findReadyToShip(): Collection
    {
        $readyToShipSpec = new OrderReadyToShipSpecification();
        
        return Order::whereSpecification($readyToShipSpec)
            ->with(['items', 'shipping', 'customer'])
            ->get();
    }
    
    public function findRequiringAttention(): Collection
    {
        $attentionSpec = new OrderRequiresAttentionSpecification();
        
        return Order::whereSpecification($attentionSpec)
            ->orderBy('priority', 'desc')
            ->get();
    }
}

Value Objects and Specifications ​

Validating Value Objects ​

php
namespace Domain\Shared\ValueObjects;

class Email
{
    private string $value;
    
    public function __construct(string $email)
    {
        $spec = new ValidEmailSpecification();
        
        if (!$spec->isSatisfiedBy($email)) {
            throw new InvalidArgumentException(
                "Invalid email format: {$email}"
            );
        }
        
        $this->value = $email;
    }
}

class Money
{
    private float $amount;
    private string $currency;
    
    public function __construct(float $amount, string $currency)
    {
        $spec = new ValidMoneySpecification();
        
        if (!$spec->isSatisfiedBy(['amount' => $amount, 'currency' => $currency])) {
            throw new InvalidArgumentException(
                "Invalid money: {$amount} {$currency}"
            );
        }
        
        $this->amount = $amount;
        $this->currency = $currency;
    }
    
    public function isGreaterThan(Money $other): bool
    {
        $spec = new MoneyComparisonSpecification($this, '>');
        return $spec->isSatisfiedBy($other);
    }
}

Domain Events and Specifications ​

Event-Driven Specifications ​

php
class OrderEventHandler
{
    public function handle(DomainEvent $event): void
    {
        // Different specifications trigger different workflows
        $this->checkPromotionalUpgrade($event);
        $this->checkFraudRisk($event);
        $this->checkInventoryAlerts($event);
    }
    
    private function checkPromotionalUpgrade(DomainEvent $event): void
    {
        if (!$event instanceof OrderPlacedEvent) {
            return;
        }
        
        $upgradeEligibleSpec = new PromotionalUpgradeEligibleSpecification();
        
        if ($upgradeEligibleSpec->isSatisfiedBy($event->getOrder())) {
            dispatch(new OfferPromotionalUpgrade($event->getOrder()));
        }
    }
    
    private function checkFraudRisk(DomainEvent $event): void
    {
        $highRiskSpec = new HighFraudRiskSpecification();
        
        if ($highRiskSpec->isSatisfiedBy($event->getOrder())) {
            dispatch(new ReviewOrderForFraud($event->getOrder()));
        }
    }
}

Specification-Based Sagas ​

php
class OrderFulfillmentSaga
{
    private array $steps = [];
    
    public function __construct()
    {
        $this->defineSteps();
    }
    
    private function defineSteps(): void
    {
        $this->steps = [
            'payment_captured' => new PaymentCapturedSpecification(),
            'inventory_reserved' => new InventoryReservedSpecification(),
            'shipping_arranged' => new ShippingArrangedSpecification(),
            'customer_notified' => new CustomerNotifiedSpecification(),
        ];
    }
    
    public function getNextStep(Order $order): ?string
    {
        foreach ($this->steps as $step => $specification) {
            if (!$specification->isSatisfiedBy($order)) {
                return $step;
            }
        }
        
        return null; // All steps complete
    }
    
    public function isComplete(Order $order): bool
    {
        $completeSpec = array_reduce(
            $this->steps,
            fn($carry, $spec) => $carry ? $carry->and($spec) : $spec
        );
        
        return $completeSpec->isSatisfiedBy($order);
    }
}

Domain Services with Specifications ​

Encapsulating Complex Business Logic ​

php
namespace Domain\Pricing\Services;

class PricingService
{
    private array $pricingRules = [];
    
    public function __construct()
    {
        $this->initializePricingRules();
    }
    
    private function initializePricingRules(): void
    {
        $this->pricingRules = [
            'volume_discount' => [
                'specification' => new VolumeDiscountEligibleSpecification(),
                'calculator' => new VolumeDiscountCalculator(),
            ],
            'loyalty_discount' => [
                'specification' => new LoyaltyDiscountEligibleSpecification(),
                'calculator' => new LoyaltyDiscountCalculator(),
            ],
            'seasonal_promotion' => [
                'specification' => new SeasonalPromotionActiveSpecification(),
                'calculator' => new SeasonalPromotionCalculator(),
            ],
            'bundle_discount' => [
                'specification' => new BundleDiscountApplicableSpecification(),
                'calculator' => new BundleDiscountCalculator(),
            ],
        ];
    }
    
    public function calculatePrice(Order $order, Customer $customer): Money
    {
        $basePrice = $order->getSubtotal();
        $context = new PricingContext($order, $customer);
        
        // Apply all applicable discounts
        foreach ($this->pricingRules as $rule) {
            if ($rule['specification']->isSatisfiedBy($context)) {
                $basePrice = $rule['calculator']->apply($basePrice, $context);
            }
        }
        
        return $basePrice;
    }
}

Anti-Corruption Layer with Specifications ​

Protecting Your Domain ​

php
namespace Infrastructure\Integration;

class ExternalSystemAdapter
{
    private array $translationSpecs = [];
    
    public function importCustomer(array $externalData): ?Customer
    {
        // Use specifications to validate external data
        $validDataSpec = new ValidExternalCustomerDataSpecification();
        
        if (!$validDataSpec->isSatisfiedBy($externalData)) {
            $this->logInvalidData($externalData, $validDataSpec->getViolations());
            return null;
        }
        
        // Transform only if data meets our domain requirements
        $meetsDomainRequirements = new MeetsDomainCustomerRequirementsSpecification();
        
        if (!$meetsDomainRequirements->isSatisfiedBy($externalData)) {
            return null; // Don't pollute our domain with invalid data
        }
        
        return $this->transformToCustomer($externalData);
    }
    
    private function transformToCustomer(array $data): Customer
    {
        // Clean transformation with confidence
        return new Customer(
            email: new Email($data['email']),
            name: new PersonName($data['first_name'], $data['last_name']),
            status: $this->mapStatus($data['status'])
        );
    }
}

Strategic Design with Specifications ​

Core Domain Specifications ​

php
namespace Domain\Core;

/**
 * These specifications represent our competitive advantage
 * They encode the unique business rules that differentiate us
 */

class ProprietaryRiskAssessmentSpecification extends AbstractSpecification
{
    // This is our secret sauce - complex risk calculation
    public function isSatisfiedBy(mixed $application): bool
    {
        return $this->proprietaryAlgorithm($application) < 0.3;
    }
    
    private function proprietaryAlgorithm($application): float
    {
        // Complex domain logic that gives us competitive advantage
        // This is what makes our business unique
    }
}

Supporting Domain Specifications ​

php
namespace Domain\Supporting;

/**
 * Standard business rules that support our core domain
 */

class StandardCreditCheckSpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $customer): bool
    {
        // Industry-standard credit check
        return $customer->creditScore >= 650;
    }
}

Generic Domain Specifications ​

php
namespace Domain\Generic;

/**
 * Common specifications used across the enterprise
 */

class ActiveEntitySpecification extends AbstractSpecification
{
    public function isSatisfiedBy(mixed $entity): bool
    {
        return $entity->isActive() && !$entity->isDeleted();
    }
}

DDD Patterns in Specifications ​

Specification as Domain Concept ​

php
/**
 * When a specification itself becomes a domain concept
 */
class CreditPolicy // The specification IS the domain concept
{
    private string $id;
    private string $name;
    private array $rules;
    private SpecificationInterface $specification;
    
    public function __construct(string $name, array $rules)
    {
        $this->id = Uuid::generate();
        $this->name = $name;
        $this->rules = $rules;
        $this->specification = $this->buildSpecification($rules);
    }
    
    public function evaluate(CreditApplication $application): CreditDecision
    {
        if ($this->specification->isSatisfiedBy($application)) {
            return CreditDecision::approved($this);
        }
        
        return CreditDecision::declined(
            $this,
            $this->specification->getUnmetRequirements($application)
        );
    }
    
    private function buildSpecification(array $rules): SpecificationInterface
    {
        return new CompositeCreditSpecification($rules);
    }
}

The DDD Specification Mindset ​

Think in Domain Terms ​

php
// Bad: Technical thinking
class UserWithEmailAndActiveStatus extends AbstractSpecification {}

// Good: Domain thinking
class ContactableCustomer extends AbstractSpecification {}

Express Business Invariants ​

php
// Bad: Scattered validation
if ($order->total > 0 && count($order->items) > 0 && $order->customer) {}

// Good: Named business invariant
class ValidOrderInvariant extends AbstractSpecification {}

Capture Domain Knowledge ​

php
// Bad: Magic numbers and conditions
if ($customer->score > 750 && $customer->history > 5) {}

// Good: Domain knowledge captured
class PlatinumTierCustomer extends AbstractSpecification {
    // The specification documents and enforces the business rule
}

Your DDD Journey with Specifications ​

Specifications are your tools for:

  • Speaking the ubiquitous language
  • Protecting aggregate invariants
  • Defining context boundaries
  • Expressing domain concepts
  • Enforcing business rules

DDD Mastery

Specifications turn Domain-Driven Design from theory into practice, making your code speak the language of your business.

Ready for Enterprise Scale? ​

Now that you understand DDD with specifications, learn how to scale these patterns to enterprise architectures.

Explore Enterprise Architecture →

References ​

  1. Evans, Eric (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Professional. ISBN 0-321-12521-5.

  2. Vernon, Vaughn (2013). Implementing Domain-Driven Design. Addison-Wesley Professional. ISBN 0-321-83457-4.

  3. Vernon, Vaughn (2016). Domain-Driven Design Distilled. Addison-Wesley Professional. ISBN 0-134-43442-7.

  4. Fowler, Martin (2003). Patterns of Enterprise Application Architecture. Addison-Wesley Professional. ISBN 0-321-12742-0.

  5. Nilsson, Jimmy (2006). Applying Domain-Driven Design and Patterns. Addison-Wesley Professional. ISBN 0-321-26820-2.

  6. Avram, Abel & Marinescu, Floyd (2006). Domain-Driven Design Quickly. InfoQ. Available online: domainlanguage.com/ddd/

  7. Evans, Eric (2014). Domain-Driven Design Reference: Definitions and Pattern Summaries. Domain Language, Inc.

  8. Millett, Scott & Tune, Nick (2015). Patterns, Principles, and Practices of Domain-Driven Design. Wrox Press. ISBN 1-118-71415-6.

Released under the MIT License.