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 contextadd_tag— append a tag to the context entitysend_notification— trigger a notification via configured channelcall_webhook— POST context data to an external URLblock— halt processing and return a blocked responseroute_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.
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