Katkat

Build a Transaction Rule Engine with AND/OR Chains and Nested Parentheses

StripeSoftware Engineer · Technical Screen

Last updated

Multi-part coding exercise: build a rule engine that accepts or denies payment transactions by evaluating text conditions over their attributes. The parts add single comparisons, then AND/OR chains evaluated strictly left to right, then nested parentheses. It tests parser design, modular code, and directing an AI coding assistant.

Build a rule engine that decides whether each payment transaction is accepted or denied.

  • Transactions: a list of records, each a set of named attributes, for example {"id": "t1", "amount": 1500, "currency": "USD", "country": "US", "card_type": "prepaid"}. These attribute names are illustrative.
  • Rules: a list of rules, each pairing a condition over a transaction's attributes with an action, ACCEPT or DENY.
  • Output: one decision per transaction, in input order.

Assume a condition arrives as text, such as amount > 1000. A comparison is a field name, an operator (==, !=, >, >=, <, <=) and a literal value, which is a number or a double-quoted string.

The task grows in parts, each extending what a condition can express. Three parts are given below. The round continues with further parts that add more kinds of conditions, and finishing all of them is not expected, so structure the code so that each new part is a small, local change.

An AI coding assistant is available during the round. Use it as a tool you direct: you decide the modules and their interfaces, hand it well-scoped pieces, and check what it produces, rather than letting it produce one monolithic solution.

Clarifying Questions

  • When several rules match one transaction, which one decides: the first matching rule in list order, any matching DENY, or something else? What is the decision when no rule matches?
  • Do rules arrive as text that must be parsed, or as already-structured objects?
  • What should a comparison return when the transaction lacks the field it tests, or when the types do not match, as in country > 5?
  • Should a malformed rule be rejected when the rules are loaded, or skipped when transactions are evaluated?
  • Are amounts integers in the smallest currency unit, or decimals?

Part 1 — Single-condition rules

Each rule's condition is a single comparison. Parse the rules and return a decision for every transaction. For example, the condition amount > 1000 matches a transaction whose amount is 1500 but not one whose amount is exactly 1000.

What This Part Should Cover

  • A condition representation that is parsed once and evaluated many times
  • Correct comparison semantics for numbers and strings, including the boundary of strict operators
  • A decision step kept separate from condition evaluation
  • Tests for matching, non-matching and malformed rules

Part 2 — AND and OR without precedence

A condition may now chain comparisons with AND and OR. The two operators have no precedence over each other: evaluate the chain strictly from left to right. So A OR B AND C means (A OR B) AND C, not the usual A OR (B AND C).

For the transaction {"country": "US", "amount": 50, "card_type": "credit"}, the condition country == "US" OR amount > 1000 AND card_type == "prepaid" is false, because (true OR false) AND false is false.

Clarifying Questions for this Part

  • Are AND and OR always uppercase?
  • May a string literal contain the word AND or OR, or spaces?

What This Part Should Cover

  • Tokenizing that does not break on spacing or on keywords inside string literals
  • Left-to-right combination that matches the stated semantics, verified by a test where the usual precedence would give a different answer
  • An extension of Part 1's structures rather than a rewrite

Part 3 — Nested conditions with parentheses

Conditions may now group sub-conditions with parentheses, nested to any depth, for example (amount > 1000 AND country != "US") OR (card_type == "prepaid" AND amount > 200). A parenthesized group is evaluated as a unit before it is combined with its neighbors. For the transaction {"amount": 300, "country": "US", "card_type": "prepaid"}, that condition is true, because the second group is true.

Clarifying Questions for this Part

  • Outside parentheses, does left-to-right evaluation still hold, or should AND now bind more tightly than OR?
  • Is there a maximum nesting depth or rule length?

What This Part Should Cover

  • Parsing of arbitrary nesting, with clear errors for unbalanced or misplaced parentheses
  • A tree representation whose evaluation follows the grouping exactly
  • Tests for nested groups and for malformed input
  • A change confined to parsing, with evaluation and the decision step untouched

Hints

Hint 1

Separate the jobs: Even with one comparison there are distinct jobs: reading the rule text, representing a condition, evaluating it against a transaction, and turning matches into a decision. Decide which of these will have to change in later parts.

Hint 2

Fold as you go: Think about how an already-evaluated left side combines with the next comparison, and whether splitting the text on spaces or keywords will survive the next part.

Hint 3

Recursion in the input: A parenthesized group contains a whole condition, which may itself contain groups. Consider which structure in your code can mirror that.

Follow-up questions

  • How would you add `NOT` and an `IN` operator, as in `country IN ("US", "CA")`, and which modules would change?
  • How would you report which rule decided each transaction, and why it matched?
  • A new rule needs state across transactions, such as the number of transactions on the same card in the last hour. What changes in your design?
  • With thousands of rules and a high transaction rate, where does the time go, and how would you reduce it?