Package Exports
- @rolandsall24/specification-pattern
 - @rolandsall24/specification-pattern/package.json
 
Readme
Specification Pattern
A TypeScript implementation of the Specification Pattern for Domain-Driven Design, enabling composable business rules with type safety.
Features
- Clean implementation of the DDD Specification Pattern
 - Type-safe specification composition with TypeScript generics
 - Logical operators (AND, OR, NOT) for combining specifications
 - Zero runtime dependencies
 - Framework-agnostic design
 - Comprehensive error messages for debugging
 - Easy to extend and customize
 
Installation
npm install @rolandsall24/specification-patternWhat is the Specification Pattern?
The Specification Pattern is a Domain-Driven Design pattern that encapsulates business rules into reusable, combinable objects. It allows you to:
- Express complex business rules in a clear, maintainable way
 - Combine simple specifications into complex ones using logical operators
 - Keep your domain logic separate from infrastructure concerns
 - Make business rules testable and reusable
 
Quick Start
1. Create a Specification
Extend CompositeSpecification and implement the required methods:
import { CompositeSpecification } from '@rolandsall24/specification-pattern';
interface User {
  age: number;
  isActive: boolean;
  email: string;
}
class IsAdultSpecification extends CompositeSpecification<User> {
  isSatisfiedBy(user: User): boolean {
    return user.age >= 18;
  }
  getErrorMessage(): string {
    return 'User must be at least 18 years old';
  }
}
class IsActiveUserSpecification extends CompositeSpecification<User> {
  isSatisfiedBy(user: User): boolean {
    return user.isActive;
  }
  getErrorMessage(): string {
    return 'User must be active';
  }
}2. Use Specifications
const user = {
  age: 25,
  isActive: true,
  email: 'john@example.com'
};
const isAdult = new IsAdultSpecification();
const isActive = new IsActiveUserSpecification();
// Check individual specifications
if (isAdult.isSatisfiedBy(user)) {
  console.log('User is an adult');
}
// Combine specifications using AND
const canPurchase = isAdult.and(isActive);
if (canPurchase.isSatisfiedBy(user)) {
  console.log('User can make purchases');
} else {
  console.log(canPurchase.getErrorMessage());
}3. Combine Specifications
Specifications can be combined using logical operators:
// AND: Both must be satisfied
const adultAndActive = isAdult.and(isActive);
// OR: At least one must be satisfied
const adultOrActive = isAdult.or(isActive);
// NOT: Must not be satisfied
const notAdult = isAdult.not();
// Complex combinations
const eligibleUser = isAdult.and(isActive).and(hasVerifiedEmail);Advanced Usage
Domain Validation Example
import { CompositeSpecification } from '@rolandsall24/specification-pattern';
interface Order {
  total: number;
  items: number;
  customerId: string;
  isPaid: boolean;
}
class MinimumOrderValueSpecification extends CompositeSpecification<Order> {
  constructor(private minValue: number) {
    super();
  }
  isSatisfiedBy(order: Order): boolean {
    return order.total >= this.minValue;
  }
  getErrorMessage(): string {
    return `Order total must be at least $${this.minValue}`;
  }
}
class HasItemsSpecification extends CompositeSpecification<Order> {
  isSatisfiedBy(order: Order): boolean {
    return order.items > 0;
  }
  getErrorMessage(): string {
    return 'Order must contain at least one item';
  }
}
class IsPaidSpecification extends CompositeSpecification<Order> {
  isSatisfiedBy(order: Order): boolean {
    return order.isPaid;
  }
  getErrorMessage(): string {
    return 'Order must be paid';
  }
}
// Usage
const order = {
  total: 150,
  items: 3,
  customerId: 'cust-123',
  isPaid: true
};
const canShipOrder = new MinimumOrderValueSpecification(50)
  .and(new HasItemsSpecification())
  .and(new IsPaidSpecification());
if (canShipOrder.isSatisfiedBy(order)) {
  console.log('Order can be shipped');
} else {
  console.log('Cannot ship order:', canShipOrder.getErrorMessage());
}Parameterized Specifications
Create reusable specifications with parameters:
class MinimumAgeSpecification extends CompositeSpecification<User> {
  constructor(private minimumAge: number) {
    super();
  }
  isSatisfiedBy(user: User): boolean {
    return user.age >= this.minimumAge;
  }
  getErrorMessage(): string {
    return `User must be at least ${this.minimumAge} years old`;
  }
}
// Create different age requirements
const canDrink = new MinimumAgeSpecification(21);
const canVote = new MinimumAgeSpecification(18);
const canRetire = new MinimumAgeSpecification(65);Business Rules Encapsulation
interface Product {
  price: number;
  stock: number;
  isActive: boolean;
  category: string;
}
class IsInStockSpecification extends CompositeSpecification<Product> {
  isSatisfiedBy(product: Product): boolean {
    return product.stock > 0;
  }
  getErrorMessage(): string {
    return 'Product is out of stock';
  }
}
class IsActiveProductSpecification extends CompositeSpecification<Product> {
  isSatisfiedBy(product: Product): boolean {
    return product.isActive;
  }
  getErrorMessage(): string {
    return 'Product is not active';
  }
}
class IsPremiumProductSpecification extends CompositeSpecification<Product> {
  isSatisfiedBy(product: Product): boolean {
    return product.price >= 100;
  }
  getErrorMessage(): string {
    return 'Product must be a premium product (price >= $100)';
  }
}
// Compose complex business rules
const canBePurchased = new IsInStockSpecification()
  .and(new IsActiveProductSpecification());
