Dhaka, Bangladesh
Case Studies

Turning a Growing Rule Vocabulary Into Data Instead of Code

Cover image for the case study "Turning a Growing Rule Vocabulary Into Data Instead of Code"
A component shrinking as inline if/else branches move out into a single data table the UI reads from instead.
The recommendation builder let merchants filter recommended products by a growing list of rule types - category, tag, price, stock status, browsing behavior, purchase history - each with its own valid comparison operators and, for the behavioral ones, its own set of timeframe options. I moved that vocabulary out of the component that rendered each rule row and into a single data module the UI reads from, so adding a new rule type became a data change instead of a new branch of component logic.
One data module introduced, operator sets and timeframe lists moved out of component logic, filter row and placement UI rebuilt against it.
TL;DR summary
The problemA growing filter vocabulary was being handled with inline logic inside the component that rendered each rule
What I didExtracted operator sets and timeframe options into one data module; rebuilt the rule-row UI to read from it
OutcomeAdding a new rule type became a data change rather than new branching logic in the component
Trade-offAn extra layer of indirection between what a rule type is and how it renders
TimelinePart of the same build as the state-management work
TeamShared codebase; the rule engine itself was owned separately
A dozen-plus rule types listed against their valid operators and value shapes, several with their own distinct timeframe sets.
The plugin's rule vocabulary wasn't small. A merchant could filter recommended products by attribute, category, tag, price, relative price, stock status, and by behavioral signals - recently viewed, best-selling, high-revenue, frequently viewed, added to cart, previously purchased - and several of those behavioral triggers each carried their own distinct set of valid timeframes rather than sharing one. Some rule types accepted a free value; others only made sense as a fixed dropdown; a few had no value at all. The component responsible for rendering one rule row had to know all of this - which operators were valid for a given rule type, what kind of input to render for the value, and what to reset when the merchant changed their selection partway through.
A single component shrinking as a newly introduced data file grows to hold the same information, logic moving from one to the other.
I pulled the operator sets and the per-trigger timeframe lists out of that component and into a dedicated data module - one export per concern (valid operators, and a handful of named timeframe lists for the rule types that needed their own). The rule-row component and the surrounding filter and placement UI were then rebuilt to read from that module instead of encoding the vocabulary inline. The practical difference: adding a new rule type, or changing which operators a rule type supports, became a change to one data file rather than a new conditional branch inside the component that renders every rule row.
A growing data table compared against a growing set of if/else branches - the table stays flat as rows are added; the branches compound.
I made this call because the vocabulary wasn't just growing in count - it was growing in shape. New rule types didn't all behave the same way: some needed a taxonomy picker, some a free-text range, some nothing at all. Encoding that as component logic meant every addition risked touching a component already handling a dozen other cases. Encoding it as data meant a new rule type was a new row, not a new code path, and the component's job stayed the same regardless of how many rule types existed.
A single open question: how much faster did adding a rule type actually become - not measured, only observed to have shipped together with the extraction.
I don't have a clean before/after measurement for how much faster adding a rule type became - there's no ticket history showing time spent on this specific change versus others. What's directly visible is that the extraction and the rule-row rebuild happened together, in service of the filter and placement UI that shipped alongside it, which is consistent with the vocabulary having outgrown the inline-logic approach rather than the change being made speculatively ahead of need.
Two questions this section answers: why move to data instead of more conditions, and why not build a fully generic system upfront.
Why move the rule vocabulary into data instead of just adding more conditions to the component? Because the vocabulary wasn't just growing in count, it was growing in shape - different rule types needed different input controls and different valid operators. A data table scales by adding rows; a component handling that many distinct cases inline gets harder to change safely with every addition. Why not build this as a fully generic rule-definition system from the start? The vocabulary was still settling while the builder was being built - locking in a fully generic schema early would have meant guessing at shapes I hadn't seen yet. Extracting to data once the pattern was clear was the more grounded move than designing for cases that hadn't shown up.
Part of the Product Recommendations admin builder case study series →
On this page
Quick answer
TL;DR
The situation
What changed
The decision
What I'd confirm before calling this settled
FAQ