*1. Foundations — The Problem and the Theory *
_1.1 Introduction _
Strategy is a behavioral design pattern cataloged by the Gang of Four (Gamma, Helm, Johnson & Vlissides,
1994). Its purpose is to encapsulate a family of interchangeable algorithms behind a common interface,
allowing the algorithm used by an object to vary at runtime without changing the code of the client that
invokes it.
Unlike creational patterns (which deal with how objects are instantiated) or structural patterns (which deal
with how objects compose), Strategy solves a behavioral problem: which business rule should run in a given
context, and how to swap that rule without rewriting the class that uses it.
1.2 The Problem
Medium- and long-lived systems tend to accumulate payment methods, notification providers, shipping-cost
algorithms, discount policies — any rule variation that grows over time. The naive implementation
concentrates these variations in a single class, using a chain of conditionals:
if (method.equals("CREDIT_CARD")) {
// Stripe logic
} else if (method.equals("PIX")) {
// Central Bank logic
} else if (method.equals("BOLETO")) {
// issuing bank logic
} else if (method.equals("PAYPAL")) {
// PayPal logic
} // ... and so on
This design violates the Open/Closed Principle: adding a new payment method requires editing an existing
class that already concentrates logic for several distinct external integrations. In practice, on a real legacy
system, this was exactly the bottleneck faced: a PaymentService class with more than 900 lines, low test
coverage (testing one method risked breaking another due to shared side effects), and a commit history in
which any localized change — even to a single gateway — required recompiling, retesting, and redeploying
the entire service, increasing the risk of regression in already-stable payment flows.
1.3 Concept
Strategy solves this by extracting each algorithm variation into its own class, all implementing a common
interface. The object that originally held the logic (the "Context") now depends only on the interface and
receives — through dependency injection or configuration — which concrete implementation to use.
A direct analogy from software development: think of the JDBC interface. Data-access code programs against
java.sql.Connection and java.sql.Driver — never against a specific database implementation. Switching from
PostgreSQL to MySQL doesn't require rewriting the persistence layer, only swapping the injected driver.
Strategy applies exactly the same idea to business rules: the Context (the consumer of the behavior) never
knows the concrete implementation, only the contract.
*2. Development — Case Study, Architecture, and Code
2.1 Real-World Scenario *
Scenario: a Payment Service microservice, responsible for processing payments for an e-commerce platform,
needs to support four payment methods — credit card (via Stripe), Pix (via the Central Bank/SPI API), boleto
(via the issuing bank), and PayPal — with a strong likelihood that new methods will be added next quarter
(digital wallets, crypto assets). The architectural requirement is clear: adding a new payment method must
not require changing or retesting the existing orchestration logic, only adding a new class.
*2.2 UML Class Diagram *

