Complex Business Logic Requires a Declarative, Not Procedural, Approach


Why an LLM that can write Python still needs a declarative modeling layer underneath it — and what changes the moment the data changes

 

Sanjay Mittal

Founder & CEO
Predictika.ai

 

Email: sanjay@predictika.com

 

 

The objection, stated fairly

 

Every whitepaper in this series has argued that complex business logic — CPQ pricing, network change validation, journal-entry audit — belongs in a declarative constraint model rather than hand-written procedural code. A fair reader, watching an LLM write fluent Python in one breath and fluent LVML in the next, is entitled to ask why that matters anymore. If the model can produce either, why insist on the extra step of a modeling language at all? Why not let the LLM write ordinary procedural logic and skip the translation?

 

This paper takes that question seriously rather than deflecting it, because the honest answer is not “procedural code can't compute the right thing.” It can. The honest answer is narrower and, we think, more durable: procedural code makes correctness depend on the author enumerating, by hand, every combination of conditions that can arise and every path along which a later change has to propagate. A declarative model doesn't remove that complexity — it relocates it, from something a person has to track case by case to something a general-purpose engine tracks mechanically, once, for every rule set that uses it. That relocation is the entire argument. Everything below is evidence for it, not restatement of it.

 

Two different claims, so we don't argue the wrong one

 

It helps to separate two questions that get run together in this debate:

 

The first half of the paper is about growth: what happens to correctness as the number of interacting rules increases. The second half is about change: what happens after the rules are already correct, when one input is edited and everything downstream of it has to catch up. They are related but genuinely separate failure modes, and we've watched both happen — one in our own project, one in a widely-used commercial CPQ product.

 

Correctness under growth: combinatorial interaction vs. declared invariants

 

Procedural code expresses a rule as a branch buried inside a sequence: an if, nested inside a function, called at some point in a chain of other functions. Whether that branch fires correctly on a given call depends on everything that ran before it in that call, and on every other branch that might also be touching the same state. Add a second rule and you haven't added one new thing to verify — you've added a new interaction between the new rule and every rule already there. The number of combinations to reason about grows with the number of rules, not the number of lines.

 

A declarative constraint doesn't avoid that growth in interactions — the underlying business logic is exactly as tangled either way — but it changes who's responsible for checking them. Each rule states an invariant that must hold, independently of how the engine gets there: “if the line is part of a discount bundle and the bundle total exceeds this threshold, the tier must be X.” It doesn't call the other rules and it doesn't know what order they run in. A general-purpose engine is responsible for checking every rule against every relevant combination of resolved values, exhaustively, every time. That's the whole trade-off: procedural code asks a person to have anticipated the interaction in advance; a constraint engine asks a person to have stated the invariant, and checks the interaction for them, every time, whether or not anyone thought to ask about that particular combination.

 

Procedural code with functions coupled through shared mutable state, next to declarative constraints C1 to C5 that each connect only to a central engine and parameter store

A real example, from this project, three weeks ago

 

The cleanest illustration we have didn't come from a hypothetical — it came from our own build. While validating the journal-entry audit model against a real general-ledger export, we ran the live LVML engine against a set of test cases and compared its verdicts to a hand-written Python simulator we'd built to predict expected results ahead of time, case by case. On one case (an entry touching a normally-automated posting account, submitted manually, inside a locked period) the two disagreed. The live engine was right. The simulator was wrong, not because its author misunderstood the rule, but because the simulator never modeled that particular rule's interaction with a rarely-exercised action type — the combination simply hadn't been front-of-mind when the procedural check was written.

 

That is the combinatorial-interaction failure in miniature, and it's worth being precise about what it does and doesn't show. It doesn't show that declarative models are bug-free — LVML itself had real bugs during this same project, including load-order and domain-scoping issues that had nothing to do with rule interaction (more on this in the honest-limits section below). What it shows is that the two kinds of bugs are structurally different. The engine's bugs were bootstrapping problems — get the file structure and load order right once, and they're gone for good. The simulator's bug was a missed interaction, the kind that has no natural place to hide a fix, because there's no engine enumerating combinations on the simulator's behalf. The next missed interaction will look exactly like this one: correct-looking code, wrong on a case nobody thought to write a branch for.

 

Correctness under change: propagation to a fixpoint

 

When one input changes, which rules need to be re-checked — and which of their downstream effects need to be re-checked too? That question doesn't have an obvious answer in procedural code, because procedural code has no explicit graph of what depends on what. It only has whatever call sequence the author wrote down.

 

This is the argument this paper exists to make, because it's less obvious than the interaction argument and, in our experience building several of these models over the past few weeks, it's the one that actually decides whether a system stays correct in production. Growth is a problem you can budget testing time for. Propagation is a problem that shows up quietly, weeks after a system ships, the first time someone edits one field and three other fields that logically depend on it silently stay stale.

 

