Design Patterns in Java: Strategy, Chain, Template, Observer & Singleton

This article explores six classic design patterns—Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—detailing their real‑world business scenarios, core principles, Java implementations with Spring integration, and how they improve code maintainability, extensibility, and adherence to SOLID principles.

macrozheng
macrozheng
macrozheng
Design Patterns in Java: Strategy, Chain, Template, Observer & Singleton

Usually we write code in a linear pipeline fashion to achieve business logic, but to make coding enjoyable we can use design patterns to optimize our business code. Below are common design patterns and how to apply them in daily work.

1. Strategy Pattern

1.1 Business Scenario

Assume a big‑data system pushes files and, based on file type, different parsing methods are needed. Many developers write code like:

if(type=="A"){
    // parse as A format
}
else if(type=="B"){
    // parse as B format
}
else{
    // default parsing
}

This code can become bloated, hard to maintain, and violates the Open/Closed Principle and Single Responsibility Principle.

If branches increase, the code becomes bulky and unreadable.

Adding a new parsing type requires modifying existing code.

When you have multiple if...else branches that can be encapsulated and swapped, the Strategy pattern is a good fit.

1.2 Strategy Pattern Definition

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from the clients that use it.

Imagine dating different personalities; you need different strategies—movie, snack, shopping—to win their hearts. The strategies are interchangeable, but the goal remains the same.

1.3 Using Strategy Pattern

Implementation steps:

Define an interface or abstract class with two methods (type matching and executable logic).

Create concrete strategy classes implementing the interface.

Use the strategy via a map initialized by Spring.

1.3.1 Interface with Two Methods

public interface IFileStrategy {
    FileTypeResolveEnum gainFileType();
    void resolve(Object objectParam);
}

1.3.2 Concrete Strategies

A type strategy implementation:

@Component
public class AFileResolve implements IFileStrategy {
    @Override
    public FileTypeResolveEnum gainFileType() {
        return FileTypeResolveEnum.File_A_RESOLVE;
    }
    @Override
    public void resolve(Object objectParam) {
        logger.info("A type parsing file, param: {}", objectParam);
        // A type specific logic
    }
}

B type strategy implementation:

@Component
public class BFileResolve implements IFileStrategy {
    @Override
    public FileTypeResolveEnum gainFileType() {
        return FileTypeResolveEnum.File_B_RESOLVE;
    }
    @Override
    public void resolve(Object objectParam) {
        logger.info("B type parsing file, param: {}", objectParam);
        // B type specific logic
    }
}

Default strategy implementation:

@Component
public class DefaultFileResolve implements IFileStrategy {
    @Override
    public FileTypeResolveEnum gainFileType() {
        return FileTypeResolveEnum.File_DEFAULT_RESOLVE;
    }
    @Override
    public void resolve(Object objectParam) {
        logger.info("Default type parsing file, param: {}", objectParam);
        // Default parsing logic
    }
}

1.3.3 Using Strategy Pattern

Leverage Spring's lifecycle and ApplicationContextAware to populate a map of strategies, then call resolveFile to execute the appropriate strategy.

@Component
public class StrategyUseService implements ApplicationContextAware {
    private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, IFileStrategy> tempMap = applicationContext.getBeansOfType(IFileStrategy.class);
        tempMap.values().forEach(service -> iFileStrategyMap.put(service.gainFileType(), service));
    }
    public void resolveFile(FileTypeResolveEnum fileTypeResolveEnum, Object objectParam) {
        IFileStrategy strategy = iFileStrategyMap.get(fileTypeResolveEnum);
        if (strategy != null) {
            strategy.resolve(objectParam);
        }
    }
}

2. Chain of Responsibility Pattern

2.1 Business Scenario

Order processing often involves parameter validation, security checks, blacklist checks, rule interception, etc. Using exceptions for flow control leads to problems and violates best practices.

public class Order {
    public void checkNullParam(Object param) { throw new RuntimeException(); }
    public void checkSecurity() { throw new RuntimeException(); }
    public void checkBackList() { throw new RuntimeException(); }
    public void checkRule() { throw new RuntimeException(); }
    public static void main(String[] args) {
        Order order = new Order();
        try {
            order.checkNullParam();
            order.checkSecurity();
            order.checkBackList();
            order.checkRule();
            System.out.println("order success");
        } catch (RuntimeException e) {
            System.out.println("order fail");
        }
    }
}

Instead, the Chain of Responsibility pattern can decouple these checks.

