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.