Fundamentals 27 min read

Master 9 Essential Design Patterns for Cleaner Java Code

This article explains nine widely used design patterns—Singleton, Builder, Factory, Strategy, Template Method, Chain of Responsibility, Proxy, Adapter, and Observer—detailing their concepts, typical use‑cases, and concrete Java code examples drawn from popular open‑source projects such as Spring, Dubbo, MyBatis and Guava.

macrozheng
macrozheng
macrozheng
Master 9 Essential Design Patterns for Cleaner Java Code

Ever struggled with unreadable legacy code full of long methods, tangled if‑else branches, and no one to ask for clarification? This guide shows how to refactor such code using nine common design patterns, providing clear explanations and real‑world source code snippets from open‑source projects.

Singleton Pattern

The Singleton pattern ensures a class has only one instance within a process (or within a Spring container). Two popular implementations are the eager static constant and the lazy double‑checked locking.

1. Static Constant (eager)

Used frequently in Spring, e.g.,

AnnotationBeanNameGenerator

for bean name generation.

2. Double‑Checked Locking (lazy)

Common in interviews and open‑source code. The implementation uses a volatile instance and synchronizes only on the first creation.

<code>public class Singleton {
    private volatile static Singleton INSTANCE;
    private Singleton() {}
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
</code>

The outer null check reduces synchronization overhead, while the inner check prevents multiple creations. The

volatile

keyword avoids instruction reordering that could expose a partially constructed object.

Double‑Checked Locking in Dubbo

Dubbo’s SPI mechanism also employs this pattern to guarantee a single instance.

Builder Pattern

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is especially useful when an object has many parameters.

<code>PersonDTO personDTO = PersonDTO.builder()
        .name("三友的java日记")
        .age(18)
        .sex(1)
        .phone("188****9527")
        .build();
</code>

Spring’s

BeanDefinitionBuilder

, Guava’s

CacheBuilder

, and JDK’s

StringBuilder

are real‑world examples.

Factory Pattern

Factory patterns encapsulate object creation. Three variants are covered:

Simple Factory

Factory Method

Abstract Factory

Simple Factory

<code>public class SimpleAnimalFactory {
    public Animal createAnimal(String animalType) {
        if ("cat".equals(animalType)) {
            Cat cat = new Cat();
            // complex initialization
            return cat;
        } else if ("dog".equals(animalType)) {
            Dog dog = new Dog();
            // complex initialization
            return dog;
        } else {
            throw new RuntimeException("animalType=" + animalType + " cannot be created");
        }
    }
}
</code>

Factory Method

Introduces an

AnimalFactory

interface so each concrete animal has its own factory, adhering to the Open/Closed Principle.

<code>public interface AnimalFactory {
    Animal createAnimal();
}
public class CatFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        Cat cat = new Cat();
        // complex initialization
        return cat;
    }
}
public class DogFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        Dog dog = new Dog();
        // complex initialization
        return dog;
    }
}
</code>

Abstract Factory

Extends the idea to create families of related products, e.g., animals and their food.

<code>public interface AnimalFactory {
    Animal createAnimal();
    Food createFood();
}
</code>

MyBatis’s

SqlSessionFactory

and Spring’s

BeanFactory

are practical applications.

Strategy Pattern

Encapsulates interchangeable algorithms. The article shows a messaging example where different notification channels (SMS, App, Email) are implemented as separate strategies.

<code>public interface MessageNotifier {
    boolean support(int notifyType);
    void notify(User user, String content);
}
@Component
public class SMSMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int notifyType) { return notifyType == 0; }
    @Override
    public void notify(User user, String content) { /* send SMS */ }
}
public class AppMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int notifyType) { return notifyType == 1; }
    @Override
    public void notify(User user, String content) { /* send App push */ }
}
// usage
@Resource
private List<MessageNotifier> messageNotifiers;
public void notifyMessage(User user, String content, int type) {
    for (MessageNotifier n : messageNotifiers) {
        if (n.support(type)) { n.notify(user, content); break; }
    }
}
</code>

Spring MVC’s

HandlerMethodArgumentResolver

and

HandlerMethodReturnValueHandler

are concrete strategy implementations.

Template Method Pattern

Defines the skeleton of an algorithm in a base class while allowing subclasses to override specific steps. Examples include

HashMap

’s

afterNodeInsertion

, Spring’s

ApplicationContext.refresh

, and MyBatis’s

BaseExecutor

workflow.

Chain of Responsibility Pattern

Links handlers in a chain so a request traverses until a handler processes it. The article provides a leave‑approval example and shows its use in Spring MVC interceptors, Sentinel slots, and MyBatis executor chaining.

Proxy Pattern

Provides a surrogate object that controls access to the real subject. Examples include a logging proxy for a service, Spring AOP, MyBatis’s

CachingExecutor

proxying a

SimpleExecutor

, and adapter usage for logging frameworks.

Adapter Pattern

Converts one interface to another so incompatible classes can work together. The tutorial shows a USB‑Type‑C to Micro‑USB adapter and explains MyBatis’s logging abstraction that adapts to various logging frameworks.

Observer Pattern

Establishes a one‑to‑many dependency so that when the subject changes, all observers are notified. Spring’s event mechanism and typical fire‑alarm examples illustrate the pattern.

Conclusion

The nine patterns—Singleton, Builder, Factory, Strategy, Template Method, Chain of Responsibility, Proxy, Adapter, and Observer—are not only prevalent in open‑source frameworks but also valuable tools for everyday software development.

design patternsJavaSpringsingletonstrategyFactoryObserverBuilder
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

login 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.