Skip to content

Engine design

How to design a rules engine, and when not to build one

Somebody asks for a threshold to be configurable. Then a second condition. Then an exception for one group. Six months later you have built a programming language with no debugger, no tests and no error messages.

The short answer

Build one when the policy changes faster than you can ship, when the person who owns the policy is not an engineer, and when the outcomes are enumerable. Keep the conditions a fixed vocabulary rather than free expressions, evaluate at the moment of use, and give every rule a floor it cannot cross.

The request that starts it

It never arrives as “build a rules engine”. It arrives as “can this number be a setting”. That is reasonable, so it gets done.

Then the number needs to differ per group. Then there is an exception for one case. Then somebody wants two conditions combined. Each step is small and defensible, and at no point does anyone decide to build an interpreter, which is why everybody ends up with one.

The question worth asking early is not how to build it. It is whether to.

When it is worth it

Three conditions, and you want all three.

The policy changes faster than you ship. If a threshold moves twice a year, a constant in code is correct and a rules engine is an elaborate way of doing the same thing more slowly.

The person who owns the policy is not an engineer. This is the real driver. If a director wants to change how something behaves and the only path runs through a developer and a release, then the system is slower than the business it serves, and that gap is what the engine closes.

The outcomes are enumerable. Rules must select from a fixed set of consequences. If a rule can trigger arbitrary behaviour, you have not built a rules engine, you have built a scripting host with your production database attached.

If any of those is missing, write the conditional. It will be shorter, testable, and readable by the next person.

A vocabulary, not an expression language

The strongest constraint in the whole design: do not let users write expressions.

The moment rules become free form, you owe them a syntax, a parser, error messages a non programmer can act on, versioning, a sandbox with resource limits, and a debugger. That is a language project, and it will be a worse language than the ones that exist.

Instead offer a closed vocabulary. Each condition is a named fact with a typed comparison and a value. Facts are supplied by the system, not by the user.

fact           operator        value
days_overdue   greater_than    30
category       is_one_of       [A, B]
amount         between         1000, 5000

Combine with all or any, one level of grouping, and stop there. In practice this covers almost everything people actually ask for, and the cost is a fraction. When a request genuinely does not fit the vocabulary, that is a signal to add a fact, which is a small, reviewable, testable change.

Evaluate at the moment of use

The most damaging bug in this category is a rule evaluated once and cached.

Somebody meets a condition on Monday, is added to a list, and on Wednesday the list is acted upon, by which time the condition is no longer true. They are treated according to a state that no longer exists, and they know it, and they are right to be angry.

Evaluate at the point of consequence, always. If that is too expensive, the correct fix is to make evaluation cheaper, not to make it stale. Correctness is not a performance trade you get to make quietly.

Floors, which the rules cannot cross

Every configurable system needs limits that configuration cannot reach. Not preferences, not defaults, hard floors, written in code below the configurable layer.

If a system can restrict access for non payment, it must be structurally incapable of restricting somebody who owes nothing. Not “no rule currently does that”. Incapable.

The reason is that configuration is edited by humans in a hurry, and the blast radius of a mistake in a rules engine is everybody at once. A floor turns a catastrophe into an error message.

Write the floors first. They are the specification for what the engine is not allowed to do, and having them makes the rest of the design safe to be flexible about.

Dry run before enable

A rule set must be runnable in preview: here is exactly who this would affect right now, and how many of them. Then a human decides.

Without a dry run, the first real test of any rule is production, and the feedback arrives as complaints from the people it hit. The dry run is the single highest value feature in a rules engine, and it is usually the last thing anybody builds. Build it second, after the floors.

Explain every outcome

Every decision the engine makes must be able to say why. Which rule matched, which facts were true at the time, and what the alternative would have been.

This is not a nicety. Somebody will eventually ask why they were treated a particular way, and the answer has to be better than “the system decided”. A rules engine that cannot explain itself is a liability wearing the costume of a feature.

Questions people actually ask

When is a rules engine the wrong answer?
When there are three cases and they change once a year. A conditional in code is simpler, testable, and readable. The engine earns its cost when policy changes faster than releases do and the owner of the policy cannot ship one.
Why not let users write expressions?
Because an expression language is a programming language, and it will need a debugger, error messages, versioning and a sandbox. A fixed vocabulary of conditions covers almost every real requirement and none of that cost.
What is a floor condition?
A rule the rules cannot break. For example, a system that can restrict access for non payment must be structurally incapable of restricting somebody who owes nothing. Floors live in code, below the configurable layer.
How do you test configurable policy?
With a dry run that shows exactly who a rule set would affect, before it is enabled. Anything less means the first test is production.

Published May 2026, updated July 2026.

Hiring someone to own work like this?

I am open to senior backend and full stack roles, remote and permanent.

Get in touch / Read the CV