Your `if` statements are a database nobody can query

작성자

카테고리:

← 피드로
DEV Community · Pablo Albornoz Afanasiev · 2026-08-16 개발(SW)

Somewhere in your codebase there is a line that looks like this:

if (user.plan === 'enterprise' || user.tenantId === 'acme-corp') {
  // ...
}

Enter fullscreen mode Exit fullscreen mode

Nobody remembers who wrote the second half of that condition. It has been there for two years. It is almost certainly still load-bearing.

Here is the thing I want to convince you of: that line isn’t code. It’s data, and it’s stored in the worst possible place.

Every conditional that encodes a business decision is really a row. It has a condition, an outcome, and a bunch of implicit context about when it applies. You have hundreds of these rows. They’re spread across a dozen services, written in four different styles, and there is no way to list them.

You have a database. You just can’t query it.

Five things a database gives you that your code doesn’t

Once you look at it this way, the problems stop feeling like sloppiness and start feeling structural.

There’s no schema. One service decides a customer is premium by checking plan === 'premium'. Another checks subscription.tier > 2. A third checks a flag that was set during a migration in 2023. All three are “the same rule” until the day they aren’t, and there’s nothing in the system that would notice the drift.

There’s no way to query it. Try to answer a simple question: what rules are live in production right now? You can’t. Someone has to read the source. And grep won’t save you, because the interesting conditions are compound, spread across guard clauses, and half of them are expressed as an early return rather than an if.

There are no migrations. Changing a rate limit from 100 to 200 requires a pull request, a review, a CI run, and a deploy window. You’re pushing a code change through the full pipeline to change a number. It’s a schema migration with none of the tooling that makes schema migrations tolerable.

There’s no audit log. Git tells you who edited the line. It doesn’t tell you who decided the rule, when it was supposed to expire, or whether the customer it was written for is still a customer. Blame gives you an author and a timestamp, which is the least interesting part of the history.

There’s no access control. The person accountable for the pricing policy can’t read the pricing policy. They have to ask an engineer, who has to find it, and any change routes through a sprint. Not because anyone designed it that way — just because the rule lives in a file that only engineers can open.

None of these are hard problems in a database. All of them are unsolved in your codebase.

The usual fix trades one problem for a worse one

The standard answer is to pull the rules into a service. Remote config, a feature flag platform, a policy server. And it genuinely works: you get a UI, you get an audit trail, you get changes without a deploy.

But look at what just moved. Your application now makes a network call to decide whether a user can read a document. That call is in the request path. It has a latency budget, a failure mode, and a dependency on somebody else’s uptime.

Which brings us to the part that always bothered me most. The single most valuable rule in your system is the kill switch — the one you flip when something is on fire at 3 a.m. And in this architecture, flipping it requires a third-party service to be up, during exactly the kind of incident where things are not up.

You’ve fixed visibility by creating an availability problem. That’s a real trade, and for a lot of teams it’s the right one. But it’s not the only option.

What you actually want

Work backwards from the five problems, and the requirements fall out on their own:

  1. A declarative format with a schema, so a rule is a thing you can validate rather than a thing you have to read
  2. One artifact, so “what’s live right now” is a file you can open instead of an archaeology project
  3. Changeable without a deploy, because that’s the whole point
  4. Verifiable, because a file that decides who can do what is a file worth signing
  5. Evaluated locally, so nothing sits between your request and its answer

That last one is what keeps the 3 a.m. case honest. If the rules are already on the machine, there’s no service that can be down when you need them.

This is what we ended up building, and it’s called Govplane. Rules compile into a signed JSON bundle. You ship it like any other config artifact — a Git commit, an object in a bucket, a layer in your image. Your app loads it, checks the signature, and evaluates everything in memory.

Four use cases that turned out to be the same one

Here’s the part I didn’t expect when we started.

Feature flags are the obvious case. A policy matches on some context and returns allow or deny, and your code asks instead of assuming:

const decision = govplane.evaluate({
  target: { service: 'web', resource: 'checkout-v2', action: 'access' },
  context: { plan: 'pro', region: 'eu' },
});

if (decision.decision === 'allow') {
  // new checkout
}

Enter fullscreen mode Exit fullscreen mode

Kill switches are the same mechanism with a different effect. kill_switch sits at the top of the precedence order, above deny, above throttle, above allow — so a rule that shuts something off wins over every rule that would have permitted it. Flipping it means shipping a new bundle, which is a file copy, not a deploy.

RBAC is where it got interesting. Permissions are the same shape as flags: something about the caller, something about the resource, an allow-or-deny at the end. Put the role in the context and the resource in the target, and authorization is just another policy in the same bundle.

This is also where signing stops being a nice-to-have. A file that decides who can delete records is a file that needs provenance. The SDK verifies the signature before it will evaluate anything — an unsigned bundle is refused unless you explicitly opt in.

Custom effects are the one I’d point at if you only read one section.

The first three all answer a yes-or-no question. But policies can return a payload instead — and that means a policy can decide not whether something happens, but with what value it happens.

We use this for things like per-tenant limits, where the answer isn’t “allowed” or “denied” but “your ceiling is 500”:

const decision = govplane.evaluate({
  target: { service: 'api', resource: 'uploads', action: 'create' },
  context: { tenant: 'acme', plan: 'pro' },
});

if (decision.decision === 'custom') {
  maxUploadSize = decision.parsedValue.maxUploadSizeMb;
}

Enter fullscreen mode Exit fullscreen mode

A custom decision carries the value twice: value as a raw string, and parsedValue as the deserialized object when you enable parseCustomEffect. The string is the canonical form — it’s what gets signed — so whatever shape you put in there travels through the bundle untouched and comes out the other side exactly as you wrote it.

That number used to be a const in a config file, with a couple of ifs wrapped around it for the tenants that negotiated something different. Now it’s a rule, in the same bundle as everything else, changed the same way.

Once you can return values, the boundary between “feature flag system” and “business rules engine” stops being meaningful. It’s the same lookup either way. The only difference is what comes back.

Where this leaves you

I’m not going to pretend every conditional in your code should move. Most if statements are control flow and belong exactly where they are.

But some of them aren’t. Some of them are business decisions that happen to be written in JavaScript — the ones somebody from finance or compliance would want to read, the ones that change on a different schedule than your code, the ones you’d want to flip in a hurry. Those are data pretending to be logic, and treating them as data makes the whole system easier to reason about.

Govplane is MIT licensed and the bundle specification is public, so you can write and sign a bundle yourself without running any of our tooling. It runs entirely on your machine, in production, without an account. There’s a hosted version for teams that want delivery and observability handled, but it’s optional and most people won’t need it.

If you’ve got a || user.tenantId === 'acme-corp' sitting in your codebase right now, you already know which rules I’m talking about.

원문에서 계속 ↗