How to Build an Extensible Multi‑Method Login System with Strategy & Factory Patterns in Spring Boot

This article walks through designing a flexible login module that supports password, WeChat, and SMS authentication by applying the Strategy and Factory patterns in a Spring Boot project, showing code examples, project structure, and best‑practice tips for clean, maintainable backend development.

Top Architect
Top Architect
Top Architect
How to Build an Extensible Multi‑Method Login System with Strategy & Factory Patterns in Spring Boot

Requirement Analysis

The login module must support multiple authentication methods (username/password, WeChat QR‑code, SMS verification) and allow new methods (e.g., Alipay) to be added without modifying existing code.

Username/Password : validate encrypted password, check if the account is locked.

WeChat QR‑code : exchange an auth code for an OpenID via the WeChat Open Platform API and verify the OpenID is bound to a system user.

SMS verification : generate a one‑time code, store it (e.g., in Redis), and validate its correctness and expiration.

Using a plain if‑else chain leads to poor extensibility, mixed responsibilities and duplicated logic.

Design Pattern Selection

Combine the Strategy pattern (encapsulate each login algorithm behind a common interface) with the Factory pattern (create the appropriate strategy instance based on the login type). This satisfies the Open‑Closed Principle.

Spring Boot Project Structure

src/main/java/com/example/login
├── config
│   └── StrategyConfig.java          // optional bean name configuration
├── controller
│   └── LoginController.java        // REST endpoint
├── factory
│   └── LoginStrategyFactory.java   // strategy lookup
├── model
│   └── LoginRequest.java          // request DTO
├── service
│   ├── impl
│   │   ├── PasswordLoginStrategy.java
│   │   ├── WechatLoginStrategy.java
│   │   └── SmsLoginStrategy.java
│   └── LoginStrategy.java          // strategy interface
└── Application.java

Strategy Interface

public interface LoginStrategy {
    /** Unique identifier of the login type, e.g. "password", "wechat" */
    String getLoginType();

    /** Execute the login logic. Parameters are passed in a map to keep the interface generic. */
    String execute(Map<String, Object> params);
}

Strategy Implementations

Password Login Strategy

@Service
public class PasswordLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() { return "password"; }

    @Override
    public String execute(Map<String, Object> params) {
        String username = (String) params.get("username");
        String password = (String) params.get("password");
        // TODO: replace with real DB query and password hash verification
        if (!"123456".equals(password)) {
            throw new IllegalArgumentException("Invalid password");
        }
        checkUserLocked(username);
        return "Login successful (username/password)";
    }

    private void checkUserLocked(String username) {
        // Placeholder for account‑lock check (e.g., call user service)
        System.out.println("Check if user " + username + " is locked");
    }
}

WeChat QR‑code Strategy

@Service
public class WechatLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() { return "wechat"; }

    @Override
    public String execute(Map<String, Object> params) {
        String authCode = (String) params.get("authCode");
        String openId = callWechatApi(authCode);
        String userId = getUserIdByOpenId(openId);
        if (userId == null) {
            throw new IllegalArgumentException("WeChat account not bound to a system user");
        }
        return "Login successful (WeChat QR‑code)";
    }

    private String callWechatApi(String authCode) {
        // Real implementation should invoke WeChat Open Platform API
        System.out.println("Calling WeChat API with authCode=" + authCode);
        return "wechat_open_id_123"; // mock OpenID
    }

    private String getUserIdByOpenId(String openId) {
        // Mock DB lookup – replace with actual persistence query
        return "user123";
    }
}

SMS Verification Strategy

@Service
public class SmsLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() { return "sms"; }

    @Override
    public String execute(Map<String, Object> params) {
        String phone = (String) params.get("phone");
        String code = (String) params.get("code");
        // TODO: retrieve the real code from Redis or another store
        if (!"666888".equals(code)) {
            throw new IllegalArgumentException("Invalid verification code");
        }
        checkPhoneRegistered(phone);
        return "Login successful (SMS verification)";
    }

    private void checkPhoneRegistered(String phone) {
        // Placeholder for phone‑registration check
        System.out.println("Check if phone " + phone + " is registered");
    }
}

Factory Implementation

@Component
public class LoginStrategyFactory {
    private final Map<String, LoginStrategy> strategyMap;

    public LoginStrategyFactory(Map<String, LoginStrategy> beans) {
        // Re‑map beans by the value returned from getLoginType()
        this.strategyMap = new HashMap<>();
        beans.forEach((beanName, strategy) ->
            this.strategyMap.put(strategy.getLoginType(), strategy));
    }

    public LoginStrategy getStrategy(String loginType) {
        LoginStrategy strategy = strategyMap.get(loginType);
        if (strategy == null) {
            throw new IllegalArgumentException("Unsupported login type: " + loginType);
        }
        return strategy;
    }
}

DTO for Login Requests

public class LoginRequest {
    /** e.g. "password", "wechat", "sms" */
    private String loginType;
    /** Method‑specific parameters */
    private Map<String, Object> params;
    // getters and setters omitted for brevity
}

Controller Integration

@RestController
@RequestMapping("/login")
public class LoginController {
    private final LoginStrategyFactory factory;

    @Autowired
    public LoginController(LoginStrategyFactory factory) {
        this.factory = factory;
    }

    @PostMapping
    public String login(@RequestBody LoginRequest request) {
        // Basic validation of request fields can be added here
        LoginStrategy strategy = factory.getStrategy(request.getLoginType());
        return strategy.execute(request.getParams());
    }
}

Testing the Three Login Types

Example JSON payloads sent to /login:

{
  "loginType": "password",
  "params": {"username": "user123", "password": "123456"}
}
{
  "loginType": "wechat",
  "params": {"authCode": "wechat_auth_code_456"}
}
{
  "loginType": "sms",
  "params": {"phone": "13800138000", "code": "666888"}
}

Extending the Module

To add a new authentication method (e.g., Alipay), create a class AlipayLoginStrategy that implements LoginStrategy and annotate it with @Service (or define a bean in StrategyConfig). No changes are required in the factory or controller.

@Service
public class AlipayLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() { return "alipay"; }

    @Override
    public String execute(Map<String, Object> params) {
        // Implement Alipay specific login logic here
        return "Login successful (Alipay)";
    }
}

Core Advantages

Strategy pattern : each login algorithm lives in its own class, improving readability and testability.

Open‑Closed Principle : new login types are added without touching existing code.

Factory pattern : hides creation details; callers only need the login type identifier.

Spring integration : automatic injection of all LoginStrategy beans via a Map<String, LoginStrategy> simplifies lifecycle management.

Best Practices

Parameter validation : perform required‑field checks in the controller to avoid duplicated validation in each strategy.

Extract common logic : if multiple strategies share steps (e.g., token generation), create an abstract base class that implements LoginStrategy and let concrete strategies extend it.

Logging and exception handling : wrap strategy execution with try‑catch, log failures, and translate technical exceptions into business‑level error messages.

Configuration‑driven login types : optionally list supported loginType values in application.yml so the front‑end can query the list instead of hard‑coding.

Javastrategy patternSpring BootloginFactory Pattern
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.