Explore Strategy in Scala as interchangeable policy behavior using functions, traits, and type classes, with guidance on when strategy is clearer than state, branching, or configuration flags.
Strategy pattern: A behavioral pattern in which a caller selects one of several interchangeable algorithms or policies without changing the rest of the surrounding workflow.
Scala is a very strong fit for Strategy because interchangeable behavior often collapses into a function value, a small trait, or a type class instance instead of a class-heavy object hierarchy.
Use Strategy when the important decision is “which policy should apply here?” rather than “what state is this object in?”
Typical examples include:
The main value is that the caller can swap the behavior without rewriting the workflow that depends on it.
If the strategy has one operation and little internal state, a plain function is usually the cleanest form:
1final case class Order(total: BigDecimal, loyaltyTier: String)
2
3type PricingStrategy = Order => BigDecimal
4
5val standardPricing: PricingStrategy =
6 order => order.total
7
8val loyaltyPricing: PricingStrategy =
9 order =>
10 if order.loyaltyTier == "gold" then order.total * BigDecimal("0.90")
11 else order.total
12
13final class Checkout(pricing: PricingStrategy):
14 def quote(order: Order): BigDecimal =
15 pricing(order)
This keeps the design honest. There is a policy choice, but there is no need to invent inheritance just to represent it.
A trait or class-based strategy still makes sense when the behavior has:
That is common for payment gateways, cache eviction policies, or retry schedulers. In those cases, the extra wrapper is carrying real boundary meaning, not just ceremony.
Scala also gives you a third option: behavior selected by available capability rather than by passing a value explicitly. That moves Strategy toward type-class design.
Use that shape when the policy should be chosen by type context, not by runtime user choice. If the user or workflow chooses the behavior at runtime, functions or trait instances are usually clearer.
Strategy and State can look similar because both change behavior. The difference is where the decision comes from:
If the checkout process applies a different pricing policy because the caller selected a plan, that is Strategy. If behavior changes because an order moved from Draft to Submitted, that is State.
Strategy helps when algorithm choice is a real design concern. It becomes noise when:
if branchA short pattern match is often better than a fake family of strategies.
Every small branch becomes its own “strategy,” and the codebase fills with tiny wrappers that do not protect a meaningful boundary.
Anonymous function strategies quietly close over too much state, making testing and review harder than the equivalent named policy.
Type classes, passed functions, and injected services all express variation differently. Pick the one that matches when and how the choice is supposed to happen.
Start with a function when the strategy is small and runtime-selected. Move to a trait or service boundary when the strategy has richer dependencies or multiple operations. Reach for type classes only when behavior should follow available capability rather than explicit runtime choice.