2.2 Chain of Responsibility Definition

The pattern creates a chain of handler objects, each having a chance to process a request. If a handler processes it, it may pass the request to the next handler or stop.

2.3 Using Chain of Responsibility

Implementation steps:

Define an abstract handler with a reference to the next handler and a template method.

Implement concrete handlers for each validation step.

Initialize the chain (e.g., via Spring) and invoke the first handler.

2.3.1 Abstract Handler

public abstract class AbstractHandler {
    private AbstractHandler nextHandler;
    public void setNextHandler(AbstractHandler nextHandler) { this.nextHandler = nextHandler; }
    public void filter(Request request, Response response) {
        doFilter(request, response);
        if (getNextHandler() != null) {
            getNextHandler().filter(request, response);
        }
    }
    public AbstractHandler getNextHandler() { return nextHandler; }
    protected abstract void doFilter(Request request, Response response);
}

2.3.2 Concrete Handlers

@Component
@Order(1)
public class CheckParamFilterObject extends AbstractHandler {
    @Override
    protected void doFilter(Request request, Response response) {
        System.out.println("Non‑null parameter check");
    }
}

@Component
@Order(2)
public class CheckSecurityFilterObject extends AbstractHandler {
    @Override
    protected void doFilter(Request request, Response response) {
        System.out.println("Security check");
    }
}

@Component
@Order(3)
public class CheckBlackFilterObject extends AbstractHandler {
    @Override
    protected void doFilter(Request request, Response response) {
        System.out.println("Blacklist check");
    }
}

@Component
@Order(4)
public class CheckRuleFilterObject extends AbstractHandler {
    @Override
    protected void doFilter(Request request, Response response) {
        System.out.println("Rule check");
    }
}

2.3.3 Chain Initialization and Usage

@Component("ChainPatternDemo")
public class ChainPatternDemo {
    @Autowired
    private List<AbstractHandler> abstractHandleList;
    private AbstractHandler abstractHandler;
    @PostConstruct
    public void initializeChainFilter() {
        for (int i = 0; i < abstractHandleList.size(); i++) {
            if (i == 0) {
                abstractHandler = abstractHandleList.get(0);
            } else {
                AbstractHandler current = abstractHandleList.get(i - 1);
                AbstractHandler next = abstractHandleList.get(i);
                current.setNextHandler(next);
            }
        }
    }
    public Response exec(Request request, Response response) {
        abstractHandler.filter(request, response);
        return response;
    }
}

Running the demo prints:

Non‑null parameter check
Security check
Blacklist check
Rule check

3. Template Method Pattern

3.1 Business Scenario

Different merchants invoke a third‑party service via HTTP. The process includes querying merchant info, signing the request, sending HTTP, and verifying the response. Some merchants use a proxy, others direct connection.

3.2 Template Method Definition

The pattern defines the skeleton of an algorithm in an abstract class, leaving some steps to subclasses to implement.

Dating analogy: first hold hands, then hug, then kiss— the steps are fixed, but the exact hand used can vary.

3.3 Using Template Method

Create an abstract class with the template method and abstract steps.

Implement common steps in the abstract class.

Let subclasses implement the variable steps.

3.3.1 Abstract Class Skeleton

abstract class AbstractMerchantService {
    abstract void queryMerchantInfo();
    abstract void signature();
    abstract void httpRequest();
    abstract void verifySignature();
    public Resp handlerTemplate(Req req) {
        queryMerchantInfo();
        signature();
        httpRequest();
        verifySignature();
        return resp;
    }
    abstract boolean isRequestByProxy();
}

3.3.2 Concrete Implementations

class CompanyAServiceImpl extends AbstractMerchantService {
    @Override
    public Resp handler(Req req) { return handlerTemplate(req); }
    @Override
    public boolean isRequestByProxy() { return true; }
}

class CompanyBServiceImpl extends AbstractMerchantService {
    @Override
    public Resp handler(Req req) { return handlerTemplate(req); }
    @Override
    public boolean isRequestByProxy() { return false; }
}

4. Observer Pattern

4.1 Business Scenario

After a user registers successfully, the system may need to send IM, SMS, email, etc. Adding new notification types should not modify the registration method, adhering to the Open/Closed Principle.

4.2 Observer Pattern Definition

The pattern defines a one‑to‑many dependency: when the subject changes state, all observers are notified and can react.

4.3 Using Observer Pattern

Implementation steps:

Define an observable class.

