Engineering, architecture, and performance — straight from the builders
home/ blog/ article
Feature Flags · 5 min read

Feature Flags and Multi-Tenancy: How I Test in Production Without Breaking Customers

How I use feature flags to ship unfinished code to production without giant PRs, and how a multi-tenant architecture lets me enable features for a single test tenant — testing in production without affecting other customers.

TT
TryTechSoftware Development

The problem: giant PRs and branches that rot

Anyone who has ever worked on a large feature knows the feeling. You open a branch, start implementing, and three weeks later you have a PR with 4,000 lines changed across 60 files. Nobody reviews that carefully. The branch has diverged so far from main that merging turns into hours of conflict resolution.

Worse still: while the feature isn’t ready, it lives isolated on a branch. It doesn’t integrate with the rest of the system, doesn’t run in the production environment, doesn’t receive real data. You only find out you broke something on merge day — the worst possible day to find out.

The root of the problem is a common confusion: deploying code and releasing a feature are different things. We treat them as if they were the same, and that’s where the pain lives.


The idea: decouple deploy from release

Feature flags (or feature toggles) solve exactly this. The idea is simple: you wrap the new code in a conditional that checks a flag. If the flag is off, the new code doesn’t run — even though it’s in production.

if (features.isEnabled("new-checkout")) {
  return renderNewCheckout(cart);
}
return renderOldCheckout(cart);

With this, I can ship unfinished code to main and to production with the flag turned off. The code is there, deployed, living alongside the rest of the system — but invisible to users. This changes everything:

  • Small, frequent PRs. Instead of one 4,000-line PR, I make ten 400-line PRs over two weeks. Each gets a real review, merges fast, and never diverges much from main.
  • Actual continuous integration. The new code is already integrated with the system from the first commit. There’s no traumatic “merge day.”
  • Instant kill switch. If something goes wrong with a live feature, I turn off the flag. No rollback, no redeploy, nothing. One toggle and it’s back to the previous state.

Where the flag lives

The implementation can be as simple or as sophisticated as you want. Early on, a flag can be just an environment variable or a row in a config table:

type FeatureContext = {
  tenantId: string;
  userId?: string;
};

class FeatureFlags {
  async isEnabled(flag: string, ctx: FeatureContext): Promise<boolean> {
    const config = await this.loadFlag(flag);
    if (!config) return false;

    // Globally on
    if (config.enabledGlobally) return true;

    // On for specific tenants
    if (config.enabledTenants?.includes(ctx.tenantId)) return true;

    return false;
  }
}

The important point is that the decision is made at runtime, checking a state I control without needing to deploy. Turning a flag on or off is a data operation, not a code operation.


The multi-tenant superpower: really testing in production

Here comes the part that, for my case, is the most valuable. My system is heavily multi-tenant — several customers share the same application and the same infrastructure, logically isolated by a tenantId.

This means the feature flag doesn’t have to be a global on/off switch. It can be segmented per tenant. And that’s exactly what lets me do something that sounds dangerous but is actually very safe: test in production.

I keep a test tenant — a fictional customer that lives in the same database, the same application, the same servers as the real customers. When I finish a feature, I enable the flag only for that tenant:

// Flag on only for the test tenant
{
  flag: "new-checkout",
  enabledGlobally: false,
  enabledTenants: ["tenant-internal-qa"]
}

Now the feature runs in production — real infrastructure, real data in the real format, real latency, real integrations with payment gateways and external services. But it’s only visible to the test tenant. The other customers keep seeing exactly what they saw before, unaware that new code is running right next to them.

graph TB
  subgraph prod["Production — same application, same database"]
    F{{"feature flag: new-checkout"}}
    T1["Tenant A (real customer)"]
    T2["Tenant B (real customer)"]
    T3["Tenant C (real customer)"]
    QA["Tenant QA (test)"]
  end

  F -->|"off"| T1
  F -->|"off"| T2
  F -->|"off"| T3
  F -->|"on"| QA

When I gain confidence that the feature is solid, I do a gradual rollout: enable it for a pilot customer who agreed to test, then for 10% of tenants, then for everyone. If a problem shows up along the way, I turn the flag off for that segment and investigate — without affecting those who were already happy.


Why this beats a traditional staging environment

A staging environment tries to mimic production, but never quite gets there. The data volume is smaller, external integrations are usually in sandbox mode, the configuration drifts over time. Bugs that only appear with real data and real scale slip through unnoticed.

Testing in production with a test tenant eliminates that gap. There’s no “imitation”: it’s the real environment, with a blast radius that’s controlled by design. The per-tenant isolation the application already guarantees to separate customers becomes the security boundary of my tests too.

Of course, this requires discipline. Some rules I follow:

  • The test tenant never triggers real side effects for third parties — emails, actual charges, webhooks to someone else’s production. I use sandbox accounts on external services for that tenant.
  • Every flag has an owner and an expiration date. A forgotten feature flag becomes technical debt. Once the feature is 100% released, I remove the flag and the dead code from the old path.
  • The flag is observable. Logs and metrics record which code path ran, so I know what happened when something goes wrong.

The result

Trading “long branch + giant PR + merge day” for “small PRs + code behind a flag + testing on a real tenant” completely changed my rhythm. I ship faster, with less fear, and with a reversal mechanism that costs one click instead of a redeploy.

Feature flags aren’t magic — they move part of the complexity from the branching process into the application runtime. But when the system is already multi-tenant, you get the most valuable piece for free: a safe place inside production itself to see new code really working, before any real customer touches it.