JSPM

@rolandsall24/specification-pattern

0.1.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 24
  • Score
    100M100P100Q66234F
  • License MIT

A TypeScript implementation of the Specification Pattern for Domain-Driven Design, enabling composable business rules with type safety

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-pattern

What 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 active

API 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 specification
  • abstract getErrorMessage(): string - Returns the error message when not satisfied
  • and(other: ISpecification<T>): ISpecification<T> - Combines with another specification using AND logic
  • or(other: ISpecification<T>): ISpecification<T> - Combines with another specification using OR logic
  • not(): 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 specification

4. 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:

  1. Domain Validation: Validating entities against business rules
  2. Querying: Building complex queries in a type-safe manner
  3. Business Rules: Encapsulating business logic for reuse
  4. Access Control: Defining permission rules
  5. Filtering: Filtering collections based on criteria
  6. 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

  • Domain-Driven Design (DDD)
  • Composite Pattern
  • Chain of Responsibility Pattern
  • Strategy Pattern

Resources