const qualifiesForFreeShipping = new IsPremiumProductSpecification()
  .and(new IsInStockSpecification());Filtering Collections
const users: User[] = [
  { age: 17, isActive: true, email: 'teen@example.com' },
  { age: 25, isActive: true, email: 'adult@example.com' },
  { age: 30, isActive: false, email: 'inactive@example.com' },
  { age: 45, isActive: true, email: 'senior@example.com' }
];
const activeAdults = new IsAdultSpecification()
  .and(new IsActiveUserSpecification());
const eligibleUsers = users.filter(user => activeAdults.isSatisfiedBy(user));
console.log(eligibleUsers);
// Output: Users aged >= 18 and activeAPI Reference
Interfaces
ISpecification<T>
The base specification interface.
interface ISpecification<T> {
  isSatisfiedBy(candidate: T): boolean;
  and(other: ISpecification<T>): ISpecification<T>;
  or(other: ISpecification<T>): ISpecification<T>;
  not(): ISpecification<T>;
  getErrorMessage(): string;
}Classes
CompositeSpecification<T>
Abstract base class for implementing specifications.
Methods:
abstract isSatisfiedBy(candidate: T): boolean- Checks if the candidate satisfies this specificationabstract getErrorMessage(): string- Returns the error message when not satisfiedand(other: ISpecification<T>): ISpecification<T>- Combines with another specification using AND logicor(other: ISpecification<T>): ISpecification<T>- Combines with another specification using OR logicnot(): ISpecification<T>- Negates this specification
Design Patterns & Best Practices
1. Single Responsibility Principle
Each specification should encapsulate one business rule:
// Good: Each specification has one responsibility
class IsAdultSpecification extends CompositeSpecification<User> { ... }
class IsActiveSpecification extends CompositeSpecification<User> { ... }
const eligibleUser = new IsAdultSpecification().and(new IsActiveSpecification());
// Bad: Specification does too much
class IsEligibleUserSpecification extends CompositeSpecification<User> {
  isSatisfiedBy(user: User): boolean {
    return user.age >= 18 && user.isActive && user.hasVerifiedEmail;
  }
}2. Composition Over Inheritance
Combine simple specifications rather than creating complex hierarchies:
// Good: Compose simple specifications
const premiumEligible = new IsAdultSpecification()
  .and(new HasPremiumAccountSpecification())
  .and(new HasValidPaymentMethodSpecification());
// Bad: Deep inheritance
class PremiumEligibleUserSpecification extends IsAdultSpecification { ... }3. Immutability
Specifications should be immutable and return new instances:
const spec1 = new IsAdultSpecification();
const spec2 = spec1.and(new IsActiveSpecification());
// spec1 remains unchanged, spec2 is a new specification4. Clear Error Messages
Provide descriptive error messages for debugging:
class MinimumBalanceSpecification extends CompositeSpecification<Account> {
  constructor(private minBalance: number) {
    super();
  }
  isSatisfiedBy(account: Account): boolean {
    return account.balance >= this.minBalance;
  }
  getErrorMessage(): string {
    return `Account balance must be at least $${this.minBalance}`;
  }
}Use Cases
The Specification Pattern is particularly useful for:
- Domain Validation: Validating entities against business rules
 - Querying: Building complex queries in a type-safe manner
 - Business Rules: Encapsulating business logic for reuse
 - Access Control: Defining permission rules
 - Filtering: Filtering collections based on criteria
 - Workflow Rules: Defining state transition rules
 
Comparison with Other Patterns
Specification vs Strategy Pattern
- Strategy: Encapsulates algorithms/behaviors
 - Specification: Encapsulates business rules and supports composition
 
Specification vs Validator
- Validator: Typically validates all rules at once
 - Specification: Allows selective, composable rule checking
 
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
Author
Roland Salloum
Related Patterns
- Domain-Driven Design (DDD)
 - Composite Pattern
 - Chain of Responsibility Pattern
 - Strategy Pattern