I’ve Rebuilt the Same Rule Engine in Four Languages. It Broke the Same Way Every Time
Three years ago, a discount rule that had been quietly running in production for almost two years started giving every customer free…
I’ve Rebuilt the Same Rule Engine in Four Languages. It Broke the Same Way Every Time
Photo by Mohammad Rahmani on Unsplash
Three years ago, a discount rule that had been quietly running in production for almost two years started giving every customer free shipping. Nobody touched the discount logic. Someone updated an unrelated currency formatting function, and that was apparently enough.
It took us six hours to find it, because the rule wasn’t sitting anywhere obvious. It was buried four files deep, inside a function that had grown over time, written by three engineers who didn’t work there anymore. That’s usually how it goes.
I’ve seen this exact shape of failure more times than I’d like to admit, across companies, across stacks. These days I spend most of my time around exactly this problem — I work on rule-engine tooling at Nected — which is probably why I’ve stopped blaming the language for any of this and started looking at the decision sitting underneath it instead.
A rule engine, at its core, is just a system that lets you define “if this, then that” logic somewhere outside your main application code, so business rules can change without a deploy. Pricing tiers, fraud checks, loan approvals, eligibility rules — anything that changes more often than your codebase should, technically, live in one. In practice, it rarely does. Most of it ends up scattered across if-else blocks nobody really wants to touch.
Read more about the top 10 rule engines: nected.ai/blog/top-10-business-rules-engine
The Java version
My first real exposure to this was a Java-based insurance platform. Eligibility rules — age limits, region restrictions, exclusions — lived inside a service class that had ballooned past 2,000 lines. Every new exclusion meant a new conditional. Every new conditional meant a new deployment, a new round of QA, a new chance to break something three teams away from the one actually making the change.
We eventually moved some of it into a Drools-based setup, the kind a lot of Java teams reach for once they hit this wall. It helped, for a while. Decisions got faster to change. But the engine itself slowly became its own maintenance burden — XML configs nobody fully understood anymore, rule files only the original author could safely edit. We’d traded one bottleneck for a smaller, quieter one.
Then Python, then Node
A few years later, different company, same shape of problem, different stack. This time it was a fraud-scoring system in Python, running on a pile of nested conditionals that had started simple and stopped being simple about a year in. We talked about packaging it into something closer to an actual rules library, but never finished the move. For a while the nested-if version worked fine, mostly because the team was small and everyone knew where the bodies were buried.
Then two people left within a month of each other, and whoever inherited it had no idea why a transaction over a certain amount from a certain region got auto-flagged. The logic worked. Nobody could explain why anymore. That’s actually a worse position than something being broken outright — broken things get fixed, things nobody understands just get left alone, quietly, until they cause a real incident.
The Node.js version of this story is almost identical, just with promises instead of nested ifs. We were building rule-based routing for support tickets — priority, team assignment, SLA tiers — directly inside an Express middleware layer. Fast to build. Fast to break, too. A teammate touched the SLA logic to add a new tier and accidentally changed how an unrelated routing rule evaluated, because both shared a helper function nobody remembered was shared. Libraries like json-rules-engine exist for exactly this reason — we just didn’t reach for one until after the incident.
And then there’s the SQL stretch, which I think gets overlooked the most. At one company, half our “business rules” lived as CASE WHEN chains inside stored procedures, leaning on nothing more exotic than standard conditional logic. Honestly not a bad approach for certain things. Fast, transparent if you know SQL, easy to audit with a query. But nobody outside the data team could read it, and the moment a rule needed conditional logic spanning multiple tables, the query started looking like abstract art.
What a friend told me about the C# version
I haven’t built one of these in C# myself, but a developer friend has lived with one for a couple of years at his company, and the way he describes it matches the pattern almost exactly. They’re running something built on top of NRules, and on paper it’s a clean setup. In practice, only two people on a twelve-person team are comfortable editing the rule definitions. Everyone else routes changes through them. He told me the backlog for “small rule tweaks” is longer than the backlog for actual features, which is a strange thing to sit with once you notice it.
The actual pattern, if there is one
Here’s what all of these had in common, once I stopped looking at the language and started looking at the decision sitting underneath it: every single one treated rules as code. Not data. Code that needs a developer, a deploy pipeline, and a code review just to change a number or a threshold.
That’s closer to the real reason most rule engines fail in production than scaling issues or bad architecture, honestly. I’ve written about this pattern in more depth before — why most rule engines fail in production — but the short version is that the failures I’ve actually run into weren’t about throughput or load. They were about ownership. A rule that only an engineer can touch is a rule that takes two days to update when the business needed it changed an hour ago.
Most rule engine design patterns you’ll come across eventually circle back to that same question — is the rule data, or is it code dressed up to look flexible? I’ve gone deeper into the common rule engine design patterns elsewhere, since it’s too big a tangent to fully unpack here.
There’s a smaller, separate mistake buried in a couple of these stories too — picking the wrong chaining direction for the problem. The fraud system would have been a much better fit for backward chaining, starting from “is this fraudulent” and working backward through which conditions support that conclusion, instead of forward chaining through every rule on every transaction regardless of relevance. We didn’t think about this distinction at the time. We just wrote rules in whatever order they occurred to us, which is how most rule logic actually gets written, if I’m being honest. If the difference between the two isn’t obvious, I broke it down with examples here: forward chaining vs backward chaining.
What eventually worked, for me
The fix that stuck, once I actually went looking for one, was pulling rules out of the codebase entirely and into something a non-engineer could open and understand. This is the exact gap that pulled me toward working on Nected in the first place — it approaches the problem from the data-not-code side, instead of bolting an engine onto whatever language a team happens to be using. Rules get defined visually, with versioning and an audit trail attached by default, so a change doesn’t quietly disappear into a git blame six months later. A business user can edit a threshold without filing a ticket and waiting for a sprint.
Written down like that, it sounds obvious. It wasn’t, while we were three sprints deep into a Python conditional chain that “worked fine, mostly.” What changed wasn’t the language underneath. It was deciding, deliberately, that the rule didn’t belong inside the application at all.
If there’s one thing I’d tell anyone starting a rule engine from scratch, it’s this — decide on day one whether a rule is data or code. Everything else, which language, which design pattern, forward or backward chaining, matters a lot less than getting that one decision right early. Undoing it later is expensive in a way that’s hard to see coming, right up until you’re the one explaining a free-shipping bug to your CTO at 11pm.
메타데이터
- post_id
- f16644becd58
- slug
- ive-rebuilt-the-same-rule-engine-in-four-languages-it-broke-the-same-way-every-time-f16644becd58
- url
- https://levelup.gitconnected.com/ive-rebuilt-the-same-rule-engine-in-four-languages-it-broke-the-same-way-every-time-f16644becd58
- canonical_url
- https://levelup.gitconnected.com/ive-rebuilt-the-same-rule-engine-in-four-languages-it-broke-the-same-way-every-time-f16644becd58
- author_url
- https://medium.com/@snehasishkonger
- status
- ok
- fetched_at
- 2026-07-08 10:09:58