Strategy in modern Java, separated into three things that are usually conflated: the design concept (an algorithm varies), the classical class hierarchy, and the lambda or functional interface that expresses it today. Covers when a function value is enough and when a named type earns its keep, selecting a strategy by key instead of an if-else chain, the trap of strategies that differ only in constants and may be configuration, how shared state changes concurrency obligations, and the contract...
Scanned 9/19/2026
Install to Claude Code
npx -y skills add robsonkades/agent-skills --skill gof-strategy --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gof Strategy?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/robsonkades-gof-strategy)More formats (shields.io, HTML) on the badges page.
---
name: gof-strategy
description: >
Strategy in modern Java, separated into three things that are usually conflated: the design
concept (an algorithm varies), the classical class hierarchy, and the lambda or functional
interface that expresses it today. Covers when a function value is enough and when a named type earns its
keep, selecting a strategy by key instead of an if-else chain, the trap of strategies that
differ only in constants and may be configuration, how shared state changes concurrency
obligations, and the contract test implementations can share. Use when an algorithm must vary
at runtime, when a switch over a type code keeps growing, when a class hierarchy exists whose
members are one-line methods, or when strategy classes differ only in a rate or a threshold.
Does not cover lifecycle-governed legal operations and transitions (gof-state), two independently varying hierarchies (gof-bridge), an
algorithm skeleton with varying steps (gof-template-method), or choosing which object to create
(gof-factory-method).
---
# Strategy
## Purpose
Let an operation's algorithm vary independently of the code that uses it. Strategy is the most
useful and frequently over-implemented pattern in object design. The design concept can be sound
while a class hierarchy, lambda, enum strategy, table or direct branch is the better mechanism.
Three things share the name, and separating them settles most arguments:
```text
The concept "This algorithm varies; callers rely on a shared
contract." Selection may still be explicit.
The class interface + N implementations + a selector. One
hierarchy expression of the concept, and the heaviest.
The function a lambda or method reference passed where the
value algorithm is needed. Another expression of the same
concept, and usually the right one.
```
A `Comparator` lambda is Strategy. Say so in review — recognising the intent is what keeps the
design legible; hand-building the hierarchy is what makes it bulky.
Start with consumer calls, required inputs/results/failures, the extension model and existing
selection/configuration evidence. Reuse accepted contracts before asking about material gaps;
keep an adequate branch or function. A changing policy reference alone does not justify a new pattern.
## When it is the answer
```text
An operation has more than one algorithm and the choice is made at
runtime — by configuration, by a caller, by data
→ Strategy, in whichever mechanism fits.
A switch over a type code keeps growing, and each branch is a
self-contained calculation
→ Strategy, keyed by that code.
Callers must be able to supply their own algorithm
→ Strategy as a functional interface in your public API.
```
## When it is not
- **One algorithm exists and there is no boundary/ownership reason to abstract it.** An interface
may be premature, while a published port, platform SPI or testable external policy can justify
one implementation (`gof-pattern-thinking`).
- **The variants differ only in data and share identical rules.** Prefer typed configuration.
Separate named policies can still be warranted when validation, authorization, rollout,
compatibility or lifecycle differs.
- **The behavior governs lifecycle-dependent operations and valid transitions.** That is State
(`gof-state`). Interchangeable policies may still be selected internally or adapt to current data/load.
- **Only part of an algorithm varies, inside a fixed sequence.** Compare Template Method with
a fixed method taking the varying part as a function, preserving existing extension contracts
(`gof-template-method`).
- **The branches are not self-contained.** If each `switch` arm mutates shared state and depends
on the others, extracting them into strategies moves a tangle rather than resolving it.
## Lambda or named type?
```text
A lambda / method reference is enough when:
one operation; captured dependencies and lifetime have clear contracts;
selection/metadata can live in a registration rather than the implementation
A named type earns its place when:
related operations and invariants are clearer together in one implementation
(apply and supports(...), for example)
key/metadata and related operations belong together for cohesion
its injected dependencies or implementation are clearer as a class
it must appear in stack traces, thread dumps and metrics by name
it has its own tests and its own reason to change
```
Both are Strategy. The failure is not choosing the "wrong" one — it is building a five-class
hierarchy when three lambdas would do, or scattering anonymous lambdas that nobody can find when
the calculation misbehaves in production.
## Selection
Examples are partial Java 17 snippets with application types. Sealed classes are standard in 17;
type-pattern switch is final in 21 (earlier supported releases require preview). Inspect the target
toolchain and framework configuration; no upgrade or new dependency is implied.
```java
// what grows badly
if (code.equals("FLAT")) return flat(order);
else if (code.equals("TIERED")) return tiered(order);
else if (code.equals("WEIGHT")) return byWeight(order);
// ... and the else branch, which returns zero and nobody noticed
// keyed lookup, with the failure defined
private final Map<ShippingMethod, ShippingCost> byMethod;
ShippingCost costFor(ShippingMethod method) {
var strategy = byMethod.get(method);
if (strategy == null) throw new UnsupportedShippingMethod(method, byMethod.keySet());
return strategy;
}
```
Spring can inject eligible registered beans as a list or string-keyed map; classpath presence alone
does not register every implementation. Conditions, qualifiers and scanning affect the set, so an
accidental extra bean silently joins it and a missing one silently does not. Build the map from an
explicit key the strategy or registration declares, and fail at startup if a key is duplicated or a required key is
absent.
For a closed key set, compare an enum or compatible exhaustive switch with a validated registry.
Compilation checks known variants, not null inputs, binary evolution or calculation failures.
## Decision rules
```text
IF strategies differ only in values
THEN first model typed validated configuration. Keep named strategies only when the
values carry distinct policy ownership, compatibility or behavior contracts.
IF a strategy holds mutable state and is shared
THEN define synchronization, confinement or immutable snapshots. Stateless strategies
are easiest to share, but stateful incremental algorithms are valid when lifetime
and thread-safety are part of the contract.
IF selection is by a chain of if-else on a code
THEN compare an exhaustive switch for a closed set with a validated map/registry for
open contributions. Define unknown/default semantics explicitly.
IF a strategy needs to know whether it applies
THEN compare a cohesive named implementation with a predicate/function registration;
metadata and applicability need not be abstract methods on the public functional interface.
Consider Chain of Responsibility if several may
apply in order (gof-chain-of-responsibility).
IF strategies are selected from data crossing a trust boundary
THEN validate/authorize against the supported registry. Extensible sets need not be
compile-time closed, but untrusted input must never become an arbitrary class name.
IF a calculation is hard to attribute in profiles/logs
THEN use a named method/type, explicit metric tag, or registration metadata. A whole
class is one option, not the only diagnostic identity.
IF every strategy needs the same pre- and post-processing
THEN that is a template, and it belongs in the caller once — not
copied into each strategy.
IF the strategy choice changes system behaviour beyond this call —
partitioning, routing, serialisation
THEN inspect persisted data, in-flight work and old/new coexistence requirements.
Incompatible placement or format changes need migration; a compatible stateless policy
change can use a validated configuration rollout
(message-ordering-and-partitioning).
```
## Cross-cutting checks
- **Concurrency.** A shared strategy needs an explicit synchronization/confinement or immutable-state
contract for itself and captured/injected collaborators. The recurring bug is a strategy
accumulating results in a field — possibly missed by a one-call test, but leaking across calls,
with one request's data appearing in another's (`java-memory-model`).
- **Distribution.** Several of the most consequential strategies in a distributed system are
chosen by configuration and have system-wide effects: the partitioning strategy determines
ordering guarantees, the serialisation strategy determines compatibility, the retry policy
determines amplification under failure, the load-balancing strategy determines tail latency.
Assess coexistence, rollback and retained effects before choosing a rollout or migration;
a strategy label alone does not require data migration or a compatibility window
(`sharding-and-partitioning`, `load-balancing-and-routing`, `retries-and-backoff`).
- **Performance.** Dispatch and inlining depend on receiver profiles, compilation tier and code
shape rather than a fixed implementation count. Non-capturing lambdas may be cached; capturing
lambdas can allocate and either form may inline. Inspect profiles/compilation on measured hot paths
(`jit-inlining-and-escape-analysis`).
- **Testing.** Three levels. Test each strategy against its inputs and declared state/lifecycle;
strategies are not inherently pure. Test the selector separately, including the
unknown-key case. And a shared contract test that every implementation must pass, which is what
stops the fifth strategy from quietly violating an invariant the first four honour.
## Review checklist
- [ ] Variation exists today, or a concrete port/SPI/ownership boundary justifies one implementation
- [ ] Variants justify behavior or a named policy/ownership reason; data-only alternatives were considered
- [ ] Strategy state has explicit immutability, confinement or synchronization semantics
- [ ] Selection matches the extension model; unknown/default semantics are deliberate
- [ ] External keys are validated/authorized against a supported registry
- [ ] Diagnostic identity is available through named methods/types or bounded registration metadata
- [ ] Shared pre/post processing lives in the caller, not duplicated per strategy
- [ ] A contract test runs against every implementation
- [ ] System-wide policy changes have an evidence-based rollout or migration that preserves required contracts
## References
Deliver the variation and contract, chosen mechanism/selector, state ownership and failure behavior,
plus focused checks. Keep performance benefits conditional on measurements.
- [Concept, mechanism and selection](references/concept-mechanism-selection.md) — the three levels
in detail; lambda against named type with the criteria that decide; selection mechanisms
compared (map, sealed switch, injected list, `ServiceLoader`) with their failure modes; the
constants-are-configuration test; and the shared contract test. Read when choosing a mechanism.
- [Worked example](references/worked-example.md) — shipping cost calculation taken from a growing
if-else to lambdas, then to named strategies when logging, metrics and a `supports` check were
needed, with unknown-method handling, an illustrative shared-state race, and the
contract test. Read when implementing.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!