ArchitectMind
  1. Home
  2. Tutorials
  3. Book 5 · Software Design & Design Patterns
  4. Volume IV
  5. Strategy in Java
Book 5 · Volume IV — Behavioral Design Patterns · Topic 1.2

Strategy in Java

Interfaces, lambdas and functional interfaces

8 min readStage 2 · Frameworks & design8 interview questions
Chapter
Chapter 1 — Strategy
Prerequisites
Topic 1.1; Book 2 lambdas and functional programming basics
You will be able to
Implement strategies as interfaces, enums, lambdas and method references, pick the right java.util.function type or a custom functional interface, and register strategies as Spring beans selected at runtime
Covers
Interfaces; Lambdas; Functional interfaces
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — Lambdas on the JVM
  5. 4A. Deep Dive — Strategies as Spring Beans
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

The Strategy pattern is language-independent, but how you write it in Java has changed dramatically. Since Java 8, any interface with a single abstract method is a functional interface that can be implemented by a lambda or method reference — which removes most of the boilerplate the classic pattern required.

Strategy through Java's history
Figure 1 — Strategy through Java's history
Java formCode size per strategyBest for
Named class10–30 linesStrategies with dependencies, names, tests of their own
Anonymous class5–10 linesLegacy code (pre-Java 8)
Enum constant3–8 linesFixed, known set of variants
Lambda1–3 linesSmall, stateless, local strategies
Method reference1 lineReusing an existing method as a strategy
Record3–10 linesParameterised strategies with validation
Table 1 — Java forms of a strategy

2Why It Exists

Before Java 8, every strategy needed a class — often an anonymous inner class with more ceremony than logic. This friction pushed developers towards if/else instead. Lambdas and functional interfaces made passing behaviour as cheap as passing data, turning Strategy from a "design pattern" into everyday Java style: list.sort(comparing(Order::placedAt)) is a strategy in one line. Knowing the forms lets you choose the lightest one that still keeps the design clear.

3Core Concepts

3.1 Interfaces

The strategy contract is a Java interface. Design it deliberately:

JAVA
@FunctionalInterface                                  // compiler enforces single method
public interface RoundingPolicy {
    BigDecimal round(BigDecimal amount);
 
    // default methods add composition without breaking the functional shape
    default RoundingPolicy andThen(RoundingPolicy next) {
        return v -> next.round(round(v));
    }
 
    // static factories give names to common strategies
    static RoundingPolicy bankers(int scale) {
        return v -> v.setScale(scale, RoundingMode.HALF_EVEN);
    }
    static RoundingPolicy commercial(int scale) {
        return v -> v.setScale(scale, RoundingMode.HALF_UP);
    }
}
Interface design choiceRecommendation
Number of abstract methodsOne — keeps it lambda-compatible
@FunctionalInterfaceAlways, to document and enforce intent
Parameter typesDomain types (Parcel, Money), not Object or maps
ExceptionsUnchecked, or a result type; checked exceptions don't mix well with lambdas
Default methodsFor composition (andThen, orElse) and convenience
Static factoriesNamed, reusable standard strategies
Table 2 — Designing a strategy interface

Enum strategies suit a fixed set of variants that also needs names (for configuration or persistence):

JAVA
public enum VatMode implements RoundingPolicy {
    INVOICE_TOTAL { public BigDecimal round(BigDecimal v) {
        return v.setScale(2, RoundingMode.HALF_UP); } },
    PER_LINE      { public BigDecimal round(BigDecimal v) {
        return v.setScale(2, RoundingMode.HALF_EVEN); } };
}
 
VatMode mode = VatMode.valueOf(config.get("vat.mode"));    // selection by name

3.2 Lambdas

A lambda implements a functional interface inline. For small, stateless strategies it removes almost all ceremony:

JAVA
// Classic anonymous class (pre-Java 8)
orders.sort(new Comparator<Order>() {
    @Override public int compare(Order a, Order b) {
        return a.placedAt().compareTo(b.placedAt());
    }
});
 
// Lambda
orders.sort((a, b) -> a.placedAt().compareTo(b.placedAt()));
 
// Method reference + comparator combinators
orders.sort(Comparator.comparing(Order::placedAt).thenComparing(Order::total).reversed());

Lambdas can capture effectively-final variables, which lets you build parameterised strategies on the fly:

JAVA
static ShippingPolicy flat(Money amount) {
    return (parcel, dest) -> amount;                       // captures 'amount'
}
static ShippingPolicy perKg(Money base, Money perKg) {
    return (parcel, dest) -> base.plus(perKg.times(parcel.weightKg()));
}
Lambda, named class or enum?
Figure 2 — Lambda, named class or enum?
ConsiderationLambdaNamed class
Stack traces / logslambda$select$3 — anonymousZoneBasedShipping — readable
DependenciesCaptured variablesConstructor-injected beans
Testing in isolationThrough the factory methodDirectly
Configuration by nameNeeds a registry keyBean name / enum constant
Size of logicA few linesAny size
Table 3 — Lambda vs named class

