Engineering Practice
Routing Rules Are Data, Not Code
When the decisions a system makes change far more often than the machinery that executes them, the decisions belong in a data table — so a routing change is an edited row, not a code deploy.
- Engineering Practice
- Architecture
- Configuration
- Design
One of the most clarifying things I’ve seen in a large, busy system was how cleanly it separated the decisions from the machinery. The engine that processed requests barely changed. The rules about where each request should go changed all the time — and those rules lived in a data table, not in the code. So the overwhelming majority of “routing changes” were edits to rows in that table, never a code change at all. That split is one of the highest-leverage architectural moves I know, and it applies far beyond the system I learned it in.
Two things change at very different rates
Inside almost any dispatch-shaped system there are two kinds of logic with wildly different change frequencies. There’s the machinery: how you parse a request, evaluate a condition, hand off to the next stage. That’s stable — it’s the engine, and it changes rarely. Then there’s the policy: which input goes to which destination, which cases get rejected, which path a given category takes. That changes constantly, because it tracks the messy outside world.
When you tangle those two together — encode the policy directly in the engine’s code — you’ve coupled the thing that changes weekly to the thing that changes yearly. Every routine policy tweak now requires touching, testing, and redeploying the engine, with all the risk that implies. You’ve made the volatile part as expensive to change as the stable part, which is exactly backwards.
Put the volatile part in a table
The fix is to pull the decisions out into data: a table of rules the engine reads and interprets, rather than logic the engine hard-codes. Each rule is a row — a condition and what to do when it matches (send here, reject, tag as that category). The engine becomes a generic evaluator that walks the table; the table holds the actual policy.
The engine answers “how do I evaluate a rule.” The table answers “what are the rules.” Keep those in different files and most of your changes stop touching code.
In the system that taught me this, that’s exactly how it played out: changing where a category of request routed meant editing a rule row, not editing the program. The machinery didn’t know or care about the specific policies — it just faithfully evaluated whatever the table said. New policy, new row.
Why this is worth the indirection
Table-driven dispatch costs you a little indirection up front and pays it back many times over:
- Changes get cheaper and safer. Editing a data row has a smaller blast radius than editing engine code. You’re changing what the system decides, not how it decides, so you can’t accidentally break the evaluation machinery while adjusting a policy.
- The rules become legible. A table of conditions-and-outcomes is something you can read top to bottom and actually understand the system’s behavior from. Policy buried across thousands of lines of imperative code is not.
- More people can safely change it. A reviewable rule table is approachable to people who shouldn’t be editing the engine’s internals. The decision surface is separated from the implementation surface.
- It’s testable as data. You can exercise the rule set with fixtures — feed inputs, assert destinations — without standing up and mutating the whole engine.
The discipline that keeps it honest
Data-driven logic has a failure mode, and it’s worth naming so you don’t trade one mess for another. A rule table can rot into an unreadable thousand-row sprawl just like code can, and “it’s only data” can become an excuse to skip review on changes that are every bit as consequential as code changes — because they are. A routing rule edit is a behavior change; it deserves the same review and testing as one.
So the table isn’t a free pass. It needs the same care code gets: version control, review, tests against fixtures, and enough structure that a human can still reason about it. The win isn’t “data doesn’t need discipline.” The win is that you’ve moved the frequent, volatile changes onto a surface where that discipline is cheaper and safer to apply than it would be in the engine.
Where the line actually falls
The judgment call is what counts as policy versus machinery, and the test is change frequency. If something changes far more often than the code around it, and it’s expressible as a condition-and-outcome, it probably wants to be data. Routing decisions, eligibility rules, feature gating, pricing logic — these are classic “policy” that too often ends up hard-coded. The machinery that evaluates them should be boring and stable; the policy should be editable without a deploy.
This is a cousin of treating a config language as real code — there, the point is that externalized config deserves engineering rigor; here, the point is which logic to externalize in the first place. It also pairs with the idea that environments should differ by config, not code: the more of your variability you can express as data the engine reads, the less of it you have to express as branches the engine hard-codes. If you’ve got policy logic calcified into an engine and want to talk through prying it out, I’m easy to reach.