**
2.3 High-Level Architecture Diagram**
*2.4 Java Implementation *
The interface defines the contract every payment strategy must fulfill:
public interface PaymentStrategy {
boolean supports(String method);
PaymentResult pay(Order order);
}
Each external gateway becomes a concrete implementation, isolated and independently testable:
@Component
public class CreditCardPaymentStrategy implements PaymentStrategy {
private final StripeClient stripeClient;
public CreditCardPaymentStrategy(StripeClient stripeClient) {
this.stripeClient = stripeClient;
}
@Override
public boolean supports(String method) {
return "CREDIT_CARD".equalsIgnoreCase(method);
}
@Override
public PaymentResult pay(Order order) {
StripeCharge charge = stripeClient.charge(
order.getAmount(), order.getCardToken());
return new PaymentResult(charge.getId(), PaymentStatus.APPROVED);
}
}
@Component
public class PixPaymentStrategy implements PaymentStrategy {
private final PixClient pixClient;
public PixPaymentStrategy(PixClient pixClient) {
this.pixClient = pixClient;
}
@Override
public boolean supports(String method) {
return "PIX".equalsIgnoreCase(method);
}
@Override
public PaymentResult pay(Order order) {
PixCharge charge = pixClient.createCharge(order.getAmount());
return new PaymentResult(charge.getTxId(), PaymentStatus.PENDING);
}
}
BoletoPaymentStrategy and PayPalPaymentStrategy follow exactly the same shape — each isolated in its
own class, with no coupling between them.
The Context handles orchestration only: it selects the correct strategy, delegates execution, persists the
result, and publishes the domain event. It never knows any gateway-specific detail:
@Service
public class PaymentProcessor {
private final List<PaymentStrategy> strategies;
private final PaymentRepository paymentRepository;
private final PaymentEventPublisher eventPublisher;
public PaymentProcessor(List<PaymentStrategy> strategies,
PaymentRepository paymentRepository,
PaymentEventPublisher eventPublisher) {
this.strategies = strategies;
this.paymentRepository = paymentRepository;
this.eventPublisher = eventPublisher;
}
public PaymentResult processPayment(String method, Order order) {
PaymentStrategy strategy = strategies.stream()
.filter(s -> s.supports(method))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentMethodException(method));
PaymentResult result = strategy.pay(order);
paymentRepository.save(new PaymentRecord(order.getId(), method, result));
eventPublisher.publish(
new PaymentProcessedEvent(order.getId(), result.getStatus()));
return result;
}
}
Note that Spring automatically injects every bean implementing PaymentStrategy into the constructor's
List<PaymentStrategy> — no manual registration is required. Adding a fifth payment method comes down
to creating a new class annotated with @Component; PaymentProcessor is not touched, and no existing test
is affected.
The REST controller simply delegates to the Context, keeping the presentation layer decoupled from any
business rule:
@RestController
@RequestMapping("/payments")
public class PaymentController {
private final PaymentProcessor paymentProcessor;
public PaymentController(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
}
@PostMapping
public ResponseEntity<PaymentResult> pay(@RequestBody PaymentRequest request) {
PaymentResult result = paymentProcessor.processPayment(
request.method(), request.toOrder());
return ResponseEntity.ok(result);
}
*2.5 Pros and Cons *
Advantages
● Open/Closed Principle compliance: new payment methods are added by extension (a new class),
not by modifying the Context.
● Real testability: each strategy is tested in isolation with trivial mocks of its respective HTTP
client/SDK; the Context is tested with fake strategies, without depending on any external gateway.
● Elimination of cyclomatic complexity: the 900-line class with multiple if/else branches was replaced
by five small, cohesive classes, each with a single responsibility (SRP).
● Runtime flexibility: the strategy used depends on an input value (the payment method), not on
conditional compilation or reflection.
Costs and trade-offs
● More classes to manage: a small project with only two payment methods may not justify the
indirection — the pattern pays off once the variation is expected to grow.
● The supports(method) method decentralizes selection logic; if each strategy decides on its own
when it applies, discipline (or contract tests) is needed to prevent two strategies from claiming the
same method simultaneously.
● The API client still needs to supply a correct method identifier; Strategy does not by itself solve
routing — it solves execution once routing has already been decided.
● The indirection overhead (one extra interface call) is technically measurable but irrelevant next to
the network latency of any external payment gateway — it is not a real argument against the
pattern in this scenario.
*3. Conclusion
3.1 Summary *
The Strategy pattern resolved the architectural bottleneck by replacing a monolithic class coupled to multiple
payment gateways with a set of independent strategies orchestrated by a lean Context. The gain is not merely
cosmetic: it is a direct reduction in the cost and risk of evolving the system, measurable in the amount of
code changed and the test surface affected by each new integration.
3.2 Personal Take
The most valuable part of implementing this refactor was not the pattern itself — it is one of the simplest in
the GoF catalog — but the exercise of identifying exactly where behavioral variation was genuinely,
unnecessarily coupled to the Context. A practical warning is worth stating: Strategy should not be applied
preemptively to every if/else. It pays off when there is concrete evidence that the family of algorithms will
grow or has already changed more than once — applying it too early, in a still-unstable domain, only adds
indirection without return.
*3.3 Over to You *
Have you ever dealt with a "do-everything" class that grew until it became a risk with every deploy? What
strategy (pun intended) did you use to decouple that logic? Share in the comments — I'd like to know whether
you reached for Strategy, Chain of Responsibility, or another approach.

Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.