Definition
- Strategy pattern – defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.
- AuthenticationStrategy – the common interface declaring
authenticate(AuthRequest)andtype(). - Concrete strategies –
PasswordAuthStrategy,OtpAuthStrategy,CaptchaAuthStrategy, each fully self-contained. - Runtime selection – the request carries an
authTypefield; the handler looks up the matching strategy from an injectedMap. - Open/Closed – the handler depends only on the interface and the map, never on concrete strategies, so a new method is a new class with zero code changes.
National Bank of India has one login desk. Depending on what Priya presents, the clerk follows a different verification procedure: a password check, an OTP check, or a CAPTCHA check. The desk itself never changes; it picks the matching procedure based on a label on the request. When the bank adds face recognition later, it trains one new procedure and slots it in without rebuilding the desk.
Understanding the Concepts
What is the Strategy pattern for authentication?
The Strategy pattern isolates each authentication algorithm into its own class. A common interface defines the contract, and the correct strategy is selected at runtime based on a type field in the request.
package com.bank.auth;
public interface AuthenticationStrategy {
AuthResult authenticate(AuthRequest request);
String type();
}
How do you implement each login method?
Each method is a concrete strategy. Password verifies credentials against the store, OTP verifies a one-time code, and CAPTCHA verifies a token plus credentials.
package com.bank.auth;
import org.springframework.stereotype.Component;
@Component("PASSWORD")
public class PasswordAuthStrategy implements AuthenticationStrategy {
@Override
public AuthResult authenticate(AuthRequest request) {
// Verify username + password against user store
boolean valid = userStore.verify(
request.getUsername(), request.getPassword());
return new AuthResult(valid, "PASSWORD");
}
@Override
public String type() { return "PASSWORD"; }
}
@Component("OTP")
public class OtpAuthStrategy implements AuthenticationStrategy {
@Override
public AuthResult authenticate(AuthRequest request) {
// Verify one-time code against OTP service
boolean valid = otpService.verify(
request.getPhone(), request.getOtp());
return new AuthResult(valid, "OTP");
}
@Override
public String type() { return "OTP"; }
}
@Component("CAPTCHA")
public class CaptchaAuthStrategy implements AuthenticationStrategy {
@Override
public AuthResult authenticate(AuthRequest request) {
// Verify CAPTCHA token + credentials
boolean captchaValid = captchaService.verify(request.getCaptchaToken());
boolean credsValid = userStore.verify(
request.getUsername(), request.getPassword());
return new AuthResult(captchaValid && credsValid, "CAPTCHA");
}
@Override
public String type() { return "CAPTCHA"; }
}
How does the handler select the strategy at runtime?
The login request carries an authType field. The handler reads it and looks up the matching strategy from a Spring-injected Map.
package com.bank.controller;
import com.bank.auth.AuthenticationStrategy;
import com.bank.auth.AuthRequest;
import com.bank.auth.AuthResult;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class LoginController {
private final Map<String, AuthenticationStrategy> strategies;
public LoginController(Map<String, AuthenticationStrategy> strategies) {
this.strategies = strategies;
}
@PostMapping("/api/login")
public AuthResult login(@RequestBody AuthRequest request) {
String type = request.getAuthType().toUpperCase();
AuthenticationStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException(
"Unsupported auth type: " + type);
}
return strategy.authenticate(request);
}
}
Spring populates the map automatically from all beans implementing the interface. The key is the bean name, so @Component("OTP") creates the map entry "OTP" -> OtpAuthStrategy.
How do you add a new login method?
Write one new class. Spring adds it to the map automatically. The handler dispatches to it with zero code changes.
package com.bank.auth;
import org.springframework.stereotype.Component;
@Component("FACE")
public class FaceAuthStrategy implements AuthenticationStrategy {
@Override
public AuthResult authenticate(AuthRequest request) {
boolean valid = faceRecognitionService.verify(
request.getUserId(), request.getFaceData());
return new AuthResult(valid, "FACE");
}
@Override
public String type() { return "FACE"; }
}
// The handler now dispatches FACE requests automatically
// Zero changes to LoginController, zero changes to existing strategies
How does this satisfy the Open/Closed Principle?
The controller and endpoint depend only on the interface and the injected map, never on concrete strategies. Adding a new method is adding a class, not editing existing code.
// Open for extension: add FaceAuthStrategy.java
// Closed for modification: LoginController is untouched
// Existing strategies (PASSWORD, OTP, CAPTCHA) are untouched
// No regression risk in tested paths
What are the common mistakes?
The first mistake is putting a switch statement in the handler to select the strategy. This defeats the purpose because adding a new method requires editing the switch.
// Wrong: switch statement couples handler to concrete strategies
switch (request.getAuthType()) {
case "PASSWORD" -> new PasswordAuthStrategy().authenticate(request);
case "OTP" -> new OtpAuthStrategy().authenticate(request);
// Must edit this switch for every new method
}
// Correct: map lookup, no switch
strategies.get(request.getAuthType()).authenticate(request);
The second mistake is making strategies stateful. Each strategy must be stateless so the single shared Spring bean is thread-safe under concurrent logins.
Where should cross-cutting concerns go?
Cross-cutting concerns like rate limiting, audit logging, and security headers belong around the dispatch, not inside each strategy. This avoids duplication.
@PostMapping("/api/login")
public AuthResult login(@RequestBody AuthRequest request) {
rateLimiter.check(request.getIp()); // cross-cutting
AuthResult result = strategies
.get(request.getAuthType().toUpperCase())
.authenticate(request);
auditLog.record(request.getAuthType(), result); // cross-cutting
return result;
}
How do you handle unknown auth types securely?
Reject unknown types with a clear error but do not leak which strategies exist to an attacker.
if (strategy == null) {
// Do NOT say "OTP, PASSWORD, CAPTCHA are available"
throw new IllegalArgumentException("Unsupported authentication method");
}
What should a senior engineer know?
The bean-name-as-map-key trick eliminates all dispatch logic. If each strategy bean is named exactly after its type key, the injected Map keys line up with the request's authType value, so dispatch is a direct map lookup with no switch anywhere.
At National Bank of India, this is exactly how Pankaj structures the login service. It satisfies the one-endpoint constraint, keeps each method isolated and independently testable, and makes future methods a pure plug-in.
Visual Overview
Login Request { authType: "OTP", phone: "...", otp: "..." }
│
▼
LoginController
│
│ strategies.get("OTP")
▼
┌──────────────────────────────────────────┐
│ Map<String, AuthenticationStrategy> │
│ │
│ "PASSWORD" → PasswordAuthStrategy │
│ "OTP" → OtpAuthStrategy ←── │
│ "CAPTCHA" → CaptchaAuthStrategy │
│ "FACE" → FaceAuthStrategy │
└──────────────────────────────────────────┘
│
│ strategy.authenticate(request)
▼
OtpAuthStrategy → otpService.verify(phone, otp)
│
▼
AuthResult { success: true, method: "OTP" }
Key Takeaway
The constraint in this scenario is strict: one login endpoint, one handler that must not change, and new authentication methods must be added without touching existing code. The clean answer is the Strategy pattern combined with the Open/Closed Principle. An AuthenticationStrategy interface declares authenticate and type, and each login method becomes its own concrete implementation, fully self-contained and independently testable.
The key to not creating a new endpoint is data-driven selection. The login request carries an authType field, and the handler uses that value to pick the matching strategy from a Spring-injected Map. Spring populates the map from every bean implementing the interface, using the bean name as the key. If each strategy bean is named after its type, dispatch is a direct map lookup with no switch statement.
This makes the design closed for modification and open for extension. Adding face recognition next quarter means writing one new FaceAuthStrategy class annotated as a bean named FACE. Spring adds it to the map automatically. The handler dispatches to it with zero code changes, and nothing in the tested password, OTP, or CAPTCHA paths is touched.
The senior details: each strategy must be stateless for thread safety under concurrent logins, cross-cutting concerns like rate limiting and audit belong around the dispatch rather than inside each strategy, and unknown auth types should be rejected without leaking which methods exist. At National Bank of India, this is exactly how Pankaj structures the login service: one endpoint, one handler, and every authentication method is a pure plug-in.
Remember one sentence: the Strategy pattern with a Spring-injected map makes each login method a plug-in class, so adding a new method never touches the endpoint, the handler, or existing strategies.
---