What “propagation to a fixpoint” means

 

In a declarative model, every rule's condition and consequence name specific parameters. That's enough for an engine to build a dependency graph automatically: parameter A feeds rule R, rule R sets parameter B, parameter B feeds rule S, and so on. When A changes — because a user edited it, or because an earlier rule computed a new value for it — the engine doesn't need to be told what else might be affected. It looks up which rules read A, re-evaluates exactly those, and if any of them changes a value that other rules depend on, those get queued too. This continues, one wave at a time, until a pass produces no further changes. That stable state is the fixpoint: every rule's output is consistent with every rule's current inputs, and nothing left on the worklist would change if it were re-run again.

 

Parameter dependency graph from bundle component through line total, discount tier, order total and approval, and the step-by-step propagation of one change to a fixpoint

The mechanism matters because of what it removes from the author's job. In LVML specifically, the engine's default behavior is deliberately conservative in a related but different way: it stops at the first hard constraint failure rather than backtracking through alternatives, because in practice a later failure is often just an artifact of the same root conflict, and exhaustive reporting rarely saves a real round-trip. That's a separate design choice from propagation — propagation is about re-firing rules whose satisfied inputs changed, not about searching for an alternative assignment after a failure. The two shouldn't be conflated: propagation-to-fixpoint is the ordinary way a declarative engine keeps a satisfied model consistent after an edit; true backtracking search is a distinct, explicitly-invoked capability for finding a different valid assignment when one is needed. The propagation behavior described here is the default, always-on one.

 

A worked example: a discount cascading through a quote

 

Picture a CPQ quote with a bundled line item. A sales rep swaps one component in the bundle. That single edit has to ripple through: the bundle's line total changes; the line total change may cross a discount-tier threshold; a tier change may cross an approval-authority threshold; and the order total, which rolls up every line, has to be recomputed before anyone can tell whether approval is even required. Four hops, in a real deal, from one small edit.

 

In a declarative model this is simply what the engine does by construction — it doesn't need a human to have anticipated this exact chain, only for each individual rule (“bundle total determines tier,” “tier determines approval requirement”) to have been stated correctly on its own. In procedural code, someone has to write the equivalent of a change-notification system by hand: an explicit call from the bundle-edit handler to the tier-recalculation function, from there to the approval check, from there to the total recompute — and, critically, they have to get the *order* right, and they have to remember to add a new call every time a new dependency is introduced later. Nothing about the language stops a future edit from missing one hop in that chain. Nothing catches the omission until a quote silently reaches a customer with a stale approval status.

 

We've watched a version of this exact symptom in the wild, in production CPQ software: a change to a special price on a quote line failed to trigger the price-rule recalculation that was supposed to depend on it, because the price rule's execution scope didn't include the field that had actually changed. We're not in a position to certify the precise root cause of that specific case from the outside — that would require access we don't have — but the symptom is the textbook shape of a missed propagation edge: a downstream rule that depends on an upstream value, wired up by hand, with the wiring silently incomplete. That's not a criticism of the engineers who built it. It's the natural failure mode of any system that has to hand-maintain its own dependency graph instead of deriving one from declared rules.

 

A brief, deliberately limited spreadsheet analogy

 

A spreadsheet is the one place most people have already felt propagation-to-a-fixpoint work correctly without thinking about it: change one cell, and every formula that references it recalculates, and every formula that references *those* cells recalculates too, automatically, in dependency order. That's a genuinely useful intuition for the core idea, and it's why we reach for it here.

 

It's worth being honest about exactly where the analogy stops helping, because pushed further it becomes misleading. A spreadsheet's dependency graph is a graph of arithmetic formulas, not of conditional business rules — there's no equivalent of “if this bundle's total crosses this threshold, a different rule entirely now governs the tier.” Spreadsheets have no native concept of constraints that can conflict, no backtracking, no notion of a rule that's currently unsatisfiable pending more information, and no iterative convergence check when logic (not just arithmetic) needs to stabilize over several rounds. The comparison is a useful first foothold for the propagation idea and nothing more; treat it as an analogy for *what it feels like when propagation works*, not as a model for what a constraint engine actually has to do to make it work on business logic instead of formulas.

 

Why declarative rules expose this graph, and procedural code structurally can't

 

The dependency graph isn't something we build for a declarative model after the fact — it falls out of how the rules are written. A rule's condition and consequence name the exact parameters it reads and the exact parameters it sets. That's already, mechanically, an edge list: read this, write that. A generic engine can walk every rule once, at load time, and produce the full graph for any model, without anyone authoring the graph directly.

 

