SD System Design In Depth
0%
lesson system-designworkflowsevents

Rule engine and trigger framework

Module 4.2: Asynchronous Workflows & Partitioned Logs Updated Sep 17, 2026
📄 Cheat Sheet
Rete Algorithm: Optimizing the Match
NPTEL-NOC IITM Rete Algorithm: Optimizing the Match Watch on YouTube ↗

An IIT Madras (NPTEL) lecture that explains Rete's alpha and beta networks and how caching partial matches avoids re-evaluating every rule on every fact, the core algorithm of this lesson.

0:00 / --:--

Rule engine and trigger framework

An order arrives, and the product wants some orders reviewed before fulfillment. The condition can change while old events remain available for replay. A rule engine must preserve why an action was selected, or the same recorded order can quietly acquire a different history each time a worker sees it.

The same pattern can issue a rider incentive after delivery, a coupon after cart abandonment or a workflow after document approval. We'll define the event-to-action path, resolve overlapping rules, then replay an order under a changed policy.

A decision before an effect

The workflow lesson started with an agreed set of steps. Here the first decision is whether that workflow should exist at all. An incoming event supplies facts; a versioned rule tests those facts and chooses an action obligation, such as creating a review task.

Keep evaluating the rule separate from carrying out the action. Evaluation should produce the same decision from the same recorded input and rule version. That is a pure evaluation: it does not charge a card, send a message or ask a changing external database what the order looks like now. Effect workers use the identity and recovery agreements already established in Module 08.

If the rule needs additional facts, record those facts as inputs with their source versions or timestamps before evaluating. Otherwise a historical order can match differently because its customer's profile changed, even though the rule stayed fixed. Sometimes “use the current profile” is the intended product policy, but that evaluation must record which profile it actually used.

Normalize the incoming event into a validated, typed input before matching. The rule store supplies an immutable policy version; matching produces a decision and an action obligation. In this proposed architecture, a dispatcher carries out the obligation later and records the outcome.

flowchart TD accTitle: Match first, dispatch from a retained decision accDescr: An event is validated and normalized before matching against a versioned rule store. The matcher commits a decision and action obligation together. A dispatcher executes the allowed action and records its outcome. E["Event"] --> N["Validate + normalize"] N --> M["Rule matcher"] R["Versioned rule store"] --> M M -->|One transaction| D["Decision + action obligation"] D --> W["Dispatcher"] W --> A["Allowed action"] A --> L["Execution outcome"]

The decision explains why work was requested; the execution outcome explains what happened. Preserve both. An audit log written after an external effect cannot repair a lost decision by itself.

The matching policy

Our chosen event is order-A, with type order.submitted, schema version one and a quantity of 150 units. The units describe a synthetic order size, not a measured production threshold. Rule review-large matches at or above 100 units in rule set version one; review-all matches at or above zero.

Both rules can therefore match the same event. We choose a policy where the lower priority number wins, with rule identifier as a deterministic tie-breaker. They select the same action category, review, so matching both must still create only one review obligation. Another product could permit several independent actions, but it would need an identity for each action and a defined conflict policy.

Read the table as the chosen rule data. The two version columns make the change visible without changing the recorded order.

RulePriorityVersion one minimumVersion two minimum
review-large10100 units200 units
review-all200 units0 units
Changing the threshold changes the selected rule; the retained order stays the same.

Rule-set version and event-schema version answer different questions. The schema version says how to interpret the event fields, as the event contract lesson established. The rule-set version says which policy evaluates those fields. Store both with the evaluation, together with the matched rules and selected action, so an explanation does not depend on today's configuration.

A versioned replay

The workflow replay stores evaluations and action obligations in a private temporary database; it creates no real review tasks or customer messages.

The evaluation key is event identity plus rule-set version, and the saved canonical input must agree on repeat delivery. dispatch records the first action obligation. audit records a new evaluation for comparison without issuing another action. A change of rule set on an ordinary repeat delivery is refused until someone explicitly requests that audit replay.

