DEV Community

Cover image for Build a Tiny Feature Flag Evaluator, Then Explain Rule Precedence in an Interview
Karuha
Karuha

Posted on • Originally published at aceround.app

Build a Tiny Feature Flag Evaluator, Then Explain Rule Precedence in an Interview

A feature flag is not just an if statement with a remote config behind it. It is a policy decision: given a context, which rule wins, and can someone explain that answer later? In interviews, the hard part is rarely naming flags. It is making the evaluation order, defaults, and failure modes unambiguous.

Feature-flag evaluation flow: context enters ordered rules; the first match returns a value, otherwise the default is returned.

Why a 30-line evaluator is a useful interview exercise

“Add a feature flag” sounds like a small task. The moment a flag is targeted by plan, region, or team, it has a policy. Policies need an order.

Consider this product request:

  • Pro accounts should see a new export flow.
  • The beta team should see it during internal testing.
  • EU traffic should remain on the old flow for now.
  • Everyone else should get the safe default.

There is an ambiguity hiding in that list. What happens to a Pro account in the EU? If the EU rule is evaluated first, it is held back. If the Pro rule is evaluated first, it gets the new flow. Neither answer is inherently correct; an undocumented answer is the problem.

A good interview answer starts there: state the precedence rule, make it executable, and add examples that prove the conflicting cases. That is more convincing than saying “I would use LaunchDarkly” and moving on.

A tiny, deterministic evaluator

This example is deliberately small. A rule is an AND-match over context fields. Rules are checked in source order. The first match wins. If no rule matches, the evaluator returns the default value and a reason that makes the result inspectable.

const assert = require("node:assert/strict");

function matches(when, context) {
  return Object.entries(when).every(
    ([key, value]) => context[key] === value,
  );
}

function evaluate(flag, context) {
  const rule = flag.rules.find(({ when }) => matches(when, context));

  if (rule) {
    return { value: rule.value, reason: rule.name };
  }

  return { value: flag.default, reason: "default" };
}

const newExport = {
  default: false,
  rules: [
    {
      name: "paid-plan",
      when: { plan: "pro" },
      value: true,
    },
    {
      name: "beta-team",
      when: { team: "beta" },
      value: true,
    },
    {
      name: "eu-holdback",
      when: { region: "eu" },
      value: false,
    },
  ],
};

assert.deepEqual(
  evaluate(newExport, { plan: "pro", region: "eu" }),
  { value: true, reason: "paid-plan" },
);

assert.deepEqual(
  evaluate(newExport, { plan: "free", team: "beta" }),
  { value: true, reason: "beta-team" },
);

assert.deepEqual(
  evaluate(newExport, { plan: "free", region: "eu" }),
  { value: false, reason: "eu-holdback" },
);

assert.deepEqual(
  evaluate(newExport, { plan: "free", region: "us" }),
  { value: false, reason: "default" },
);

console.log("4 evaluation cases passed");
Enter fullscreen mode Exit fullscreen mode

The code is not a replacement for a mature flag platform. It is an interview-sized model of the decisions a platform must make. Run it with Node 18+ and it prints:

4 evaluation cases passed
Enter fullscreen mode Exit fullscreen mode

The first assertion is the one worth discussing. The context matches both paid-plan and eu-holdback, but the first rule wins. That is a product decision encoded as a technical invariant.

What should you say while walking through it?

A solid explanation has four beats.

First, describe the input contract. The caller supplies a flag definition and a context. In a real service, validate both at the boundary. A missing region should not accidentally behave like a region that was deliberately excluded.

Next, name the precedence rule before showing the code. Here it is “first match wins.” You could choose most-specific-match instead, but then define specificity, resolve ties, and test the rule. Implicit precedence is how rollout policies become surprises.

Then explain observability. Returning reason is tiny, but it gives a support engineer an answer to “why did this user see the old flow?” Production systems should also record the flag key, rule identifier, evaluation timestamp, and a privacy-safe version of the context. Do not log a raw email address just to debug a flag.

Finally, talk about the default. A default is not filler. It is the behavior during a provider outage, a malformed configuration, or a new user who matches no segment. For a risky write path, a disabled default is often the safer choice. For an existing revenue-critical read path, the safe default may be the established experience.

How would this change in production?

The next layer is not “add more if statements.” It is adding explicit constraints.

  • Use a schema so malformed rules fail before they reach users.
  • Keep rule order in the stored configuration; do not depend on object iteration or a database query without an ORDER BY.
  • For percentage rollouts, hash a stable identifier such as an account ID. Random selection on every request makes a user bounce between experiences.
  • Cache configuration with a bounded TTL, then define what happens when the provider is unreachable.
  • Separate evaluation from side effects. Evaluating a flag should not create a record, send a message, or mutate a rollout counter.
  • Version definitions and include that version in telemetry. It makes an incident timeline explainable.

This is also where it helps to acknowledge trade-offs. A local cached snapshot gives low latency and resilience, but it may be stale. A remote evaluation service centralizes policy, but adds network dependency and privacy considerations. Interviewers are usually listening for whether you can make those costs visible, not whether you claim there is a universal choice.

A ten-minute practice drill

Take the snippet and swap the requirements:

  1. A new dashboard is on for the internal team.
  2. It is off for accounts on a legacy contract.
  3. It rolls out to 10% of eligible accounts.
  4. It is off by default.

Before touching code, write the precedence sentence. Then list one context that matches two rules and one that matches none. Those two examples expose most rule-engine bugs.

If you want to rehearse that explanation aloud with follow-up questions, aceround.app — AI interview assistant is one option for turning a short design drill into a mock interview.

References

Disclosure: AI assistance was used to edit an author-reviewed draft; the example and its four assertions were run before publication.

Top comments (0)