3.3 Functional interfaces

The JDK's java.util.function package provides ready-made strategy shapes. Using them avoids declaring new interfaces for simple cases:

java.util.function as a strategy toolbox
Figure 3 — java.util.function as a strategy toolbox
JAVA
public final class ReportService {
    private final Predicate<Order> include;              // filtering strategy
    private final Function<Order, String> label;          // formatting strategy
    private final Comparator<Order> order;                // ordering strategy
 
    public ReportService(Predicate<Order> include, Function<Order, String> label,
                         Comparator<Order> order) {
        this.include = include;
        this.label = label;
        this.order = order;
    }
 
    public List<String> lines(List<Order> orders) {
        return orders.stream().filter(include).sorted(order).map(label).toList();
    }
}
 
var service = new ReportService(
        o -> o.status() == Status.OPEN,
        o -> o.id() + " " + o.total(),
        Comparator.comparing(Order::total).reversed());

Generic vs domain-specific interfaces. BiFunction<Parcel, Destination, Money> works, but ShippingPolicy is self-documenting, can carry default methods and static factories, and appears meaningfully in stack traces and Javadoc. Rule of thumb: use java.util.function for local, technical strategies; declare a named functional interface for strategies that are part of your domain or public API.

Built-in typeDomain alternativeWhen to prefer the domain type
Predicate<Order>OrderFilterWhen it appears in public APIs or configuration
Function<Cart, Money>DiscountPolicyAlways for business rules
Supplier<Instant>Clock (JDK)Use the existing JDK abstraction
BiFunction<Parcel, Destination, Money>ShippingPolicyAlways — two-argument generics are hard to read
Table 4 — Built-in vs domain-specific

4How It Works Internally — Lambdas on the JVM

1. The compiler turns the lambda body into a private synthetic method in the enclosing class.

2. At the call site it emits an invokedynamic instruction bootstrapped by LambdaMetafactory.

3. On first execution the JVM spins a hidden class implementing the functional interface; non-capturing lambdas are cached as a single instance.

4. Subsequent calls are ordinary interface calls — typically inlined by the JIT.

Lambda kindAllocation per evaluation
Non-capturing ((a, b) -> a.compareTo(b))None — cached singleton
Capturing ((p, d) -> amount)One small object per evaluation of the lambda expression
Method reference to static methodNone
Bound method reference (obj::method)One object capturing obj
Table 5 — Lambda costs

For strategies created once (at startup or per configuration change), these costs are irrelevant.

4ADeep Dive — Strategies as Spring Beans

Strategies as Spring beans
Figure 4 — Strategies as Spring beans

When strategies need dependencies (repositories, HTTP clients, configuration), make each a Spring bean and let the container build the registry:

JAVA
public interface FraudCheck {
    FraudVerdict assess(Payment p);
    String name();
}
 
@Component class VelocityCheck implements FraudCheck { /* uses Redis counters */ }
@Component class GeoMismatchCheck implements FraudCheck { /* uses GeoIP client */ }
@Component class DeviceFingerprintCheck implements FraudCheck { /* uses vendor API */ }
 
@Service
class FraudService {
    private final Map<String, FraudCheck> checks;                  // bean name → strategy
 
    FraudService(List<FraudCheck> all) {
        this.checks = all.stream()
                .collect(Collectors.toUnmodifiableMap(FraudCheck::name, c -> c));
    }
 
    // per-tenant selection of enabled checks
    FraudVerdict assess(Payment p, List<String> enabled) {
        return enabled.stream().map(checks::get).map(c -> c.assess(p))
                      .reduce(FraudVerdict.PASS, FraudVerdict::worst);
    }
}
Spring mechanismUse
List<T> injectionAll implementations, in @Order order
Map<String, T> injectionKeyed by bean name
@Qualifier / @PrimaryPick one strategy at injection time
@ConditionalOnPropertyEnable a strategy by configuration
ObjectProvider<T>Lazy or optional strategy resolution
Table 6 — Spring support for strategies

Validate the registry at startup: if configuration references "deviceCheck" but no bean has that name, fail fast rather than at the first payment.

5APIs / Features / Syntax