Procedural code has no equivalent structural signal. A function reads and writes whatever variables its body happens to touch, and that information isn't declared anywhere the language forces you to state up front — it's implicit in the code, discoverable only by reading it (or, in a large codebase, by tracing calls that themselves call other calls). Nothing stops a function from reading a global that isn't obvious from its signature, or from being called from three different places in three different orders. Static analysis tools can approximate a call graph after the fact, but that's a diagnostic bolted on after the code exists, not a property the code was built to expose. The dependency graph a declarative model gives you for free is exactly the artifact procedural code would need a separate engineering effort to reconstruct — and reconstruct freshly, project by project, rather than inheriting once from a shared engine.

 

The right architecture: a declarative core with narrow procedural escape hatches

 

None of this argues that procedural code is bad at what it's actually good at: pure computation with no branching business logic behind it — summing a list, computing a weighted average, parsing a date. LVML itself leans on this directly. The journal-entry audit model uses a small number of user-defined functions (UDFs) for exactly this kind of work — narrow, single-purpose aggregations with no conditional logic of their own. The UDF is always called *by* a declared rule; it never calls a rule itself, and it never decides which rule fires next. That asymmetry is the architecture we're actually recommending: declarative rules own the branching, the interactions, and the propagation; procedural code is a computation a rule can delegate to, not a place where control flow lives.

 

This is a stronger and more falsifiable claim than “declarative is always better,” and it's worth stating precisely because the UDF pattern itself is evidence for it. If procedural code were the right place for conditional business logic, there would be no reason to keep UDFs this narrow. The fact that the pattern works well specifically when confined to pure computation — and starts breaking down the moment a UDF tries to make a decision that should have been a rule — is itself a small, repeated confirmation of the larger argument.

 

What this argument does not claim

 

A few honest limits, stated plainly rather than buried in a footnote:

 

Bottom line

 

Procedural code and declarative constraints can compute the same answer. They differ in who is responsible for noticing when a new rule interacts with an old one, and who is responsible for knowing what has to be re-checked after something changes. A declarative model answers both questions structurally, once, for every rule that follows it. Procedural code answers them only as reliably as the person who last touched the code remembered to.

 

That's a maintainability claim, not a computability one, and it's the reason this series keeps returning to the same architectural recommendation across very different domains — CPQ pricing, network change validation, journal-entry audit, and now the general case of any system where an AI agent is proposing actions against business logic complex enough that a person can no longer hold every interaction and every dependency in their head at once. As agents take on more of the work of both writing and executing this logic, the case for keeping the logic itself declarative, rather than delegating it to procedurally-generated code, gets stronger, not weaker: an agent is exactly as prone to missing a combinatorial interaction or a propagation edge as the engineers in the examples above, and exactly as well served by an engine that checks for both automatically.

 


 

 

About Predictika

 

Predictika.ai builds a patented constraint-based configuration and pricing engine rooted in a research lineage that runs from Xerox PARC through Selectica, the enterprise CPQ pioneer whose customers included IBM, Cisco, Dell, GE, Hitachi, Disney, and AT&T. Predictika's declarative modeling language, LVML, carries forward a distinctive prototype object-oriented architecture in which sub-entities inherit both parameters and constraints from the structures that contain them. The company holds patents covering deterministic validation of LLM-driven systems (U.S. 11,544,475 and 11,914,970).

 

About the author

 

Dr. Sanjay Mittal is the founder and CEO of Predictika and the architect of its constraint-based reasoning engine. He began his career as a research scientist at Xerox PARC (1982–1990), with pioneering work in artificial intelligence and constraint reasoning. He went on to found Selectica, which defined enterprise sales configuration (CPQ) for large organizations, building it from inception to a $5 billion IPO in 2000 and serving customers such as IBM, Cisco, Dell, GE, Hitachi, Disney, AT&T, and Reliance. Dr. Mittal was the inventor of Selectica’s industry-leading configuration engine and holds six patents from that work. At Predictika he has carried the constraint-based approach into the era of large language models, with further patents — including U.S. Patents 11,544,475 and 11,914,970 — covering the deterministic validation of AI-driven systems. He holds a B.Tech. from IIT Kanpur and a Ph.D. from Ohio State University.

 

Suggested Further reading

 

Requirements for a Constraint Server Behind an LLM Front End

 

Accuracy Is Not Consistency: Why Enterprise AI Needs Deterministic Guardrails

 

Syntactically Valid, Semantically Wrong: A Constraint-Based Approach to Safe AI-Agent Execution

 

ExecGuard: Execution Integrity for Enterprise AI Agents

 

Complex Product Configuration Requires a Constraint-Based Approach

 



Copyright © Predictika Inc. 2026