Define an observer interface.

Implement concrete observers (IM, SMS, Email).

Optionally use an event bus such as Guava for decoupled publishing.

4.3.1 Observable and Observers

public class Observable {
    private List<Observer> observers = new ArrayList<>();
    private int state;
    public int getState() { return state; }
    public void setState(int state) { this.state = state; notifyAllObservers(); }
    public void addObserver(Observer observer) { observers.add(observer); }
    public void removeObserver(Observer observer) { observers.remove(observer); }
    public void notifyAllObservers() {
        for (Observer o : observers) { o.doEvent(); }
    }
}

interface Observer { void doEvent(); }

class IMMessageObserver implements Observer { public void doEvent() { System.out.println("Send IM message"); } }
class MobileNoObserver implements Observer { public void doEvent() { System.out.println("Send SMS message"); } }
class EmailObserver implements Observer { public void doEvent() { System.out.println("Send Email message"); } }

4.3.2 EventBus (Guava) Example

public class EventBusCenter {
    private static final EventBus eventBus = new EventBus();
    private EventBusCenter() {}
    public static EventBus getInstance() { return eventBus; }
    public static void register(Object obj) { eventBus.register(obj); }
    public static void unregister(Object obj) { eventBus.unregister(obj); }
    public static void post(Object obj) { eventBus.post(obj); }
}

public class EventListener {
    @Subscribe
    public void handle(NotifyEvent event) {
        System.out.println("Send IM: " + event.getImNo());
        System.out.println("Send SMS: " + event.getMobileNo());
        System.out.println("Send Email: " + event.getEmailNo());
    }
}

public class NotifyEvent {
    private String mobileNo;
    private String emailNo;
    private String imNo;
    public NotifyEvent(String mobileNo, String emailNo, String imNo) {
        this.mobileNo = mobileNo;
        this.emailNo = emailNo;
        this.imNo = imNo;
    }
    public String getMobileNo() { return mobileNo; }
    public String getEmailNo() { return emailNo; }
    public String getImNo() { return imNo; }
}

public class EventBusDemoTest {
    public static void main(String[] args) {
        EventListener listener = new EventListener();
        EventBusCenter.register(listener);
        EventBusCenter.post(new NotifyEvent("13372817283", "[email protected]", "666"));
    }
}

5. Factory Pattern

5.1 Business Scenario

Factory pattern is often combined with Strategy to replace long if…else or switch statements when creating objects based on type.

5.2 Using Factory Pattern

Define a factory interface.

Implement concrete factories for each type.

Use the factory to obtain the appropriate object.

5.3.1 Factory Interface

interface IFileResolveFactory { void resolve(); }

5.3.2 Concrete Factories

class AFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("File A type parsing"); } }
class BFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("File B type parsing"); } }
class DefaultFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("Default file type parsing"); } }

5.3.3 Factory Usage

IFileResolveFactory factory;
if (fileType.equals("A")) { factory = new AFileResolve(); }
else if (fileType.equals("B")) { factory = new BFileResolve(); }
else { factory = new DefaultFileResolve(); }
factory.resolve();

6. Singleton Pattern

6.1 Business Scenario

The Singleton pattern ensures a class has only one instance and provides a global access point. Commonly used for I/O, database connections, etc.

6.2 Classic Implementations

Lazy (lazy‑initialization) singleton.

Eager (hungry) singleton.

Double‑checked locking.

Static inner class.

Enum singleton.

6.2.1 Lazy Singleton

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}
    public static synchronized LazySingleton getInstance() {
        if (instance == null) { instance = new LazySingleton(); }
        return instance;
    }
}

6.2.2 Eager Singleton

public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    private EagerSingleton() {}
    public static EagerSingleton getInstance() { return INSTANCE; }
}

6.2.3 Double‑Check Locking

public class DoubleCheckSingleton {
    private static volatile DoubleCheckSingleton instance;
    private DoubleCheckSingleton() {}
    public static DoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) { instance = new DoubleCheckSingleton(); }
            }
        }
        return instance;
    }
}

6.2.4 Static Inner Class

public class InnerClassSingleton {
    private static class Holder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); }
    private InnerClassSingleton() {}
    public static InnerClassSingleton getInstance() { return Holder.INSTANCE; }
}

6.2.5 Enum Singleton

public enum SingletonEnum { INSTANCE; public SingletonEnum getInstance() { return INSTANCE; } }
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSoftware ArchitecturespringSOLID
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.