@rolandsall24/specification-pattern
v0.1.1
Published
A TypeScript implementation of the Specification Pattern for Domain-Driven Design, enabling composable business rules with type safety
Downloads
28
Maintainers
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: '[email protected]'
};
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: '[email protected]' },
{ age: 25, isActive: true, email: '[email protected]' },
{ age: 30, isActive: false, email: '[email protected]' },
{ age: 45, isActive: true, email: '[email protected]' }
];
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
