DEV Community

Sacha Clerc-Renaud
Sacha Clerc-Renaud

Posted on

Meaningful implementation for business rules

Recently at Spendesk we try to adopt a Domain Driven Design (DDD) approach. To better understand what it is I decided to read the reference book about it.

If you are already used to it or being evaluating using DDD, I recommend you to read it. There are very interesting principles in it. I will present one of them that I think can be very useful in some situations, the specification pattern.

Specification allow you to know if an object satisfies some conditions. This can be very useful to explicit in your code some business rules.

let's take a simple example : You work on an application for a car retailers and during a meeting with sales people you learned that in that company there is a discount on Ford and blue cars from Tesla.

The basic pattern:

The specification pattern looks like this:

const car = {
  color: 'blue',
  model: 'X',
  brand: 'Tesla',
};

isDiscountEligibleSpecification.isSatisfiedBy(car); // => true
Enter fullscreen mode Exit fullscreen mode

For the purpose of this article I wrote a small implementation of the specification pattern available here on NPM

Let's see how to use it :

import { createSpec } from 'spoeck';

const car = {
  color: 'blue',
  model: 'X',
  brand: 'Tesla',
};

const isDiscountEligible = createSpec({
  isSatisfiedBy: (car): boolean => {
    return (
      (car.color === 'blue' && car.brand === 'Tesla') || car.brand === 'Ford'
    );
  },
});

isDiscountEligible.isSatisfiedBy(car); // => true
Enter fullscreen mode Exit fullscreen mode

Here the example is very basic and our code is not way better than creating a function dedicated to this check.

function checkDiscountAvailable(car) {
  return (
    (car.color === 'blue' && car.brand === 'Tesla') || car.brand === 'Ford'
  );
}
Enter fullscreen mode Exit fullscreen mode

But this kind of functions tends to lives in a helper directory with a lot of other stuff and as it is not clearly identified some developers will not know it exist and as the rule is pretty simple, the code risk to be duplicated.

Specification allow to encapsulate business logic into identified objects that can be reused and composed (We will see that soon).

Use cases:

Validation:

The first use case that came to my mind when I discover this pattern was the validation one. It's easy to write code that might looks like this :

function applyDiscount(car) {
  if (!isDiscountEligible.isSatisfiedBy(car)) {
    throw new Error('Car is not eligible to discount');
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Selection:

The second one is the selection use case. When you want to filter over a collection:

const discounts = cars.filter((car) => isDiscountEligible.isSatisfiedBy(car));
Enter fullscreen mode Exit fullscreen mode

This is working but in real life we often store our ressources in a database and use SQL requests to filter them. Filter all the cars from the database in memory is not a good idea as the number of rows could be huge. We will see later in this article how we can deal with specifications and databases.

Also what happen if you want to filter or validate 10 or even 1000 more rules?

Here come the composite pattern.

Composite pattern:

The real strength of specifications is when they are combined with the composite pattern.

A composite pattern is when an object is composed of objects that has the same type. In our case, our specifications will be composed by other specifications. This way we can combine them as building blocks to implement complexe business rules and avoid code duplication.

To combine them, specification object expose methods and or xor etc... and they are used like this : spec1.and(spec2) => composed_specification_1_AND_2

Let's use this with our car discount example :

const car = {
  color: 'blue',
  model: 'X',
  brand: 'Tesla',
};

const isBlue = createSpec(...);
const isFromTesla = createSpec(...);
const isFromFord = createSpec(...);

const isDiscountEligible = isFromFord.or(isFromTesla.and(isBlue));

isDiscountEligible.isSatisfiedBy(car); // => true
Enter fullscreen mode Exit fullscreen mode

I dont know you, but it looks very elegant and readable to me. Being able to compose differents rules that your application can have in an extremely explicit manner is very valuable I think.

But...

This works well in memory. As said earlier filtering a huge collection from a database in memory is evil and SQL is here for that. Let's see how we can handle that.

Specifications and database storage:

There is multiple solutions to use databases with specifications that all have their pros and cons.

Expose request in specifications object:

You can add a method to your specification object to return the SQL query that fits your needs. And then pass it to your database access layer.

const isDiscountEligible = createSpec({
  isSatisfiedBy: (car): boolean => {
    return (
      (car.color === 'blue' && car.brand === 'Tesla') || car.brand === 'Ford'
    );
  },
  asSQL: () => {
    return `
      SELECT *
      FROM cars
      WHERE (color= 'blue' AND brand = 'Tesla')
      OR brand= 'Ford'`;
  },
});
Enter fullscreen mode Exit fullscreen mode

This works but it tight couple our specification to our SQL storage which is not satisfying as SQL should be contained in our repository.

Use specialized repository methods:

If you're familiar with DDD you should know the repository pattern. If it's not the case basically a repository is an object that will abstract the storage technology for the client code. This way you never manipulate SQL or other query language outside the repository you just call methods that do it for you like getById() etc...

The second solution is to implement a method in our specification object that will call a specialized repository method that fit our needs.

const isDiscountEligible = createSpec({
  isSatisfiedBy: (car): boolean => {
    return (
      (car.color === 'blue' && car.brand === 'Tesla') || car.brand === 'Ford'
    );
  },
  satisfyingElementsFrom: async (carRepository) => {
    return await carRepository.getDiscountEligibleCars();
  },
});
Enter fullscreen mode Exit fullscreen mode

This work fine, but it requires that you implement a specific method on the repository that will maybe be used only by your specification object.

Use generic repository methods:

This solution is similar to the previous one but we will use a more generic methods from the repository and finalize the selection in memory.

const isDiscountEligible = createSpec({
  isSatisfiedBy: (car): boolean => {
    return (
      (car.color === 'blue' && car.brand === 'Tesla') || car.brand === 'Ford'
    );
  },
  satisfyingElementsFrom: async (carRepository) => {
    const fordAndTeslaCars = await carRepository.getCarsByBrand([
      'Tesla',
      'Ford',
    ]);
    return fordAndTeslaCars.reduce((results, current) => {
      if (current.brand === 'Tesla' && current.color !== 'blue') {
        return results;
      }
      results.push(current);
    }, []);
  },
});
Enter fullscreen mode Exit fullscreen mode

This present the advantage to use a generic repository function that can be used in multiple context. But there is a performance trade off as you have to filter the subset of rows in memory.

All these solutions are valid. But some are more adapted in certain situations. It's up to you to choose the right one for your needs.

Conclusion:

Pros:

  • Encapsulation: Keep business logic in a meaningful object (Avoid the helper directory).
  • Building blocks: Compose complex rules from other simpler rules.
  • Reusability: Composition of rules avoid code duplication.
  • Readability: Make the code more readable and expressive.
  • Adaptability: When your apllication requirements are changing. You can create or adapt new rules from previous one easily.

Cons:

  • Can be hard/impossible to implements combination when used against a DB.
  • Can be overengineered for small projects. The helper method may be enough.

Specification pattern is a powefull tools to modelize business rules and enforce them explicitly in the code in a reusable way. However if you have to deal with a database it is very hard to implement methods like and, or that can compose their database queries.

For me the real use case is for validating business rules and filtering on limited in memory collections.
In any case it's up to you to find the implementation that fits your needs.

Let me know in the comment what you think about this pattern. If you already use it how do you do to compose specifications with database queries ?

Top comments (0)