Browse TypeScript Design Patterns & Application Architecture

Combining Business Rules with the Specification Pattern in TypeScript

Explore how the Specification Pattern in TypeScript enables the combination of business rules using logical operations, enhancing code readability and maintainability.

6.12.2 Combining Business Rules with the Specification Pattern in TypeScript

In the realm of software engineering, especially when dealing with complex business logic, the ability to express and manage rules efficiently is paramount. The Specification Pattern is a powerful tool in this regard, allowing developers to encapsulate business rules in a reusable and combinable manner. This section delves into how the Specification Pattern can be utilized in TypeScript to combine business rules, enhancing code readability and maintainability.

Understanding the Specification Pattern

The Specification Pattern is a design pattern that allows you to encapsulate business rules in objects, enabling the combination of these rules using logical operations. This pattern is particularly useful when you need to evaluate whether an object meets certain criteria, and it provides a way to build complex conditions without resorting to intricate conditional statements.

Key Concepts

  • Specification: An interface or abstract class that defines a method for checking if an object satisfies a particular condition.
  • Composite Specification: A specification that combines multiple specifications using logical operations such as AND, OR, and NOT.
  • Reusability: Specifications can be reused across different parts of an application, promoting DRY (Don’t Repeat Yourself) principles.

Implementing the Specification Pattern in TypeScript

Let’s start by defining a simple Specification interface in TypeScript. This interface will have a method isSatisfiedBy that takes an object and returns a boolean indicating whether the object satisfies the specification.

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

Creating Basic Specifications

We can create concrete specifications by implementing the Specification interface. For example, let’s define specifications to check if a customer is an adult and if they have a valid email address.

 1// AgeSpecification.ts
 2import { Specification } from './Specification';
 3
 4export class AgeSpecification implements Specification<Customer> {
 5    private readonly minimumAge: number;
 6
 7    constructor(minimumAge: number) {
 8        this.minimumAge = minimumAge;
 9    }
10
11    isSatisfiedBy(candidate: Customer): boolean {
12        return candidate.age >= this.minimumAge;
13    }
14}
15
16// EmailSpecification.ts
17import { Specification } from './Specification';
18
19export class EmailSpecification implements Specification<Customer> {
20    isSatisfiedBy(candidate: Customer): boolean {
21        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
22        return emailRegex.test(candidate.email);
23    }
24}

Combining Specifications

The true power of the Specification Pattern lies in its ability to combine specifications. We can create composite specifications using logical operations.

 1// CompositeSpecification.ts
 2import { Specification } from './Specification';
 3
 4export class AndSpecification<T> implements Specification<T> {
 5    private readonly left: Specification<T>;
 6    private readonly right: Specification<T>;
 7
 8    constructor(left: Specification<T>, right: Specification<T>) {
 9        this.left = left;
10        this.right = right;
11    }
12
13    isSatisfiedBy(candidate: T): boolean {
14        return this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate);
15    }
16}
17
18export class OrSpecification<T> implements Specification<T> {
19    private readonly left: Specification<T>;
20    private readonly right: Specification<T>;
21
22    constructor(left: Specification<T>, right: Specification<T>) {
23        this.left = left;
24        this.right = right;
25    }
26
27    isSatisfiedBy(candidate: T): boolean {
28        return this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate);
29    }
30}
31
32export class NotSpecification<T> implements Specification<T> {
33    private readonly specification: Specification<T>;
34
35    constructor(specification: Specification<T>) {
36        this.specification = specification;
37    }
38
39    isSatisfiedBy(candidate: T): boolean {
40        return !this.specification.isSatisfiedBy(candidate);
41    }
42}

Example: Combining Business Rules

Let’s combine the AgeSpecification and EmailSpecification to create a rule that checks if a customer is an adult and has a valid email address.

 1// Customer.ts
 2interface Customer {
 3    age: number;
 4    email: string;
 5}
 6
 7// Main.ts
 8import { AgeSpecification } from './AgeSpecification';
 9import { EmailSpecification } from './EmailSpecification';
10import { AndSpecification } from './CompositeSpecification';
11
12const adultSpecification = new AgeSpecification(18);
13const validEmailSpecification = new EmailSpecification();
14
15const adultWithValidEmailSpecification = new AndSpecification<Customer>(
16    adultSpecification,
17    validEmailSpecification
18);
19
20const customer: Customer = { age: 20, email: 'example@example.com' };
21
22if (adultWithValidEmailSpecification.isSatisfiedBy(customer)) {
23    console.log('Customer is an adult with a valid email.');
24} else {
25    console.log('Customer does not meet the criteria.');
26}

Benefits of Using the Specification Pattern

Improved Readability

By encapsulating business rules in specifications, we can avoid complex conditional logic scattered throughout the codebase. This makes the code easier to read and understand.

Enhanced Maintainability

Specifications can be updated independently without affecting other parts of the application. This modularity simplifies maintenance and reduces the risk of introducing bugs when changing business rules.

Flexibility and Reusability

Specifications can be reused and combined in different ways to address various business requirements. This flexibility allows developers to adapt to changing requirements more easily.

Visualizing the Specification Pattern

To better understand how specifications can be combined, let’s visualize the process using a diagram.

    graph TD;
	    A["Age Specification"] --> C["And Specification"]
	    B["Email Specification"] --> C
	    C --> D["Customer"]

Diagram Description: This diagram illustrates how the AgeSpecification and EmailSpecification are combined using an AndSpecification to evaluate a Customer object.

Try It Yourself

To deepen your understanding, try modifying the code examples:

  • Add a new specification: Create a CountrySpecification that checks if a customer is from a specific country and combine it with existing specifications.
  • Experiment with OR logic: Use the OrSpecification to create a rule that checks if a customer is either an adult or has a valid email.
  • Implement a NOT logic: Use the NotSpecification to negate an existing specification.

References and Further Reading

Knowledge Check

  • How does the Specification Pattern improve code readability?
  • What are the benefits of using composite specifications?
  • How can you extend the Specification Pattern to include new business rules?

Embrace the Journey

Remember, mastering design patterns like the Specification Pattern is a journey. As you continue to explore and experiment, you’ll find new ways to apply these concepts to solve complex problems. Stay curious, keep learning, and enjoy the process!

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026