The Problem
Business rules such as pricing, tax, ranking, or retry policies often vary by context. If every caller uses conditionals to choose which algorithm to run, the policy logic spreads out and becomes harder to test or replace.
The Solution
Strategy hides each algorithm behind a shared interface. In Go, strategies are often tiny interfaces backed by small structs or even function adapters. The caller holds one strategy value and delegates the variable behavior through that contract.
Structure
The Strategy Interface
PricingStrategy is the single-method contract: Calculate(items []CartItem) Money. All three concrete strategies satisfy this interface identically — Checkout only ever sees this shape.
flowchart LR Client["Client"] Checkout["Checkout Context"] Strategy["PricingStrategy interface"] Standard["StandardPricing"] Member["MemberPricing"] Campaign["CampaignPricing"] Client -->|"injects strategy"| Checkout Checkout -->|"Calculate(items)"| Strategy Standard -.->|"implements"| Strategy Member -.->|"implements"| Strategy Campaign -.->|"implements"| Strategy
- Strategy interface:
PricingStrategydefines how cart totals are calculated. - Concrete strategies: Standard, member, and campaign pricing apply different rules.
- Context:
Checkoutdelegates the calculation to the selected strategy. - Client: Chooses the strategy based on runtime context.
Implementation
This example calculates a cart total through interchangeable pricing strategies. The checkout flow does not branch on membership or campaign rules once a strategy has been selected.
package main
type CartLine struct {
Name string
Qty int
UnitPrice int
}
type PricingStrategy interface {
Total(lines []CartLine) int
}
type Checkout struct {
strategy PricingStrategy
}
func NewCheckout(strategy PricingStrategy) Checkout {
return Checkout{strategy: strategy}
}
func (c Checkout) Total(lines []CartLine) int {
return c.strategy.Total(lines)
} Real-World Analogy
Think of a navigation app choosing between fastest, cheapest, or scenic route options. The trip is the same, but the routing strategy changes how the path is calculated.
Pros and Cons
| Pros | Cons |
|---|---|
| Makes algorithms easy to swap without changing callers. | Adds abstraction that is unnecessary when one algorithm is stable. |
| Shrinks large conditionals into interchangeable policy objects. | The caller or composition layer still has to choose the right strategy. |
| Supports focused testing of each rule set. | Too many tiny strategies can make the design feel fragmented. |
Best Practices
- Keep strategy interfaces very small so new policies stay cheap to implement.
- Inject the strategy from the outside; avoid contexts that secretly choose their own policy.
- Use ordinary functions when a full struct adds no value. In Go, function types can be perfectly good strategies.
- Prefer Strategy when callers need to swap algorithms at runtime.
- Do not create a strategy hierarchy for logic that rarely changes or has only one implementation.
When to Use
- You have several interchangeable algorithms with the same input and output shape.
- Callers should choose policy without long conditional branches.
- Different environments or customer tiers need different rules.
When NOT to Use
- The logic is stable and a simple helper function is enough.
- There is only one algorithm and no realistic need to swap it.
- The abstraction would add more indirection than flexibility.