python3 m15-workflows.py rules
v1: matches=review-large,review-all winner=review-large mode=dispatch
same input: duplicate
changed input: payload-conflict
v2 ordinary delivery: requires-explicit-replay
v2 explicit replay: matches=review-all winner=review-all mode=audit
action feedback: ignored-type-or-schema
evaluations=2 action obligations=1

The two evaluations explain different policy decisions about the same order. They leave one business action because the original action identity, order-A/review, survives the rule edit. The changed-input probe submits 151 units under the original event identity and receives a conflict. That prevents an event producer from rewriting the explanation of an already accepted decision.

The rule representation

Ordinary code is the simplest representation when developers own a small policy and deploy it with the application. Reviewers can use the existing code tools, and conditions can use the language's type checks. Its cost appears when a product operator needs to adjust a threshold independently or compare many historical policy versions.

A restricted data representation makes those versions explicit. For example, this proposed JSON format describes the first review rule:

{
  "rule_id": "review-large",
  "event_type": "order.submitted",
  "version": 1,
  "priority": 10,
  "condition": {
    "field": "units",
    "operator": "gte",
    "value": 100
  },
  "action": { "type": "create_review" }
}

This is our schema, not JsonLogic or a format parsed by the replay. A larger schema could compose allowed predicates with and and or. Define missing-field and type behavior explicitly: a numeric quantity and a string containing digits should not acquire accidental equivalence through an interpreter's coercion rules.

Start with allowed fields, typed operators and named actions, and reject unknown combinations before publication. The fixture is narrower still: trusted tuples supply integer thresholds and fixed action names. It never evaluates arbitrary expression text, and it exposes no user scripting interface.

User-authored scripts can express more complex conditions but add execution limits, isolation and authorization requirements. Choose that capability only when a restricted language demonstrably cannot express the needed decisions. Moving an if statement into a string creates those obligations without making the policy more understandable.

Index candidate rules by event type, tenant, region, product or active time window when those fields exclude unrelated policies. Keep priority as a conflict-resolution rule, not permission to discard a lower-priority match needed for audit. The selector must preserve the result of evaluating the applicable rule set. Measure candidate counts and compare outcomes before introducing a cache of active rules. A worker that uses a stale rule version must reveal that version in its receipt.

Publication and feedback

Treat a new rule set as immutable data that can be reviewed and exercised on saved examples before activation. Pin the selected version when accepting evaluation work so a later deployment does not change a queued event's policy silently. Compare its matches in audit mode first, then publish a version that future events can select. Returning to an older version changes future decisions; it does not revoke actions already accepted under the intervening version.

The transactional outbox supplies the next boundary. Commit the evaluation and its action obligation together, then let a worker deliver it. The local replay retains that obligation but does not run a dispatcher. Calling the action while evaluating and recording the decision afterward leaves a crash window where the effect exists without its explanation.

An action may emit another event, creating a possible feedback loop. The fixture accepts only the originating order.submitted type and ignores a review.created event. Broader systems should define which event types can trigger which action types, retain causal identifiers, and bound repeated transitions. Deduplicating the same event does not stop a loop that generates a fresh event identity every time.

For this review policy, use immutable versions, one winning review action and audit-only historical re-evaluation. Change to action-producing replay only when the product explicitly owes new work and supplies new identities for it. The next lesson, fanout patterns, follows actions owed to several recipients. Later, inventory reservations put a harder limit on an action: matching a rule can request a unit, but only the stock owner can grant it. The booking and payment designs will carry that distinction across separate services.


Under the Hood: High-Throughput Rule Evaluation

Modern platforms process millions of real-time events per second against dynamic rule collections: fraud prevention ("Flag transaction if amount > 10,000 and country != user.home_country"), promotional pricing, and security firewalls. Naive evaluation (O(N × M) linear checking) creates crippling CPU bottlenecks.

The Rete Pattern Matching Network

