Strategy Pattern #

An OrderService starts its life simple: one shipping method, one pricing algorithm. Six months later, there are three shipping methods with different cost rules, five discount tiers based on membership, and two tax algorithms for domestic and international customers. The ProcessOrder code that used to be twenty lines is now eighty lines of nested if-else. Every time a new shipping method or discount tier is added, the developer has to go inside ProcessOrder, understand the existing conditions, and insert a new block in the right place — while praying no condition gets missed. The Strategy Pattern cuts this cycle. Each algorithm is moved into a separate struct implementing the same interface, and OrderService only calls that interface — without needing to know how many strategies exist or what differentiates them.

What Is the Strategy Pattern? #

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, wraps each one in a separate struct, and makes them interchangeable without changing the code that uses them. The object using the algorithm (called the Context) delegates execution to the Strategy object — it does not know or care which algorithm is running.

Two key properties distinguish Strategy from merely “having many functions”:

  • Interchangeable — strategies can be swapped at runtime, not just at compile time
  • Isolated — each strategy stands alone; changing one strategy does not affect the others

The Strategy Pattern is the cleanest answer to the question “how do I eliminate nested switch / if-else blocks that keep growing?” — and that question appears in nearly every evolving codebase.

flowchart LR
    subgraph "Without Strategy"
        C1[OrderService] -->|"if shipping == JNE"| A1["JNE logic"]
        C1 -->|"else if shipping == JT"| A2["J&T logic"]
        C1 -->|"else if shipping == GoSend"| A3["GoSend logic"]
        C1 -.->|"add Anteraja?\\nmodify OrderService"| A4["???"]
    end

    subgraph "With Strategy"
        C2[OrderService] -->|"strategy.Calculate()"| SI[ShippingStrategy\\ninterface]
        SI --> B1[JNEStrategy]
        SI --> B2[JTStrategy]
        SI --> B3[GoSendStrategy]
        SI --> B4["AnterajaStrategy\\nadded without\\nchanging OrderService"]
    end

The Problem It Solves #

The Strategy Pattern solves one very specific problem: algorithms that vary and keep growing, embedded inside code that should not need to know the algorithm details.

The Problem: Runaway if-else #

// ANTI-PATTERN: all shipping algorithms embedded in OrderService
func (s *OrderService) CalculateShipping(order Order, method string) (int, error) {
    switch method {
    case "jne":
        // JNE logic: weight × rate per kg + handling fee + zone
        baseRate := 15000
        weightRate := int(math.Ceil(order.WeightKg)) * 8000
        zoneMultiplier := getJNEZoneMultiplier(order.DestCity)
        return int(float64(baseRate+weightRate) * zoneMultiplier), nil

    case "jt":
        // J&T logic: different from JNE, has a weekend discount
        baseRate := 12000
        weightRate := int(math.Ceil(order.WeightKg)) * 7500
        if time.Now().Weekday() == time.Saturday || time.Now().Weekday() == time.Sunday {
            baseRate = int(float64(baseRate) * 0.9)
        }
        return baseRate + weightRate, nil

    case "gosend":
        // GoSend logic: per km, max 30km
        distance := calculateDistance(order.OriginCity, order.DestCity)
        if distance > 30 {
            return 0, fmt.Errorf("GoSend only serves distances up to 30km")
        }
        return int(distance * 3500), nil

    // Every new courier adds a case here — OrderService keeps bloating
    default:
        return 0, fmt.Errorf("unknown shipping method: %s", method)
    }
}

// CORRECT: OrderService only knows the interface, not the implementation
func (s *OrderService) CalculateShipping(order Order, strategy ShippingStrategy) (int, error) {
    return strategy.Calculate(order)
}
// Adding a new courier = creating a new struct implementing ShippingStrategy
// OrderService does not change at all

The Problem: Difficult Testing #

Without the Strategy Pattern, to test the JNE logic you have to initialize the entire OrderService with all its dependencies. With the Strategy Pattern, you only need to test JNEStrategy in isolation — no OrderService, no database, no other dependencies.


