Browse TypeScript Design Patterns & Application Architecture

Specification Pattern: Enhancing Flexibility and Reusability in TypeScript

Explore the Specification Pattern in TypeScript, a powerful design pattern for combining business rules and logic to evaluate objects, enhancing code flexibility and reusability.

6.12 Specification Pattern

The Specification Pattern is a powerful tool in the software engineer’s toolkit, offering a way to encapsulate business rules and logic in a reusable and flexible manner. This pattern is particularly useful in scenarios where complex conditional logic is scattered throughout the codebase, leading to maintenance challenges and reduced readability. By employing the Specification Pattern, developers can achieve a cleaner separation of concerns and enhance the reusability of their code.

Understanding the Specification Pattern

The Specification Pattern is a behavioral design pattern that allows you to encapsulate business rules or criteria in a separate class, called a specification. This specification can then be used to evaluate objects against these criteria. The key intent of the Specification Pattern is to promote the reusability of business logic and facilitate the combination of different specifications using logical operations such as AND, OR, and NOT.

Intent of the Specification Pattern

The primary intent of the Specification Pattern is to:

  • Encapsulate Business Rules: By defining business rules in separate specification classes, you can isolate the logic from the rest of the application code.
  • Promote Reusability: Specifications can be reused across different parts of the application, reducing code duplication.
  • Enhance Flexibility: Specifications can be combined using logical operators, allowing for complex criteria to be constructed in a flexible manner.
  • Improve Readability: By moving complex conditional logic into specification classes, the main application code becomes more readable and maintainable.

Problems Solved by the Specification Pattern

In many applications, business rules are often implemented using conditional statements scattered throughout the codebase. This approach can lead to several problems:

  • Code Duplication: The same business logic may be repeated in multiple places, making it difficult to update or modify.
  • Reduced Readability: Complex conditional logic can make the code hard to read and understand.
  • Tight Coupling: Business rules are often tightly coupled with the application logic, making it difficult to change one without affecting the other.
  • Limited Reusability: Business logic implemented as inline conditions is not easily reusable across different parts of the application.

The Specification Pattern addresses these issues by encapsulating business rules in separate specification classes, which can be reused, combined, and modified independently of the application logic.

Implementing the Specification Pattern in TypeScript

Let’s explore how to implement the Specification Pattern in TypeScript. We’ll start by defining a basic specification interface and then create concrete specifications for different business rules. Finally, we’ll demonstrate how to combine these specifications using logical operations.

Defining the Specification Interface

The first step in implementing the Specification Pattern is to define a specification interface. This interface will declare a method for evaluating an object against the specification’s criteria.

1// Specification.ts
2export interface Specification<T> {
3  isSatisfiedBy(candidate: T): boolean;
4}

In this example, the Specification interface declares a single method, isSatisfiedBy, which takes a candidate object of type T and returns a boolean indicating whether the candidate satisfies the specification.

Creating Concrete Specifications

Next, we’ll create concrete specifications that implement the Specification interface. Each concrete specification will encapsulate a specific business rule or criterion.

 1// AgeSpecification.ts
 2import { Specification } from './Specification';
 3
 4export class AgeSpecification implements Specification<Person> {
 5  constructor(private minAge: number) {}
 6
 7  isSatisfiedBy(candidate: Person): boolean {
 8    return candidate.age >= this.minAge;
 9  }
10}
11
12// NameSpecification.ts
13import { Specification } from './Specification';
14
15export class NameSpecification implements Specification<Person> {
16  constructor(private name: string) {}
17
18  isSatisfiedBy(candidate: Person): boolean {
19    return candidate.name === this.name;
20  }
21}

In these examples, we define two concrete specifications: AgeSpecification and NameSpecification. The AgeSpecification checks if a person’s age is greater than or equal to a specified minimum age, while the NameSpecification checks if a person’s name matches a specified name.

Combining Specifications with Logical Operations

One of the key features of the Specification Pattern is the ability to combine specifications using logical operations. We’ll define composite specifications for AND, OR, and NOT operations.

 1// AndSpecification.ts
 2import { Specification } from './Specification';
 3
 4export class AndSpecification<T> implements Specification<T> {
 5  constructor(private spec1: Specification<T>, private spec2: Specification<T>) {}
 6
 7  isSatisfiedBy(candidate: T): boolean {
 8    return this.spec1.isSatisfiedBy(candidate) && this.spec2.isSatisfiedBy(candidate);
 9  }
10}
11
12// OrSpecification.ts
13import { Specification } from './Specification';
14
15export class OrSpecification<T> implements Specification<T> {
16  constructor(private spec1: Specification<T>, private spec2: Specification<T>) {}
17
18  isSatisfiedBy(candidate: T): boolean {
19    return this.spec1.isSatisfiedBy(candidate) || this.spec2.isSatisfiedBy(candidate);
20  }
21}
22
23// NotSpecification.ts
24import { Specification } from './Specification';
25
26export class NotSpecification<T> implements Specification<T> {
27  constructor(private spec: Specification<T>) {}
28
29  isSatisfiedBy(candidate: T): boolean {
30    return !this.spec.isSatisfiedBy(candidate);
31  }
32}

In these examples, the AndSpecification, OrSpecification, and NotSpecification classes allow us to combine existing specifications using logical AND, OR, and NOT operations, respectively.

Using the Specification Pattern

Let’s see how we can use the Specification Pattern to evaluate objects against complex criteria.

 1// Person.ts
 2export interface Person {
 3  name: string;
 4  age: number;
 5}
 6
 7// Main.ts
 8import { AgeSpecification } from './AgeSpecification';
 9import { NameSpecification } from './NameSpecification';
10import { AndSpecification } from './AndSpecification';
11
12const person: Person = { name: 'Alice', age: 30 };
13
14const ageSpec = new AgeSpecification(18);
15const nameSpec = new NameSpecification('Alice');
16
17const adultNamedAliceSpec = new AndSpecification(ageSpec, nameSpec);
18
19console.log(adultNamedAliceSpec.isSatisfiedBy(person)); // Output: true

In this example, we define a Person interface and create a Person object named Alice who is 30 years old. We then create an AgeSpecification to check if a person is at least 18 years old and a NameSpecification to check if a person’s name is Alice. Finally, we combine these specifications using an AndSpecification to check if Alice is an adult named Alice.

Visualizing the Specification Pattern

To better understand how the Specification Pattern works, let’s visualize the relationships between specifications and how they are combined.

    classDiagram
	    class Specification {
	        <<interface>>
	        +isSatisfiedBy(candidate: T) boolean
	    }
	    class AgeSpecification {
	        +isSatisfiedBy(candidate: Person) boolean
	    }
	    class NameSpecification {
	        +isSatisfiedBy(candidate: Person) boolean
	    }
	    class AndSpecification {
	        +isSatisfiedBy(candidate: T) boolean
	    }
	    class OrSpecification {
	        +isSatisfiedBy(candidate: T) boolean
	    }
	    class NotSpecification {
	        +isSatisfiedBy(candidate: T) boolean
	    }
	    Specification <|.. AgeSpecification
	    Specification <|.. NameSpecification
	    Specification <|.. AndSpecification
	    Specification <|.. OrSpecification
	    Specification <|.. NotSpecification
	    AndSpecification --> Specification
	    OrSpecification --> Specification
	    NotSpecification --> Specification

Diagram Description: This class diagram illustrates the Specification Pattern. The Specification interface is implemented by concrete specifications such as AgeSpecification and NameSpecification. Composite specifications like AndSpecification, OrSpecification, and NotSpecification combine existing specifications using logical operations.

Benefits of the Specification Pattern

The Specification Pattern offers several benefits that make it a valuable tool for software engineers:

  • Separation of Concerns: By encapsulating business rules in separate specification classes, the pattern promotes a clear separation of concerns between business logic and application logic.
  • Reusability: Specifications can be reused across different parts of the application, reducing code duplication and improving maintainability.
  • Flexibility: The ability to combine specifications using logical operations allows for flexible and dynamic construction of complex criteria.
  • Readability: By moving complex conditional logic into specification classes, the main application code becomes more readable and easier to understand.

Try It Yourself

To deepen your understanding of the Specification Pattern, try modifying the code examples provided:

  1. Add a New Specification: Create a new specification class, such as LocationSpecification, that checks if a person’s location matches a specified value.
  2. Combine Specifications: Use the new specification in combination with existing specifications using logical operations.
  3. Test Different Scenarios: Create different Person objects and test them against various combinations of specifications to see how the pattern handles different criteria.

References and Further Reading

Knowledge Check

To reinforce your understanding of the Specification Pattern, consider the following questions:

  • How does the Specification Pattern promote separation of concerns?
  • What are the benefits of combining specifications using logical operations?
  • How can the Specification Pattern improve code readability and maintainability?

Embrace the Journey

Remember, mastering design patterns like the Specification Pattern is a journey. As you continue to explore and apply these patterns in your projects, you’ll gain a deeper understanding of their benefits and how they can enhance your codebase. Keep experimenting, stay curious, and enjoy the journey!

Quiz Time!

Loading quiz…

In this section

Revised on Thursday, April 23, 2026