flowchart TD Event["Transaction Event: {amount: 15000, country: 'RO', type: 'WIRE'}"] --> RootNode["Root Alpha Node"] subgraph AlphaNetwork ["Alpha Network (Single-Attribute Filters)"] RootNode --> A1{"amount > 10000?"} RootNode --> A2{"type == 'WIRE'?"} end subgraph BetaNetwork ["Beta Network (Multi-Object Joins)"] A1 & A2 --> B1{"country != user.registered_country?"} end B1 --> Terminal["Terminal Node: Trigger Alert & Block Transaction"]

Under the Hood: The Rete Algorithm & JIT Compilation

  • The Rete Algorithm (Forgy): Avoids redundant evaluations by compiling rules into a directed acyclic evaluation graph. Alpha nodes filter single-fact conditions; Beta nodes perform stateful cross-entity comparisons. Partial matches are kept in beta memories, so when a fact is added or changed only the affected parts of the network are re-evaluated (incremental matching) instead of re-running every rule.
  • Abstract Syntax Tree (AST) JIT Compilation: High-performance engines parse business rules written in domain-specific languages (e.g. JSON-rules or CEL - Common Expression Language) into an AST that is parsed and type-checked once, then evaluated in microseconds. The reference CEL implementations interpret the checked AST; some other engines go further and compile rules to JVM bytecode or native functions.

Worked example: why indexing matters

A fraud platform has 5,000 rules and receives 20,000 transactions per second. The naive approach evaluates every rule on every event: 100 million rule evaluations per second, about 50 ms per event at 1 microsecond each. That is unacceptable.

Most rules start with a selective predicate such as country == "RO" or type == "WIRE". Index rules by their first discriminating attribute, a hash map from (field, value) to candidate rules. A typical event then matches about 40 candidate rules, which are evaluated fully: 20,000 x 40 = 800,000 evaluations per second, about 40 microseconds per event. Rete generalises this by sharing identical condition nodes across rules and caching partial joins, which matters when rules join several facts, such as a transaction plus the user profile plus the last 5 transactions.

Trigger framework architecture

  • Ingest: events arrive from Kafka, keyed by user or account, so per-entity state stays on one partition.
  • Enrich: join with profile and feature data from a low-latency store, and cache hot profiles.
  • Evaluate: compiled CEL or JSON-logic expressions, or a Rete engine such as Drools for complex joins.
  • Act: emit decisions (block, review, notify) as new events. Actions are idempotent, keyed by (rule version, event ID).
  • Audit: store the event, rule version, inputs and outcome for every decision.

Engine options

ApproachExpressivenessSafetyLatencyExamples
Hard-coded if statementsAnythingCode reviewFastestEarly-stage products
Expression languageBoolean expressions, no loopsHigh: bounded costMicrosecondsCEL (Kubernetes, Envoy, Firebase rules), JSON-logic
Rete production-rule engineMulti-fact joins, forward chainingMediumMicroseconds to millisecondsDrools, CLIPS
Stream processor with windowsTime windows, aggregates such as 5 wires in 10 minutesCode-levelMillisecondsFlink CEP, Kafka Streams
Decision tables and DMNTabular business logicHighFastCamunda DMN

Failure modes

  • Rule explosion: thousands of overlapping rules that nobody understands. Track the hit rate per rule and retire dead rules.
  • Shadow mode skipped: deploy new rules in log-only mode first to measure false positives before they block real customers.
  • Enrichment outage: decide whether rules fail open (allow) or fail closed (block) when profile data is unavailable.
Check yourself Question 1 of 3
Beta memories recall
From: Under the Hood: The Rete Algorithm & JIT Compilation

In the Rete algorithm, what do beta memories contribute?

Key Takeaways

Rule engines evaluate millions of incoming events against dynamic criteria without modifying application code.
The Rete algorithm shares common condition evaluations across rules, eliminating redundant CPU computation.
CEL defines a bounded, non-Turing-complete expression language; implementations may interpret, compile, or otherwise optimize checked expressions.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue