When your core business logic becomes a high-risk bottleneck, every deployment feels like defusing a bomb. Here's how we used the Strategy Pattern to transform our most critical code path from a deployment risk into a configuration switch.
The Problem: When a Core Business Rule is a High-Risk Bottleneck
We had a central piece of logic in our product publishing service that decided how products should be published, standalone items or grouped variants. This decision was critical and complex, driven by product categories.
// The problem area: A giant switch statement inside the main publishing service
func processProduct(product *model.Product) {
if product.Category.IsTypeA { // e.g., Fashion
// ... hundreds of lines of complex Type A-specific logic
} else if product.Category.IsTypeB { // e.g., Electronics
// ... dozens of lines of simpler Type B logic
}
// ... and so on, with every new requirement adding risk
}
The real issue wasn't messy code, it was risk and agility:
- High-risk modifications: Changing this central logic always risked breaking other categories
- No isolation: We couldn't develop or test new logic independently
- Slow rollbacks: Reverting a bad change meant redeploying the entire service
Our roadmap required migrating all products to a grouped model eventually. We needed a way to swap our publishing logic safely, test it in isolation, and roll back instantly if needed.
Sound impossible? Enter the Strategy Pattern.
The "Aha!" Moment
The breakthrough came when we realized: we don't need to change the decision-making logic, we need to swap out the decision-maker entirely.
Think of it like a chess game. Instead of rewriting the rules mid-game, we swap the entire chess AI. Same board, same pieces, different brain making the moves.
In practice: Instead of modifying a 500 line if-else block to support a new publishing rule, we write a new 50 line strategy class. The service code? Untouched.
The pattern works like this:
Service → Interface.Method()
↓
┌─────┴─────┐
↓ ↓
Strategy A Strategy B
(Current) (Future)
Your service calls the interface. The interface delegates to whichever strategy is active. The service never knows the difference.
Step 1: Defining the Contract (The Interface)
package service
import (
model "app/model"
)
// CreationDecision defines the strategy contract for determining publishing modes.
// It is completely agnostic to the specific business rule being run.
type CreationDecision interface {
ShouldCreateAbstract(category *model.Category) bool
DetermineCreationMode(product *model.Product) string
}
This is the magic: The interface doesn't know about Fashion, Electronics, or your business rules. It only knows the questions that need answers.
Step 2: Encapsulate Current Logic (Strategy A)
We took all that scary if-else logic and wrapped it in a neat package:
// CategoryBasedDecision: The existing, production-safe strategy.
// This preserves the current selective publishing rules (e.g., Type A products only).
type CategoryBasedDecision struct{}
func (f CategoryBasedDecision) ShouldCreateAbstract(category *model.Category) bool {
// SANITIZED LOGIC: Returns true only for products with specific flag values.
return category.HasSpecificFlag()
}
func (f CategoryBasedDecision) DetermineCreationMode(product *model.Product) string {
// SANITIZED LOGIC: Internal business rules for existing product groups.
if !f.ShouldCreateAbstract(product.Category) {
return "CONCRETE_ONLY" // Non-flagged products remain standalone.
}
// Complex check to see if we ASSIGN to an existing group or CREATE a new one.
if product.ProductGroup.IsPopulated() {
return "ASSIGN_EXISTING"
}
return "CREATE_BOTH"
}
Key insight: We didn't change a single business rule. We just moved the code into a strategy type. The behavior is identical to what was there before
"Note: The internal business logic is sanitized for this article"
Step 3: Build the Future (Strategy B)
Now here's where it gets interesting. Our roadmap required moving all products to the grouped model eventually. With the old if-else, this would be a terrifying rewrite. With strategies? We just create a second implementation:
// AlwaysAbstractDecision: The new, future-state strategy.
// This strategy enforces group creation for every product type.
type AlwaysAbstractDecision struct{}
func (a AlwaysAbstractDecision) ShouldCreateAbstract(category *model.Category) bool {
return true // THE KEY CHANGE: Always return true, overriding the selective rule.
}
func (a AlwaysAbstractDecision) DetermineCreationMode(product *model.Product) string {
// This logic is now optimized for a world where group creation is mandatory.
// The details are, again, proprietary.
if product.ProductGroup.IsPopulated() {
return "ASSIGN_EXISTING"
}
return "CREATE_BOTH"
}
Notice what happened: zero changes to the interface, zero changes to the calling code. We just implemented the same contract with different behavior.
Step 4: The Service (Stays Blissfully Simple)
Here's the beautiful part. Your main service code becomes trivial:
type ProductPublisher struct {
strategy CreationDecision // Injected via DI or factory
}
func (p *ProductPublisher) Publish(product *model.Product) error {
// Step 1: Ask the strategy what to do
mode := p.strategy.DetermineCreationMode(product)
// Step 2: Execute based on the answer
switch mode {
case "CONCRETE_ONLY":
return p.createStandalone(product)
case "ASSIGN_EXISTING":
return p.assignToGroup(product)
case "CREATE_BOTH":
return p.createGroupAndProduct(product)
}
return fmt.Errorf("unknown mode: %s", mode)
}
This code never changes. Not when you add Strategy C. Not when you modify Strategy A. Not when you're testing Strategy B in production.
The Deployment Advantage
Here's how the Strategy Pattern changed our deployment story:
| Scenario | Strategy | What Changed | Risk Level |
|---|---|---|---|
| Current Production | CategoryBasedDecision |
Nothing (baseline) | ✅ Low |
| Testing Future State | AlwaysAbstractDecision |
Config only, no code deploy | ⚠️ Controlled |
| Rollback | CategoryBasedDecision |
Revert config in seconds | ✅ Extremely Low |
The key insight: swapping strategies is a configuration change, not a code deployment. No recompilation, no merge conflicts, no complex rollback procedures.
Real-World Impact
📉 Before Strategy Pattern:
- 2-3 week deployment cycles due to testing complexity
- Hours long rollbacks requiring full service redeployment
- Testing in isolation was nearly impossible
- Each new requirement added to everyone's cognitive load
📈 After Strategy Pattern:
- Daily deployments via configuration changes
- Sub-minute rollbacks (revert a config value)
- Each strategy tested independently
- New strategies developed without touching existing code
Getting Started in Your Codebase
If you're dealing with a similar situation, here's the refactoring path:
- Identify the algorithm - Find the if-else or switch statement that keeps growing
- Extract the interface - What questions does your code need answered?
- Wrap existing logic - Create Strategy A that preserves current behavior exactly
- Add tests - Prove Strategy A produces identical results
- Build Strategy B - Implement your new behavior
- Add a factory - Let configuration decide which strategy to use
The beauty? You can do steps 1-4 without changing any behavior. It's a safe refactor.
When Should You Use This?
The Strategy Pattern shines when:
You have multiple algorithms for the same problem (e.g., different pricing rules, recommendation engines, publishing modes)
The algorithm needs to change at runtime (via config, feature flags, A/B tests)
The algorithm is complex and high-risk (the if-else that everyone fears)
You need instant rollback capability (because 2 AM deployments happen)
The Bottom Line
We transformed our most critical code path from a deployment risk into a configuration switch. The Strategy Pattern gave us the confidence to experiment, the safety to rollback instantly, and the architecture to scale.
Your core business logic is too important to be trapped in an if-else statement. Set it free.
Dealing with a similar "untouchable" code path? I'd love to hear your approach in the comments below.
Top comments (0)