Kuga
Writing

Why I built a config registry instead of writing more controllers

When your business changes rules faster than engineering can deploy, the right move isn't to ship faster — it's to take engineering off the critical path.

Apr 22, 2026

The business rules at the insurance platform I architected changed every two weeks. Engineering deploys ran every two weeks. You can already see the problem.

Every time the business defined a new agent incentive — "$2K bonus if they sell ≥ $150K premium AND either ≥ 15 policies OR ≥ $10K commission this month" — someone on the engineering side translated it into code, opened a PR, got it reviewed, deployed it, and waited for QA to confirm. By the time the rule was live, the business had already drafted the next two.

The first instinct is to ship faster. Better tests, better CI, smaller PRs, on-call rotation. We tried all of that. None of it solved the underlying shape of the problem: engineering was on the critical path of a business process that didn't actually need engineering.

So we changed the shape.

The pattern

Three pieces, composed:

1. A registry of metric definitions. Every business metric — total premium, count of policies, agent commission — lives in a Python registry. Each entry declares the table, joins, aggregation type, and valid filters:

{
  "parameter": "issued_policy_premium_amount",
  "base_table": "crmp_issued_policies",
  "field": ["premium_amount"],
  "agg": "sum",
  "joins": [
    { "table": "crmp_policy_base",
      "on": "crmp_issued_policies.policy_base_id = crmp_policy_base.id" },
    { "table": "core_users",
      "on": "crmp_issued_policies.sales_agent_id = core_users.id" },
  ],
  "agent_field": "crmp_issued_policies.sales_agent_id",
  "filters": ["risk_type_id", "product_id", "policy_effective_date"],
}

Adding a new metric is a single registry entry. No new controller, no serializer, no migration.

2. A recursive logic tree. Business rules are JSON, not code. A rule is either a logical node (AND / OR) with children, or a leaf condition that compares a metric to a threshold. The evaluator recurses, short-circuits AND on the first failure, returns from OR on the first match.

def evaluate(tree, data):
    if "logic" in tree:
        results = [evaluate(c, data) for c in tree["conditions"]]
        return (
            all(r for r in results) if tree["logic"] == "AND"
            else any(r for r in results)
        )
    return compare(data[tree["field"]], tree["operator"], tree["value"])

That handles arbitrarily nested rules. The deepest production rule we shipped was 4 levels deep. The engine didn't notice.

3. A dynamic query builder. The aggregation layer pulls every field referenced in a rule tree, looks each one up in the registry, and constructs the SQL on the fly — selects, joins, agent filter, period filter, aggregation. One round trip per metric.

The tradeoff

You pay this pattern up front. The framework — registry, evaluator, aggregator — was the first three weeks of work. If the system had needed ten incentive rules total, ever, this was bad architecture. Hand-write ten controllers and go home.

We had thirty rules in the first quarter and the business kept adding them. Every rule after the framework existed cost zero engineering time.

The point isn't that registry-driven design is universally better. The point is that the right architecture is a function of how the system changes. If the business writes the rules and engineering writes the framework, the critical path is wherever the slow side is. Move engineering off the slow side.

When NOT to do this

  • You have a small fixed rule set. Five known rules that won't change? Write five controllers. The framework is overhead.
  • The business doesn't actually want self-service. If "config-driven" means a JSON file in a git repo that an engineer still has to edit, you haven't moved engineering off the critical path. You've just renamed the PR.
  • The rules need novel data shapes. A registry assumes the underlying data model is stable enough that new rules are combinations of existing metrics. If every new rule needs a new table, you don't have a rules problem — you have a domain modeling problem.

The actual win

It wasn't that engineering shipped less code. It was that the engineering team stopped being the rate-limiting step on a process that didn't belong to them. The business owns the rules now. We own the framework that runs the rules. Cleaner contract, faster feedback loop, fewer Slack threads about why the next incentive batch hasn't shipped yet.

That's the real reason to build the registry. Not the code reuse. The contract.