FeatureStrategy use
@FunctionalInterfaceDeclare strategy contracts
Lambdas, method referencesInline strategies
Comparator.comparing/thenComparing/reversedComposable ordering strategies
Predicate.and/or/negateComposable filters
Function.andThen/composePipelines of transformations
Enums with bodiesNamed, fixed strategy sets
ServiceLoaderPlug-in strategies without Spring
Table 7 — Language and library features

6Practical Examples

6.1 Composing predicates as strategies

JAVA
Predicate<Order> open      = o -> o.status() == Status.OPEN;
Predicate<Order> highValue = o -> o.total().isAtLeast(Money.eur("1000"));
Predicate<Order> domestic  = o -> o.country() == Country.IN;
 
Predicate<Order> reviewQueue = open.and(highValue.or(domestic.negate()));

6.2 Choosing a strategy from configuration

JAVA
@Bean
RoundingPolicy rounding(@Value("${billing.rounding:bankers}") String mode) {
    return switch (mode) {
        case "bankers"    -> RoundingPolicy.bankers(2);
        case "commercial" -> RoundingPolicy.commercial(2);
        default -> throw new IllegalStateException("unknown rounding " + mode);
    };
}

6.3 Plug-in strategies with ServiceLoader

JAVA
ServiceLoader.load(ExportFormat.class).stream()
        .map(ServiceLoader.Provider::get)
        .forEach(f -> registry.put(f.id(), f));

Third-party JARs add formats by declaring provides ExportFormat with ... in their module-info.java.

7Common Problems & Mistakes

  • Long lambdas (20+ lines) that should be named classes.
  • Generic BiFunction types in public APIs, obscuring meaning.
  • Capturing mutable state in lambdas used concurrently.
  • Checked exceptions forcing awkward wrapping inside lambdas — design the interface for it.
  • Registry built from reflection/strings without startup validation.
  • Anonymous lambdas in logs — hard to diagnose which strategy ran.

8Performance & Production Considerations

AspectGuidance
Lambda creationCreate strategies once; avoid building capturing lambdas in hot loops
InliningFew implementations per call site inline well
NamingGive strategies toString() or a name() for logs and metrics
Hot reloadRebuild strategy registries atomically on configuration change
GraalVM nativeLambdas and method references are supported; reflection-based registries need hints
Table 8 — Production guidance

9Architecture & Design Considerations

  • Prefer domain-named functional interfaces for business strategies; use JDK functional types for technical plumbing.
  • Keep strategies pure (inputs → output) where possible; inject dependencies explicitly otherwise.
  • Build one registry per strategy family, validated at startup, observable at runtime.
  • Treat strategy names as part of configuration contracts — rename with migration.

10Interview Questions & Answers

Q1BasicConcept

What is a functional interface?

A functional interface is an interface with exactly one abstract method, so a lambda or method reference can implement it. Default and static methods do not count, nor do abstract redeclarations of public Object methods such as equals. Comparator, Runnable, Callable and everything in java.util.function, like Predicate and Function, are functional interfaces. The @FunctionalInterface annotation is optional, but it documents intent and makes the compiler reject a second abstract method that would break every lambda implementing the interface. For strategies this matters: a single-method RoundingPolicy can be supplied as v -> v.setScale(2, RoundingMode.HALF_EVEN), while default methods such as andThen add composition without breaking the functional shape.

Q2BasicConcept

How does a lambda relate to the Strategy pattern?

A lambda is a concise implementation of a strategy. The Strategy pattern defines a family of interchangeable algorithms behind a common interface and lets the client choose one at run time. If that interface is functional, each concrete strategy can be a lambda or method reference instead of a separate class, so behaviour is passed around as a value. list.sort(Comparator.comparing(Order::placedAt)) is the Strategy pattern in one line: List.sort is the context, Comparator is the strategy interface and the method reference is the concrete strategy. Lambdas can also capture effectively final variables to build parameterised strategies, such as a flat shipping policy that returns a captured amount.

Q3IntermediateTrade-off

When would you use an enum instead of lambdas for strategies?

Use an enum when the set of variants is fixed, known at compile time and needs stable names. Enum constants can implement a strategy interface with their own bodies, as VatMode.INVOICE_TOTAL and PER_LINE do, and can be selected by name from configuration or stored in a database column using valueOf and name(). You also get values() for listing options, EnumMap and exhaustive switch support, readable names in logs and singletons for free. Lambdas suit open-ended, local or parameterised strategies created in code. The trade-off is that renaming an enum constant becomes a data migration, and adding a variant means recompiling, so enums do not fit plug-in or tenant-defined strategies.

Q4IntermediateCode

Name four java.util.function interfaces and a strategy use for each.

