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:
if ($customer->created_at <= now()->subYear() &&
$customer->total_spent >= 10000 &&
!$customer->has_payment_issues) {
// Apply discount
}
With Specifications, developer codes:
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
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:
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
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
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
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
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
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
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
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
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
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
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
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
/**
* 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
// Bad: Technical thinking
class UserWithEmailAndActiveStatus extends AbstractSpecification {}
// Good: Domain thinking
class ContactableCustomer extends AbstractSpecification {}
Express Business Invariants
// Bad: Scattered validation
if ($order->total > 0 && count($order->items) > 0 && $order->customer) {}
// Good: Named business invariant
class ValidOrderInvariant extends AbstractSpecification {}
Capture Domain Knowledge
// 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
Evans, Eric (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Professional. ISBN 0-321-12521-5.
Vernon, Vaughn (2013). Implementing Domain-Driven Design. Addison-Wesley Professional. ISBN 0-321-83457-4.
Vernon, Vaughn (2016). Domain-Driven Design Distilled. Addison-Wesley Professional. ISBN 0-134-43442-7.
Fowler, Martin (2003). Patterns of Enterprise Application Architecture. Addison-Wesley Professional. ISBN 0-321-12742-0.
Nilsson, Jimmy (2006). Applying Domain-Driven Design and Patterns. Addison-Wesley Professional. ISBN 0-321-26820-2.
Avram, Abel & Marinescu, Floyd (2006). Domain-Driven Design Quickly. InfoQ. Available online: domainlanguage.com/ddd/
Evans, Eric (2014). Domain-Driven Design Reference: Definitions and Pattern Summaries. Domain Language, Inc.
Millett, Scott & Tune, Nick (2015). Patterns, Principles, and Practices of Domain-Driven Design. Wrox Press. ISBN 1-118-71415-6.