Three Components of the Strategy Pattern #

classDiagram
    class ShippingStrategy {
        <<interface>>
        +Calculate(order Order) (int, error)
        +Name() string
        +EstimatedDays(destCity string) int
    }

    class JNEStrategy {
        -baseRate int
        -zoneRates map[string]float64
        +Calculate(order Order) (int, error)
        +Name() string
        +EstimatedDays(destCity string) int
    }

    class JTStrategy {
        -baseRate int
        -weekendDiscount float64
        +Calculate(order Order) (int, error)
        +Name() string
        +EstimatedDays(destCity string) int
    }

    class GoSendStrategy {
        -maxDistanceKm float64
        -ratePerKm int
        +Calculate(order Order) (int, error)
        +Name() string
        +EstimatedDays(destCity string) int
    }

    class OrderService {
        -shippingStrategy ShippingStrategy
        +SetShipping(strategy ShippingStrategy)
        +ProcessOrder(order Order) error
    }

    ShippingStrategy <|.. JNEStrategy
    ShippingStrategy <|.. JTStrategy
    ShippingStrategy <|.. GoSendStrategy
    OrderService o-- ShippingStrategy : delegates to
ComponentRoleIn Go
Strategy interfaceThe algorithm contract every strategy must satisfyInterface with a single method or several cohesive methods
Concrete StrategyA specific algorithm implementationStruct that implements the interface
ContextUses the strategy without knowing its implementationStruct holding a field of the Strategy interface type

Full Implementation: Shipping Calculator #

A more realistic case study than the original example — a shipping cost system that considers weight, distance, and delivery time.

Strategy Interface #

package shipping

import "time"

// Order contains the order information needed for shipping cost calculation.
type Order struct {
    ID         string
    WeightKg   float64
    OriginCity string
    DestCity   string
    OrderedAt  time.Time
    IsCOD      bool
}

// ShippingResult contains the shipping cost calculation result.
type ShippingResult struct {
    Cost          int
    EstimatedDays int
    ServiceName   string
    Notes         string
}

// ShippingStrategy is the interface for all shipping cost algorithms.
// Each courier implements this interface in its own way.
type ShippingStrategy interface {
    // Calculate computes the total shipping cost for the given order.
    Calculate(order Order) (*ShippingResult, error)

    // Name returns the service name to display to the user.
    Name() string

    // Supports checks whether this strategy serves the given route.
    Supports(originCity, destCity string) bool
}

Concrete Strategies #

package shipping

import (
    "fmt"
    "math"
    "time"
)

// JNEStrategy implements JNE shipping cost calculation.
// Weight-based rates with an inter-city zone multiplier.
type JNEStrategy struct {
    BaseRate    int
    RatePerKg   int
    ZoneRates   map[string]float64 // "JAKARTA-SURABAYA" -> multiplier
    HandlingFee int
}

func NewJNEStrategy() *JNEStrategy {
    return &JNEStrategy{
        BaseRate:    15000,
        RatePerKg:   8000,
        HandlingFee: 2000,
        ZoneRates: map[string]float64{
            "JAKARTA-SURABAYA": 1.0,
            "JAKARTA-MEDAN":    1.5,
            "JAKARTA-MAKASSAR": 1.8,
            "JAKARTA-BALI":     1.2,
        },
    }
}

func (s *JNEStrategy) Calculate(order Order) (*ShippingResult, error) {
    if order.WeightKg <= 0 {
        return nil, fmt.Errorf("package weight must be greater than 0 kg")
    }

    // Round up per kg
    billableWeight := int(math.Ceil(order.WeightKg))
    baseCost := s.BaseRate + (billableWeight * s.RatePerKg) + s.HandlingFee

    // Apply the zone multiplier if one exists
    zoneKey := fmt.Sprintf("%s-%s", order.OriginCity, order.DestCity)
    multiplier := 1.0
    if m, ok := s.ZoneRates[zoneKey]; ok {
        multiplier = m
    }

    finalCost := int(float64(baseCost) * multiplier)

    // Additional 2% COD fee
    if order.IsCOD {
        finalCost += int(float64(finalCost) * 0.02)
    }

    return &ShippingResult{
        Cost:          finalCost,
        EstimatedDays: s.estimatedDays(order.DestCity),
        ServiceName:   s.Name(),
        Notes:         fmt.Sprintf("Billable weight: %d kg", billableWeight),
    }, nil
}