Predicate<T> takes a value and returns a boolean, so it is a filtering strategy, for example o -> o.status() == Status.OPEN, composable with and, or and negate. Function<T, R> transforms one value into another, so it suits mapping or formatting strategies such as o -> o.id() + " " + o.total(), and composes with andThen and compose. Supplier<T> takes nothing and returns a value, making it a strategy for generating or lazily creating values, such as ID generators or default factories. Consumer<T> accepts a value and returns nothing, so it fits sinks like sending notifications or writing audit entries. UnaryOperator, BinaryOperator and BiFunction cover other shapes, and primitive variants avoid boxing.

Q5Senior EngineerTrade-off

When should you declare your own functional interface rather than use a JDK one?

Declare your own when the strategy is part of the domain or a public API. A ShippingPolicy with cost(Parcel, Destination) documents intent far better than BiFunction<Parcel, Destination, Money>, whose meaning lives only in a variable name. A named interface can carry Javadoc, default methods for composition, static factories for standard strategies such as RoundingPolicy.bankers(2), and checked exceptions or domain result types if needed. It also appears meaningfully in stack traces, IDE searches and Spring bean types, and two unrelated BiFunctions can no longer be swapped by mistake. Use JDK types for local, technical plumbing inside a method or class, where a new interface would add ceremony without meaning.

Q6Senior EngineerCode

How do you register strategies in Spring?

Make each strategy a Spring bean implementing a common interface, typically with @Component, and let the container collect them. Injecting List<FraudCheck> gives every implementation, ordered by @Order, while Map<String, FraudCheck> gives them keyed by bean name. The service then builds an immutable registry, often keyed by a name() method rather than bean names so the key is an explicit contract, and selects strategies per request, tenant or configuration. @Qualifier or @Primary pick a single strategy at injection time, and @ConditionalOnProperty enables one through configuration. Validate the registry at startup: if configuration names a strategy with no matching bean, fail the application context instead of the first request.

Q7Technical ArchitectTricky

What are the downsides of lambdas as strategies?

Lambdas are anonymous, which hurts operations: stack traces show names like lambda$select$3, and logs cannot say which strategy ran unless you add a name or toString. They are harder to test in isolation, because you usually test them through the factory method or class that creates them. They fit poorly when logic grows beyond a few lines, needs several dependencies, or must be configured and referenced by name. Capturing mutable state can create concurrency bugs, and checked exceptions force awkward wrapping. A lambda's identity and equality are also unspecified, so never use lambdas as map keys or compare them with ==. Promote such strategies to named classes or enums.

Q8Solution ArchitectDesign

How would you expose extension points in a shared platform library?

I would define small, documented functional interfaces for each policy the platform lets consumers change, such as RetryPolicy, TenantResolver or AuditSink, using domain types in the signatures and keeping one abstract method so consumers can plug in a lambda. Each interface ships sensible defaults through static factories, like RetryPolicy.exponential(3), plus default methods for composition. The library accepts implementations through constructors or builders, Spring auto-configuration with @ConditionalOnMissingBean so a consumer's bean overrides the default, and ServiceLoader for non-Spring users. I treat these interfaces as public APIs: semantic versioning, compatibility tests, deprecation before removal, and never adding an abstract method in a minor release, because that breaks every lambda.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 5

Software Design & Design Patterns

Volume I · Topic 1.1

Software Design

Software Design Foundations & Principles

Volume I · Topic 1.2

Why Software Design Matters

Software Design Foundations & Principles

Volume I · Topic 1.3

Good vs Poor Design

Software Design Foundations & Principles

Volume II · Topic 1.1

Objects

Object-Oriented Design & SOLID

Volume II · Topic 1.2

Classes

Object-Oriented Design & SOLID

Volume II · Topic 1.3

Relationships

Object-Oriented Design & SOLID

Volume III · Topic 1.1

What Is a Design Pattern?

Creational & Structural Design Patterns

Volume III · Topic 1.2

Pattern Categories

Creational & Structural Design Patterns

Volume III · Topic 1.3

Pattern vs Framework

Creational & Structural Design Patterns

Volume IV · Topic 1.1

Strategy Pattern

Behavioral Design Patterns

Volume IV · Topic 1.3

Strategy vs Conditional Logic

Behavioral Design Patterns

Volume V · Topic 1.1

Layered Architecture

Enterprise & Application Design Patterns

Volume V · Topic 1.2

Service Layer

Enterprise & Application Design Patterns

Volume V · Topic 1.3

Repository

Enterprise & Application Design Patterns

Volume VI · Topic 1.1

What Is Refactoring?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.2

Why Refactor?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.3

Refactoring Safety

Refactoring, Code Quality & Evolutionary Design

Volume VII · Topic 1.1

Method-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.2

Class-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.3

Module-Level Design

Advanced Design & Architecture-Level Design Decisions