Skip to content
kc@kumarChandrachooda.com:~$ cd /blog/the-feature-flag-that-is-a-constant && read --section="top" 0%
Architecture

The Feature Flag That Is a Constant

A fully built cross-cutting decorator disables an entire context relationship behind a local variable that is always false - while a sibling module enforces the identical rule for real.

By Kumar Chandrachooda 24 Nov 2025 6 min read
A switch wired into everything, welded to off

Architecture drift is usually invisible. It happens as accumulation — a shortcut here, a bypass there, an abstraction that stopped being used — and by the time anyone notices, the diagram and the system disagree in a hundred small ways and nobody can point at the commit where it started. Occasionally, though, the whole drift fits in one line, and you can read it in three seconds.

Part 13 was about a handler that acts on messages it should ignore. This part is about a handler that ignores every message it should act on, and the line responsible.

The line

public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default)
{
    //We could check the appsettings.json settings here to enable/disable this feature on demand
    var enabled = false;

    if (enabled is false)
    {
        await _commandHandler.HandleAsync(command, cancellationToken);
        return;
    }
    
    var cashierId = _userContextAccessor.Get().CashierId;
    var check = command switch
    {
        AddOfferVariantCommand c => new WorkloadAccessCheck(nameof(Offer), c.OfferId.ToString(), cashierId),
        ConfirmOfferVariantCommand c => new WorkloadAccessCheck(nameof(Offer), c.OfferId.ToString(), cashierId),
        RevealOfferCommand c => new WorkloadAccessCheck(nameof(Offer), c.OfferId.ToString(), cashierId),
        _ => throw new InvalidOperationException("Cannot enforce workload management rule!")
    };

    var isAssigned = await _workloadManagementApi.CanAccessWorkload(check, cancellationToken);

    if (isAssigned)
    {
        await _commandHandler.HandleAsync(command, cancellationToken);
    }
    else
    {
        throw new NotAssignedToThisWorkloadException();
    }
}

Sales.Application/Commands/CrossCuttings/WorkloadManagementEnforcementCommandHandlerDecorator.cs:27-60, GroupFlights at commit a19b337, and it is the entire method.

var enabled = false; is a local, assigned a literal, never reassigned. The if is a tautology. Everything from line 38 down is unreachable — the user-context read, the command switch, the CanAccessWorkload call, the NotAssignedToThisWorkloadException. The decorator forwards every command to its inner handler and does nothing else.

The comment above it tells you what was meant: we could check the appsettings.json settings here to enable/disable this feature on demand. That is a note-to-self about where a configuration read would go, and it is entirely reasonable as a note. What is not reasonable is that the note is load-bearing.

What is switched off is not a feature

Here is why this line gets a whole article rather than a bullet in a code-review checklist. What it disables is not a nice-to-have; it is a context relationship from the map.

ADR 03 and its diagram put Workload Management among Sales' Open Host Service collaborators — one of four supporting contexts Sales calls out to, drawn with an OHS + PL connector. Part 4 derived the edge from the project graph and found it present: Sales.Application references WorkloadManagement.Shared, and it references it for this one purpose. The IWorkloadManagementApi interface has exactly one method, CanAccessWorkload, and the estate calls it from exactly two places.

So the derived context map and the drawn context map agree on this edge — and both are wrong, because the code behind it never executes. An edge on your architecture diagram can be present in the build, present in the dependency graph, and dead at runtime, and none of your three sources of truth will tell you. A project-reference analysis says the dependency exists. A context map says the relationship exists. Only reading the method body says otherwise.

The wiring around it makes the effect wider than one class. Commands/CrossCuttings/Extensions.cs:13-20 decorates three Sales command handlers through Scrutor:

services.TryDecorate<ICommandHandler<AddOfferVariantCommand>,
    WorkloadManagementEnforcementCommandHandlerDecorator<AddOfferVariantCommand>>();

services.TryDecorate<ICommandHandler<ConfirmOfferVariantCommand>,
    WorkloadManagementEnforcementCommandHandlerDecorator<ConfirmOfferVariantCommand>>();

services.TryDecorate<ICommandHandler<RevealOfferCommand>,
    WorkloadManagementEnforcementCommandHandlerDecorator<RevealOfferCommand>>();