func (s *JNEStrategy) Name() string { return "JNE Regular" }

func (s *JNEStrategy) Supports(origin, dest string) bool {
    // JNE serves all of Indonesia
    return origin != "" && dest != ""
}

func (s *JNEStrategy) estimatedDays(destCity string) int {
    longDistanceCities := map[string]bool{
        "MEDAN": true, "MAKASSAR": true, "MANADO": true, "JAYAPURA": true,
    }
    if longDistanceCities[destCity] {
        return 5
    }
    return 3
}


// JTExpressStrategy implements J&T Express shipping cost calculation.
// Offers weekend discounts and slightly cheaper rates.
type JTExpressStrategy struct {
    BaseRate        int
    RatePerKg       int
    WeekendDiscount float64
}

func NewJTExpressStrategy() *JTExpressStrategy {
    return &JTExpressStrategy{
        BaseRate:        12000,
        RatePerKg:       7500,
        WeekendDiscount: 0.10, // 10% weekend discount
    }
}

func (s *JTExpressStrategy) Calculate(order Order) (*ShippingResult, error) {
    if order.WeightKg <= 0 {
        return nil, fmt.Errorf("package weight must be greater than 0 kg")
    }

    billableWeight := int(math.Ceil(order.WeightKg))
    cost := s.BaseRate + (billableWeight * s.RatePerKg)

    notes := fmt.Sprintf("Billable weight: %d kg", billableWeight)

    // Weekend discount
    day := order.OrderedAt.Weekday()
    if day == time.Saturday || day == time.Sunday {
        discount := int(float64(cost) * s.WeekendDiscount)
        cost -= discount
        notes += fmt.Sprintf(", weekend discount: Rp %d", discount)
    }

    return &ShippingResult{
        Cost:          cost,
        EstimatedDays: 2,
        ServiceName:   s.Name(),
        Notes:         notes,
    }, nil
}

func (s *JTExpressStrategy) Name() string { return "J&T Express" }

func (s *JTExpressStrategy) Supports(origin, dest string) bool {
    return origin != "" && dest != ""
}


// GoSendStrategy implements GoSend (same-day) shipping cost calculation.
// Only for short distances, per-km rates.
type GoSendStrategy struct {
    MaxDistanceKm float64
    RatePerKm     int
    MinimumFee    int
}

func NewGoSendStrategy() *GoSendStrategy {
    return &GoSendStrategy{
        MaxDistanceKm: 30,
        RatePerKm:     3500,
        MinimumFee:    15000,
    }
}

func (s *GoSendStrategy) Calculate(order Order) (*ShippingResult, error) {
    distance := calculateDistance(order.OriginCity, order.DestCity)

    if distance > s.MaxDistanceKm {
        return nil, fmt.Errorf("GoSend only serves distances up to %.0f km, your distance: %.1f km",
            s.MaxDistanceKm, distance)
    }

    cost := int(distance * float64(s.RatePerKm))
    if cost < s.MinimumFee {
        cost = s.MinimumFee
    }

    return &ShippingResult{
        Cost:          cost,
        EstimatedDays: 0, // same-day
        ServiceName:   s.Name(),
        Notes:         fmt.Sprintf("Distance: %.1f km, same-day delivery", distance),
    }, nil
}

func (s *GoSendStrategy) Name() string { return "GoSend Same-Day" }

func (s *GoSendStrategy) Supports(origin, dest string) bool {
    return calculateDistance(origin, dest) <= s.MaxDistanceKm
}

