Low Level Design: Rule Engine

What Is a Rule Engine?

A configurable rule engine evaluates business logic defined as structured data rather than code. Rules are stored in a database, loaded at runtime, and can be updated and hot-reloaded without a deployment, enabling product and operations teams to change behavior independently of engineering.

Rule Table Schema

Rule (
  id          UUID PRIMARY KEY,
  name        VARCHAR(255),
  priority    INT,             -- lower number = higher priority
  conditions  JSONB,           -- condition groups
  actions     JSONB,           -- actions to execute on match
  enabled     BOOLEAN,
  version     INT,             -- incremented on each update
  created_at  TIMESTAMPTZ
)

Condition Schema

Each condition is a JSON object with three fields:

{
  "field":    "user.country",
  "operator": "eq",
  "value":    "US"
}

Supported operators:

  • Equality: eq, neq
  • Numeric: gt, gte, lt, lte
  • Set membership: in, not_in
  • String: contains, starts_with
  • Null check: is_null

Conditions are grouped: AND logic within a group, OR logic between groups.

Action Schema

{
  "type":   "send_notification",
  "params": { "channel": "slack", "template": "fraud_alert" }
}

Supported action types:

  • set_field — write a value into the rule context
  • add_tag — append a tag to the context entity
  • send_notification — trigger a notification via configured channel
  • call_webhook — POST context data to an external URL
  • block — halt processing and return a blocked response
  • route_to — direct the entity to a specified queue or workflow

Evaluation Algorithm

rules = load_rules_ordered_by_priority()

for rule in rules:
    if not rule.enabled:
        continue
    if evaluate_conditions(rule.conditions, context):
        execute_actions(rule.actions, context)
        if rule.stop_on_match:
            break   # first-match mode
        # else continue to next rule (chain mode)

Field Resolution

Fields use dot-notation paths into the rule context, which is an arbitrary key-value map:

context = {
  "user": { "country": "DE", "age": 34 },
  "order": { "amount": 250.00, "currency": "EUR" }
}

resolve("order.amount", context)  ->  250.00
resolve("user.country", context)  ->  "DE"

Hot Reload via Redis Pub/Sub

Rules are cached in application memory with a 30-second TTL. When a rule is created, updated, or deleted in the database, the admin service publishes an invalidation message to a Redis channel. All evaluator instances subscribe and flush their local cache immediately:

-- On rule update:
PUBLISH rule-invalidations "{"rule_id": "uuid"}"

-- Subscriber handler:
on message(channel, data):
    cache.delete(data.rule_id)
    -- next evaluation triggers fresh DB load

Rule Versioning and Audit

Every update to a rule increments its version integer. Previous versions are retained in a RuleVersion audit table:

RuleVersion (
  rule_id     UUID,
  version     INT,
  conditions  JSONB,
  actions     JSONB,
  changed_by  UUID,
  changed_at  TIMESTAMPTZ,
  PRIMARY KEY (rule_id, version)
)

Test Mode

Rules can be evaluated against a sample context without executing any actions. The evaluator returns which rules matched and which conditions passed or failed, enabling safe rule authoring before enabling in production.

POST /rules/evaluate-dry-run
{
  "context": { "user": { "country": "FR" }, "order": { "amount": 500 } }
}

Response:
{
  "matched_rules": ["uuid-1", "uuid-3"],
  "skipped_rules": ["uuid-2"],
  "actions_would_execute": [...]
}

Key Design Considerations

  • Priority integers should be spaced (e.g., 100, 200, 300) to allow inserting rules between existing ones without renumbering.
  • The evaluator is stateless; all state lives in the context map passed by the caller.
  • Webhook action calls are made asynchronously via a job queue to avoid blocking the evaluation response.
  • Rule changes require a two-step publish: save to DB first, then publish invalidation, so subscribers always read a consistent state.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the benefit of a configurable rule engine over hardcoded logic?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A configurable rule engine stores business logic as data in a database rather than code. This allows product and operations teams to create, modify, and disable rules without engineering deployments, and supports hot-reload so changes take effect within seconds of saving.”
}
},
{
“@type”: “Question”,
“name”: “How are rule conditions structured in a rule engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each condition specifies a field (using dot-notation like user.country), an operator (eq, gt, in, contains, is_null, etc.), and a value. Conditions are grouped with AND logic within a group and OR logic between groups, enabling complex boolean expressions without custom code.”
}
},
{
“@type”: “Question”,
“name”: “How does hot-reload work in a rule engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Rules are cached in application memory with a short TTL (typically 30 seconds). When a rule is updated in the database, the admin service publishes an invalidation message to a Redis pub/sub channel. All evaluator instances subscribe to this channel and immediately flush the affected rule from their local cache, so the next evaluation loads the fresh version.”
}
},
{
“@type”: “Question”,
“name”: “What is test mode in a rule engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Test mode (dry-run evaluation) runs the rule engine against a sample context without executing any actions. It returns which rules matched, which conditions passed or failed, and what actions would have been executed. This allows safe authoring and debugging of rules before enabling them in production.”
}
}
]
}

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Atlassian Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Shopify Interview Guide

Scroll to Top