And the host wires it in with a single call at Api/Extensions.cs:21, services.EnforceWorkloadManagementRulesInSales(). A composition root that reads as though it enforces something. Three of Sales' nine endpoints — add a variant, confirm a variant, reveal an offer to the client — go through a decorator whose name is WorkloadManagementEnforcement and which enforces nothing.

The asymmetry that makes it a finding rather than a stub

If this were the only implementation of the rule, I would file it under declared omission and move on. It is not.

Inquiries.Core/Services/InquiryService.cs:76-86 enforces the identical policy with no flag anywhere:

private async Task EnsureCanWorkOnThisInquiry(InquiryId inquiryId, CancellationToken cancellationToken = default)
{
    var isAssigned = await _workloadManagementApi.CanAccessWorkload(
        new (InquiryWorkloadType, inquiryId.Value.ToString(), _userContextAccessor.Get().CashierId),
        cancellationToken);

    if (isAssigned is false)
    {
        throw new NotAssignedToThisWorkloadException();
    }
}

Called at the top of both AcceptInquiry and RejectInquiry. Same API, same exception type from Shared.Types, same semantics: a cashier may only act on work assigned to them. One module enforces it and the other does not, and nothing anywhere records that this is a difference. No README bullet, no ADR, no comment beyond the note about appsettings.

That asymmetry is what converts a stub into a finding. It also tells you something about how the code was written: the Inquiries version is the straightforward one — a private method called explicitly at the top of two public methods — and the Sales version is the sophisticated one, a generic decorator applied through Scrutor from the composition root. The sophisticated version is the one that does not work. That is not a coincidence; the more indirection sits between a policy and its call site, the easier it is for the policy to stop happening without anyone noticing.

To be fair to the authors, there is a real reason the two look different. Inquiries is a CRUD module with a service class, so an explicit guard is idiomatic there. Sales is Clean Architecture with command handlers behind an ICommandHandler<T> dispatcher, and a cross-cutting decorator is the idiomatic answer there — it keeps authorisation out of the handler and lets the composition root decide policy. The design instinct is right. And a teaching repository has a genuine motive for leaving the decorator inert: the README's fourteen-step walkthrough would fail at step 3 if the reader had not first assigned themselves the offer workload, and the walkthrough only assigns the inquiry workload at step 2. Switching this on would break the demo.

I would accept every word of that defence if it appeared anywhere in the repository. The README declares nine omissions in careful detail — no authentication, no broker, no email gateway, no payment integration, sample tests only. A tenth bullet saying “workload enforcement in Sales is disabled so the walkthrough runs end to end” would have cost one line and converted a defect into a decision. That is this series' governing rule from part 1, and here is its cleanest instance: the declared omissions are all defensible; the undeclared one reads as a bug.

Three ways to switch a feature off, ranked

The estate's own comment names the better option, so let me finish the thought.

Worst: a local constant. What is here. It compiles, it produces no warning in a default build, and it is invisible to every tool. Search your codebase for = false; on a local named enabled, disabled, useNew or isOn and see what you find; I have never run that search on a mature codebase and come back empty.

Better: don't register the decorator. Delete the TryDecorate calls, or guard the whole EnforceWorkloadManagementRulesInSales() call in the composition root. The class stays intact and testable, the policy is off in one obvious place, and the composition root — the file whose entire job is to say what this application is — stops lying. This is a two-line change and it is what I would do here.

Best: a real switch, read once. What the comment intends: an option bound from configuration, injected into the decorator, and — importantly — logged at startup. A feature flag that nobody can see the state of is barely better than a constant. _logger.LogInformation("Workload enforcement in Sales: {State}", options.Enabled ? "on" : "off") costs one line and means the answer to “is it on in production?” is in the first hundred lines of the log rather than in a git blame.

There is a fourth option that is worse than all three and worth naming because it is the most common: leaving the flag as a constant and adding a comment saying it is temporary. The comment here does not do that — it describes an intention, not a promise — but the pattern it is one step away from is the single most durable form of technical debt I know.

A dependency you can see in the build and cannot see at runtime is the worst kind, because every tool you own will report it as healthy.

Next, the artefacts that recorded all of these decisions, read as software in their own right: decisions with an approver and no date.