// calculateDistance is a helper — in a real implementation it uses the Google Maps API
func calculateDistance(origin, dest string) float64 {
    distances := map[string]float64{
        "JAKARTA-DEPOK":   20.0,
        "JAKARTA-BEKASI":  25.0,
        "JAKARTA-BOGOR":   60.0,
        "JAKARTA-BANDUNG": 150.0,
    }
    key := fmt.Sprintf("%s-%s", origin, dest)
    if d, ok := distances[key]; ok {
        return d
    }
    return 200.0 // default: out of range
}

Context: OrderService #

package order

import (
    "fmt"
    "myapp/shipping"
)

// OrderService is the Context — it uses ShippingStrategy without knowing the details.
type OrderService struct {
    shippingStrategy shipping.ShippingStrategy
    // other dependencies...
}

func NewOrderService(strategy shipping.ShippingStrategy) *OrderService {
    return &OrderService{shippingStrategy: strategy}
}

// SetShippingStrategy swaps the shipping strategy at runtime.
// This is the power of the Strategy Pattern — no need to create a new OrderService.
func (s *OrderService) SetShippingStrategy(strategy shipping.ShippingStrategy) {
    s.shippingStrategy = strategy
}

// ProcessOrder processes an order using the configured strategy.
func (s *OrderService) ProcessOrder(order shipping.Order) error {
    if s.shippingStrategy == nil {
        return fmt.Errorf("shipping strategy not configured")
    }

    // Check whether the strategy serves this route
    if !s.shippingStrategy.Supports(order.OriginCity, order.DestCity) {
        return fmt.Errorf("service %s is not available for route %s → %s",
            s.shippingStrategy.Name(), order.OriginCity, order.DestCity)
    }

    // Calculate shipping — does not know and does not care which algorithm is used
    result, err := s.shippingStrategy.Calculate(order)
    if err != nil {
        return fmt.Errorf("shipping cost calculation failed: %w", err)
    }

    fmt.Printf("Order %s processed:\n", order.ID)
    fmt.Printf("  Service: %s\n", result.ServiceName)
    fmt.Printf("  Shipping cost: Rp %d\n", result.Cost)
    fmt.Printf("  Estimated: %d days\n", result.EstimatedDays)
    fmt.Printf("  Notes: %s\n", result.Notes)

    return nil
}

// GetShippingOptions lists all available shipping options for a given route.
func (s *OrderService) GetShippingOptions(order shipping.Order, strategies []shipping.ShippingStrategy) []shipping.ShippingResult {
    var options []shipping.ShippingResult
    for _, strategy := range strategies {
        if !strategy.Supports(order.OriginCity, order.DestCity) {
            continue
        }
        result, err := strategy.Calculate(order)
        if err != nil {
            continue
        }
        options = append(options, *result)
    }
    return options
}

Usage with Swappable Strategies #

func main() {
    order := shipping.Order{
        ID:         "ORD-001",
        WeightKg:   2.3,
        OriginCity: "JAKARTA",
        DestCity:   "SURABAYA",
        OrderedAt:  time.Now(),
        IsCOD:      false,
    }

    // Start with JNE
    svc := order.NewOrderService(shipping.NewJNEStrategy())
    _ = svc.ProcessOrder(order)

    // Swap to J&T at runtime — no changes to OrderService code
    svc.SetShippingStrategy(shipping.NewJTExpressStrategy())
    _ = svc.ProcessOrder(order)

    // List all available options
    allStrategies := []shipping.ShippingStrategy{
        shipping.NewJNEStrategy(),
        shipping.NewJTExpressStrategy(),
        shipping.NewGoSendStrategy(),
    }
    options := svc.GetShippingOptions(order, allStrategies)
    fmt.Println("\nAll shipping options:")
    for _, opt := range options {
        fmt.Printf("  %s: Rp %d (%d days)\n", opt.ServiceName, opt.Cost, opt.EstimatedDays)
    }
}

Functional Strategy: The Go Idiom #

In Go, the Strategy Pattern can be implemented more concisely using functions as values (first-class functions). This fits simple, stateless strategies very well.

// PricingStrategy as a function type — more concise than an interface for simple cases
type PricingStrategy func(basePrice float64, user User) float64

