Moving Business Rules Out of Application Code

A practical introduction to treating changing business decisions as data without turning your application into a configuration maze.

Business rules moving from a dense application core into separate modules

A discount rule often begins as one readable if statement. Then the exceptions arrive: customer tiers, regional limits, campaign windows, minimum totals, and one-off exclusions. The problem is not conditional logic itself. The problem is that a frequently changing business decision has become tangled with application flow.

A rule engine gives that decision a boundary.

Start with the change rate

Do not move every condition into configuration. Code is still the clearest home for stable invariants and behavior tied closely to an algorithm.

Rules become useful when a decision:

  • changes more often than the surrounding application;
  • needs an audit trail or explicit version;
  • appears in several entry points;
  • must be tested against many combinations of input;
  • may eventually be managed outside a deployment.

Here is a small eligibility rule with rule-engine-js:

import { createRuleEngine, createRuleHelpers } from "rule-engine-js";
type CheckoutContext = {
customer: { tier: "standard" | "gold"; active: boolean };
order: { total: number };
};
const engine = createRuleEngine();
const rules = createRuleHelpers<CheckoutContext>();
const canUseGoldDiscount = rules.and(
rules.eq("customer.tier", "gold"),
rules.eq("customer.active", true),
rules.gte("order.total", 100),
);
const result = engine.evaluateExpr(canUseGoldDiscount, {
customer: { tier: "gold", active: true },
order: { total: 140 },
});
console.log(result.success);

The application still owns what happens next. The rule only answers a question.

Keep actions outside the rule

A useful separation is:

  1. collect a trusted context;
  2. evaluate a rule;
  3. perform the action in application code.

That means a pricing rule can decide whether an order qualifies, but it should not charge a card or write directly to a database. Side effects stay in code where authentication, idempotency, retries, and observability are easier to enforce.

JSON is a transport format, not a governance model

A serializable rule can live in a file, database, or API response:

{
"and": [
{ "eq": ["customer.tier", "gold"] },
{ "eq": ["customer.active", true] },
{ "gte": ["order.total", 100] }
]
}

That flexibility is useful, but it also creates responsibility. Validate rule shape before activation. Limit rule depth and operator count. Version changes, record who approved them, and keep rollback simple. A bad configuration can break production just as effectively as bad code.

The real payoff

The main benefit is not fewer lines. It is a clearer ownership boundary: the engine handles evaluation, the rule describes a decision, and the application handles effects. Each part can change and be tested without hiding the others.

As of v1.0.7, rule-engine-js includes typed path helpers, nested path resolution, validation helpers, state-change operators, and a stateful wrapper. Start with the plain engine. Add the advanced pieces only when the problem calls for them.

Sources: rule-engine-js repository, npm package.

Read more

All posts
  1. Glowing geometric rules transforming into structured JSON objects
  2. Geometric rule structure surrounded by JSON objects and code
  3. Tangled conditional paths becoming clean independently tested rules
  4. A code editor connected to modular web development tools