
Beyond the Feature Flag: When Your Logic Becomes Infrastructure
We often start with simple feature flags. But what happens when that 'simple' pricing logic or rule engine grows, becoming intertwined with every aspect of your application? Treating core business logic as a mere feature leads to brittle systems and endless headaches. It's time to elevate it to infr
by Sunil Band
Beyond the Feature Flag
Every team, at some point, ships a piece of logic that starts small, maybe as a simple if statement or a database lookup. It's a new pricing tier, a specific discount rule, or a content personalization toggle. "Just a feature," we say. We might even throw a feature flag around it to control rollout. Fast forward a few months, and that one-off logic has sprouted branches, dependencies, and complex interactions. Suddenly, what felt like a feature is now a sprawling, critical system, deeply embedded in your application's core, and nobody quite knows how to change it safely.
This isn't just about code complexity; it's about how we perceive and manage fundamental business rules. When pricing, authorization, or content delivery rules become dynamic and central to your product, treating them as ephemeral features is a recipe for disaster. They demand the same rigor, versioning, testing, and operational discipline as your databases or message queues. They've become infrastructure.
The Evolution of Business Logic
Think about a pricing engine. It might start as a simple calculation: price = basePrice * (1 - discountPercentage). Easy. Then marketing wants a regional price adjustment. Then a segment-specific discount. Then a time-limited promotion. Suddenly, you have a dozen if/else blocks, database tables, and perhaps even some remote service calls. This logic is now affecting revenue, user experience, and legal compliance. It's not just a UI toggle anymore; it's the beating heart of your business.
The same applies to authorization rules, content recommendation algorithms, or even how you route requests. These aren't just one-off functions; they are systems that define your product's behavior. When they are scattered across your codebase, hardcoded, or tied directly to your deployment cycle, you introduce unnecessary risk and slowdowns. Every change becomes a full deployment, every bug a potential outage for a core business function.
Why Features Become Infrastructure
It boils down to mutability and impact. A UI component's color might be a feature. Changing it has limited impact. But altering how you calculate a user's subscription cost or what data they're allowed to see? That's high-stakes. These core rules need to be:
- Auditable: Who changed what, when, and why?
- Versioned: Can we roll back to a previous set of rules if something goes wrong?
- Testable Independently: Can we validate new rules without deploying the entire application?
- Performant: Can they execute quickly at scale?
- Decoupled: Can they evolve without forcing a redeployment of dependent services?
If you can't answer yes to most of these, your "feature" is likely already infrastructure in disguise, and you're treating it with the wrong level of care.
A Concrete Example: Dynamic Discount Rules
Let's say we have a product page that needs to display the correct price, including any active discounts. A naive approach might look like this:
// products.ts - a typical backend service
interface Product {
id: string;
basePrice: number;
name: string;
}
interface User {
id: string;
isPremium: boolean;
region: 'US' | 'EU' | 'ASIA';
}
const products: Product[] = [
{ id: 'prod-1', basePrice: 100, name: 'Fancy Gadget' },
// ...
];
// This function lives directly in your product service logic
function calculatePrice(product: Product, user: User): number {
let finalPrice = product.basePrice;
// Hardcoded logic for premium users
if (user.isPremium) {
finalPrice *= 0.9; // 10% off for premium
}
// Hardcoded regional discount
if (user.region === 'EU') {
finalPrice *= 0.95; // 5% off for EU
}
// Holiday sale logic (might be behind a feature flag)
const isHolidaySaleActive = process.env.HOLIDAY_SALE_ACTIVE === 'true';
if (isHolidaySaleActive && product.id === 'prod-1') {
finalPrice *= 0.8; // 20% off for specific product during holiday
}
return finalPrice;
}
// Imagine calling this in an API endpoint:
// const price = calculatePrice(getProduct(req.params.id), getUser(req.user.id));This is fine for a start. But what if the holiday sale ends? You redeploy. What if the premium discount changes? You redeploy. What if you need to add a new discount type, like "buy one get one free"? That's a new code path, new tests, and another deployment. This calculatePrice function is tightly coupled to your application's deployment lifecycle.
Elevating Logic to Infrastructure
Instead, let's treat discount rules as versioned, dynamically loadable infrastructure. We can achieve this with a simple rule engine or decision service. The core idea is to separate the definition of the rules from their execution.
One common pattern is to define rules as data, often JSON or a domain-specific language (DSL), and have a dedicated service or module evaluate them. For example, using a simple JSON-based rule engine:
// rule-engine-types.ts
export type RuleCondition = {
field: string;
operator: 'eq' | 'gt' | 'lt' | 'in';
value: any;
};
export type RuleAction = {
type: 'apply_discount';
percentage: number;
};
export type Rule = {
id: string;
conditions: RuleCondition[];
action: RuleAction;
priority: number; // Higher priority rules run first
};
// rule-engine.ts
import { Rule, RuleCondition, RuleAction } from './rule-engine-types';
export class RuleEngine {
private rules: Rule[] = [];
constructor(rules: Rule[]) {
// Sort rules by priority for consistent application
this.rules = [...rules].sort((a, b) => b.priority - a.priority);
}
evaluate(context: Record<string, any>): RuleAction[] {
const applicableActions: RuleAction[] = [];
for (const rule of this.rules) {
const conditionsMet = rule.conditions.every(condition => {
const contextValue = context[condition.field];
switch (condition.operator) {
case 'eq': return contextValue === condition.value;
case 'gt': return contextValue > condition.value;
case 'lt': return contextValue < condition.value;
case 'in': return Array.isArray(condition.value) && condition.value.includes(contextValue);
default: return false;
}
});
if (conditionsMet) {
applicableActions.push(rule.action);
// Depending on your requirements, you might stop after the first match
// or collect all applicable actions.
}
}
return applicableActions;
}
}
// Now, in your product service or API:
// product-service.ts
import { RuleEngine, Rule } from './rule-engine';
// Rules loaded from a configuration service, database, or a versioned file
const activeDiscountRules: Rule[] = [
{
id: 'premium-discount',
conditions: [{ field: 'isPremiumUser', operator: 'eq', value: true }],
action: { type: 'apply_discount', percentage: 10 },
priority: 100
},
{
id: 'eu-region-discount',
conditions: [{ field: 'userRegion', operator: 'eq', value: 'EU' }],
action: { type: 'apply_discount', percentage: 5 },
priority: 90
},
{
id: 'holiday-prod1-discount',
conditions: [
{ field: 'isHolidaySaleActive', operator: 'eq', value: true },
{ field: 'productId', operator: 'eq', value: 'prod-1' }
],
action: { type: 'apply_discount', percentage: 20 },
priority: 110
}
];
const discountRuleEngine = new RuleEngine(activeDiscountRules);
interface ProductPriceContext {
productId: string;
basePrice: number;
isPremiumUser: boolean;
userRegion: 'US' | 'EU' | 'ASIA';
isHolidaySaleActive: boolean; // This could come from a feature flag service
}
function calculateFinalPrice(context: ProductPriceContext): number {
let finalPrice = context.basePrice;
const actions = discountRuleEngine.evaluate(context);
for (const action of actions) {
if (action.type === 'apply_discount') {
finalPrice *= (1 - action.percentage / 100);
}
}
return finalPrice;
}
// Example usage:
const userContext = {
productId: 'prod-1',
basePrice: 100,
isPremiumUser: true,
userRegion: 'EU',
isHolidaySaleActive: true
};
const finalPrice = calculateFinalPrice(userContext);
console.log(`Final price: $${finalPrice.toFixed(2)}`); // Expected: $68.40 (100 * 0.9 * 0.95 * 0.8)Now, adding a new discount rule, changing an existing one, or even deactivating a sale is a matter of updating the activeDiscountRules array (which would typically come from a database, a specialized rule management system, or a Git-versioned configuration file) and, potentially, refreshing the RuleEngine instance. No code changes, no redeployment of your core application.
This decoupling means:
- Faster iterations: Business stakeholders can define and test rules more directly.
- Reduced risk: Changes to rules are less likely to break unrelated parts of the application.
- Better auditability: Rule changes can be tracked independently of code deployments.
- Scalability: The rule engine itself can be scaled independently or even run as a serverless function.
Trade-offs and Considerations
Of course, nothing comes for free. Building a robust rule engine is more complex than a few if statements. You'll need to consider:
- Rule definition: How will non-developers define and manage these rules? A good UI for rule management can be crucial.
- Complexity of conditions/actions: My example is simple. Real-world scenarios often require more complex operators (regex, date comparisons) and actions (apply a fixed amount, grant a free item).
- Performance: For very high-throughput systems, the overhead of interpreting rules at runtime might be a concern, although for most applications, it's negligible.
- Statefulness: If rules depend on external state that changes frequently, how do you keep the context up-to-date?
- Testing: You need a solid testing strategy for your rules, preferably independent of your application tests.
Frameworks like JSON Logic or more robust Business Rule Management Systems (BRMS) exist to help with this. The point isn't to build your own BRMS from scratch unless it's your core business. The point is to recognize when your internal logic has grown to become that kind of system.
Wrapping up
The next time you find yourself adding another if block for a business rule, or a new entry to a configuration file that dictates core behavior, pause. Ask yourself: Is this truly just a feature, or is it evolving into infrastructure? If it's the latter, start abstracting it. Think about how you would manage this rule if your application had to scale to thousands of daily changes without a single redeployment. Explore existing rule engines or, for simpler cases, define a declarative data structure that your code can interpret. Your future self, and your business stakeholders, will thank you for treating your logic with the architectural respect it deserves.
For a starting point, try implementing a simple rule engine similar to the one above, but instead of hardcoding activeDiscountRules, load them from a JSON file. Then, create a small UI that allows you to edit that JSON and see the price change dynamically without restarting your server. This will give you a taste of what true decoupled logic feels like.

When Your UI Needs to Break the Flat Screen: React and 3D with React Three Fiber
We've been building UIs on flat screens for decades. But what happens when you need something more? When data visualization demands depth, or an interactive product showcase needs a real sense of presence? React Three Fiber is the tool that lets you bring the full power of Three.js into your React a

When Your State Management Needs to Stop Thinking in Actions and Start Mutating
Many state management libraries force you into an 'actions and reducers' pattern. While powerful, it often adds unnecessary boilerplate for simple updates. Sometimes, you just need to directly modify state, and mutators offer a more ergonomic and intuitive approach, especially for deeply nested data

When Next.js Cache Components Refuse to Build Your App
Next.js 16.3 introduced 'Cache Components' to optimize server-side rendering, but getting them to work can be a headache. I spent a frustrating afternoon debugging why a simple page wouldn't build, only to uncover some subtle yet critical design considerations. It turns out, this feature forces you


















