Build a Transaction Rule Engine with AND/OR Chains and Nested Parentheses
StripeSoftware Engineer · Technical ScreenLast 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,
ACCEPTorDENY. - 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
ANDandORalways uppercase? - May a string literal contain the word
ANDorOR, 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
ANDnow bind more tightly thanOR? - 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?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.