// Concrete strategies as functions
var (
    // RegularPricing: normal price without discounts
    RegularPricing PricingStrategy = func(basePrice float64, user User) float64 {
        return basePrice
    }

    // MemberPricing: 10% discount for members
    MemberPricing PricingStrategy = func(basePrice float64, user User) float64 {
        return basePrice * 0.90
    }

    // PremiumPricing: 20% discount for premium members
    PremiumPricing PricingStrategy = func(basePrice float64, user User) float64 {
        return basePrice * 0.80
    }

    // FlashSalePricing: 30% discount, but only between 12.00-13.00
    FlashSalePricing PricingStrategy = func(basePrice float64, user User) float64 {
        hour := time.Now().Hour()
        if hour == 12 {
            return basePrice * 0.70
        }
        return basePrice
    }
)

// PricingContext uses the function type as its strategy
type PricingContext struct {
    strategy PricingStrategy
}

func NewPricingContext(strategy PricingStrategy) *PricingContext {
    return &PricingContext{strategy: strategy}
}

func (c *PricingContext) CalculatePrice(basePrice float64, user User) float64 {
    return c.strategy(basePrice, user)
}

// Selecting a strategy based on the user's tier
func SelectPricingStrategy(user User) PricingStrategy {
    switch user.MemberTier {
    case "premium":
        return PremiumPricing
    case "regular":
        return MemberPricing
    default:
        return RegularPricing
    }
}

// Usage
func ProcessPurchase(basePrice float64, user User) float64 {
    strategy := SelectPricingStrategy(user)
    ctx := NewPricingContext(strategy)
    return ctx.CalculatePrice(basePrice, user)
}

Comparing the struct-based and functional approaches:

AspectStruct-based StrategyFunctional Strategy
Best forStrategies with state and many methodsStateless strategies with a single operation
TestabilityVery easy — test the struct in isolationEasy — call the function directly
ComposabilityNeeds a wrapper structCan be combined with func(f) func
ReadabilityExplicit and self-documentingConcise, great for closures
Go idiomMore verboseMore idiomatic

Combining Strategy with Factory #

In real applications, Strategy and Factory work together: the Factory picks the right strategy based on input, the Strategy runs the algorithm.

// ShippingStrategyFactory selects a strategy based on user preference and availability.
type ShippingStrategyFactory struct {
    strategies map[string]shipping.ShippingStrategy
}

func NewShippingStrategyFactory() *ShippingStrategyFactory {
    return &ShippingStrategyFactory{
        strategies: map[string]shipping.ShippingStrategy{
            "jne":    shipping.NewJNEStrategy(),
            "jt":     shipping.NewJTExpressStrategy(),
            "gosend": shipping.NewGoSendStrategy(),
        },
    }
}

// Get returns the strategy by service name.
func (f *ShippingStrategyFactory) Get(name string) (shipping.ShippingStrategy, error) {
    strategy, ok := f.strategies[name]
    if !ok {
        return nil, fmt.Errorf("unknown shipping service %q", name)
    }
    return strategy, nil
}

// GetCheapest returns the cheapest strategy for a given order.
func (f *ShippingStrategyFactory) GetCheapest(order shipping.Order) (shipping.ShippingStrategy, error) {
    var (
        cheapestStrategy shipping.ShippingStrategy
        cheapestCost     = math.MaxInt64
    )

    for _, strategy := range f.strategies {
        if !strategy.Supports(order.OriginCity, order.DestCity) {
            continue
        }
        result, err := strategy.Calculate(order)
        if err != nil {
            continue
        }
        if result.Cost < cheapestCost {
            cheapestCost = result.Cost
            cheapestStrategy = strategy
        }
    }

    if cheapestStrategy == nil {
        return nil, fmt.Errorf("no shipping service available for this route")
    }
    return cheapestStrategy, nil
}

// Register adds a new strategy to the factory — for plugins or new couriers.
func (f *ShippingStrategyFactory) Register(name string, strategy shipping.ShippingStrategy) {
    f.strategies[name] = strategy
}

Testing the Strategy Pattern #

One of the biggest advantages of the Strategy Pattern is how easy testing becomes — every strategy can be tested in isolation.

func TestJNEStrategy_Calculate_BasicCase(t *testing.T) {
    strategy := shipping.NewJNEStrategy()
    order := shipping.Order{
        WeightKg:   2.0,
        OriginCity: "JAKARTA",
        DestCity:   "SURABAYA",
        OrderedAt:  time.Now(),
    }

    result, err := strategy.Calculate(order)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    // Base: 15000 + (2 * 8000) + 2000 = 33000, zone JAKARTA-SURABAYA = 1.0
    expectedCost := 33000
    if result.Cost != expectedCost {
        t.Errorf("expected cost %d, got %d", expectedCost, result.Cost)
    }
    if result.EstimatedDays != 3 {
        t.Errorf("expected 3 days, got %d", result.EstimatedDays)
    }
}

func TestJNEStrategy_Calculate_CODAdditionalFee(t *testing.T) {
    strategy := shipping.NewJNEStrategy()
    order := shipping.Order{
        WeightKg: 1.0, OriginCity: "JAKARTA", DestCity: "SURABAYA",
        OrderedAt: time.Now(), IsCOD: true,
    }

    result, err := strategy.Calculate(order)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    nonCODOrder := order
    nonCODOrder.IsCOD = false
    nonCODResult, _ := strategy.Calculate(nonCODOrder)

    if result.Cost <= nonCODResult.Cost {
        t.Error("COD order should cost more than non-COD")
    }
}

func TestJTExpressStrategy_WeekendDiscount(t *testing.T) {
    strategy := shipping.NewJTExpressStrategy()

    // Find the nearest Saturday
    saturday := time.Now()
    for saturday.Weekday() != time.Saturday {
        saturday = saturday.Add(24 * time.Hour)
    }

    weekdayOrder := shipping.Order{
        WeightKg: 1.0, OriginCity: "JAKARTA", DestCity: "SURABAYA",
        OrderedAt: time.Now(), // weekday
    }
    weekendOrder := weekdayOrder
    weekendOrder.OrderedAt = saturday

    weekdayResult, _ := strategy.Calculate(weekdayOrder)
    weekendResult, _ := strategy.Calculate(weekendOrder)

    if saturday.Weekday() == time.Saturday || saturday.Weekday() == time.Sunday {
        if weekendResult.Cost >= weekdayResult.Cost {
            t.Error("weekend order should be cheaper than weekday order")
        }
    }
}

func TestGoSendStrategy_ExceedsMaxDistance(t *testing.T) {
    strategy := shipping.NewGoSendStrategy()
    order := shipping.Order{
        WeightKg:   1.0,
        OriginCity: "JAKARTA",
        DestCity:   "BANDUNG", // distance 150km — exceeds the 30km max
    }

    _, err := strategy.Calculate(order)
    if err == nil {
        t.Error("expected error for distance exceeding max, got nil")
    }
}

func TestOrderService_SwitchStrategy(t *testing.T) {
    order := shipping.Order{
        WeightKg:   2.0,
        OriginCity: "JAKARTA",
        DestCity:   "SURABAYA",
        OrderedAt:  time.Now(),
    }

    svc := NewOrderService(shipping.NewJNEStrategy())

    // Calculate with JNE
    jneResult, _ := svc.GetShippingOptions(order, []shipping.ShippingStrategy{shipping.NewJNEStrategy()})

    // Swap to J&T and calculate again — no need to reconstruct the service
    svc.SetShippingStrategy(shipping.NewJTExpressStrategy())
    jtResult, _ := svc.GetShippingOptions(order, []shipping.ShippingStrategy{shipping.NewJTExpressStrategy()})

    // The two strategies produce different prices
    if len(jneResult) > 0 && len(jtResult) > 0 && jneResult[0].Cost == jtResult[0].Cost {
        t.Error("expected different costs for different strategies")
    }
}

// MockShippingStrategy for testing OrderService in isolation
type MockShippingStrategy struct {
    CalculateFn  func(order shipping.Order) (*shipping.ShippingResult, error)
    SupportsFn   func(origin, dest string) bool
    CalculateCalls int
}

func (m *MockShippingStrategy) Calculate(order shipping.Order) (*shipping.ShippingResult, error) {
    m.CalculateCalls++
    if m.CalculateFn != nil {
        return m.CalculateFn(order)
    }
    return &shipping.ShippingResult{Cost: 10000, EstimatedDays: 3, ServiceName: "Mock"}, nil
}

func (m *MockShippingStrategy) Name() string { return "Mock" }

func (m *MockShippingStrategy) Supports(origin, dest string) bool {
    if m.SupportsFn != nil {
        return m.SupportsFn(origin, dest)
    }
    return true
}

Strategy Pattern vs Other Similar Patterns #

Strategy is often confused with the State Pattern because both involve swapping behavior. The difference lies in who triggers the swap and whether state transitions exist.

AspectStrategyState
What changesThe algorithm for one taskThe object’s overall behavior
Who swapsThe client chooses the strategyThe object transitions between states itself
Do strategies know each other?NoStates often know other states for transitions
ExampleChoose a sort algorithm, courier, discountOrder: Draft → Paid → Shipped → Delivered

When to Use and When Not to #

USE Strategy if:
  ✓ There are several algorithm variations for the same goal
  ✓ Algorithm selection depends on runtime conditions (user tier, time, location)
  ✓ You find switch/if-else blocks that keep growing
  ✓ You want to add new algorithms without changing the Context
  ✓ You need to test each algorithm in isolation

AVOID Strategy if:
  ✗ There are only 1-2 algorithms that will not grow — a lambda or closure is simpler
  ✗ The algorithms are very short (1-2 lines) — the struct overhead is not worth it
  ✗ The Context needs to know strategy implementation details — this defeats the purpose
  ✗ Strategies need to communicate with each other — consider the Mediator

Strategy Review Checklist #

DESIGN:
  □ The Strategy interface only contains the methods actually needed
  □ Each concrete strategy focuses on one algorithm — no other logic
  □ The Context contains no type-based if-else on strategies
  □ Strategies can be swapped at runtime without reconstructing the Context

IMPLEMENTATION:
  □ Strategies are stateless when possible — safe for reuse and concurrency
  □ If a strategy has state, that state is thread-safe
  □ Errors from strategies are propagated with informative context
  □ Strategy.Name() or a similar identification method is available for logging

COMBINATION:
  □ A Factory is used to select strategies based on conditions
  □ Adding a new strategy requires no changes in the Context or Factory
  □ A strategy registry is available if the strategy count will keep growing dynamically

TESTING:
  □ Every concrete strategy is tested in isolation without the Context
  □ The Context is tested with a mock strategy
  □ Edge cases for each strategy are tested (invalid input, maximum limits, etc.)

Summary #

  • Strategy separates algorithms from their users — the Context only knows the interface, never the implementation; adding a new algorithm does not change the Context at all.
  • Replaces nested if-else — every case in a growing switch is a strong candidate for becoming a separate concrete strategy.
  • Swappable at runtime — unlike inheritance, which locks behavior at compile time; strategies can be changed any time via SetStrategy().
  • Two styles in Go: struct-based for strategies with state or many methods; functional (function types) for simple stateless strategies — both are valid.
  • Natural combination with Factory — the Factory selects the right strategy based on conditions; the Strategy runs its algorithm; the two work together without knowing each other.
  • Testing becomes very easy — every concrete strategy can be tested without the Context, without a database, without other dependencies; mock strategies enable isolated Context tests.
  • Strategies should be stateless when possible — stateless strategies are safe for concurrent use by multiple goroutines and can be shared via a Singleton or factory pool.
  • Distinguish it from the State Pattern: Strategy is chosen by the client from outside; State transitions by itself based on the object’s internal conditions.

← Previous: Proxy   Next: Observer →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact