Mastering Java Design Patterns: From High Cohesion to Low Coupling in Spring

This article explains the core concepts of high cohesion and low coupling, introduces common Java design patterns, demonstrates how Spring applies these patterns such as Singleton, Factory, Proxy, Observer, Chain of Responsibility, and Template Method, and provides practical code examples for real‑world scenarios.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Mastering Java Design Patterns: From High Cohesion to Low Coupling in Spring
所谓 "解耦",就是代码和代码说分手。

Background

When discussing design patterns, you might wonder: what are they useful for? Even if you haven't consciously used them, they may already be present in your projects. Java has 23 design patterns, but only a few are frequently used in daily work. This chapter focuses on the most common patterns, explaining their concepts, contracts, and practical applications to benefit both interview preparation and real‑world development.

Article Outline

Design Philosophy

For Java developers, a basic programmer's creed can be summarized as:

Expose when needed, hide when needed

Encapsulate when needed

Everything is an object

Code must be robust

We can condense this into two core keywords: High Cohesion and Low Coupling

As a child I watched the animated "Nezha" where the hero has three heads and eight arms, symbolizing static and dynamic features. Mapping these ideas: Static features correspond to the three heads and eight arms, while Dynamic features correspond to the countless battles.

The concept of High Cohesion is illustrated above; the next question is what Low Coupling looks like.

If I were to write a myth called "The Legend of Nezha", I would give him three heads and nine arms, but ensure that adding a new weapon never affects the existing eight arms—this is the essence of low coupling, which also leads to robust and extensible code.

The ultimate design principle for patterns is to

encapsulate the changing parts while preserving the stable ones

Design Principles

Design principles can be grouped into two major categories: Open‑Closed Principle (Liskov substitution, composition over inheritance, dependency inversion)

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, a component can change its behavior without altering its source code.

Single Responsibility (Interface Segregation, Law of Demeter)

A class should have only one responsibility and therefore only one reason to change.

Design Patterns in Spring

Spring extensively uses design patterns to provide powerful and flexible features. Below are several common patterns found in Spring's source code.

1. Singleton Pattern

In Spring, a Bean is singleton by default. The IoC container creates the instance and ensures that each Bean ID is instantiated only once. The class DefaultSingletonBeanRegistry manages singleton Beans.

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    // ...
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    // ...
    @Override
    public Object getSingleton(String beanName) {
        return getSingleton(beanName, true);
    }
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // ... omitted code
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
    // ...
}

2. Factory Pattern

Spring uses the factory pattern via BeanFactory and ApplicationContext interfaces to create and manage Bean objects. DefaultListableBeanFactory is the concrete implementation.

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
    // ...
    @Override
    public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        return doGetBean(name, requiredType, null, false);
    }
    @Override
    public <T> T getBean(Class<T> requiredType) throws BeansException {
        return doGetBean(null, requiredType, null, false);
    }
    // ... omitted code
}

3. Proxy Pattern

Spring AOP is based on the proxy pattern. It creates proxy objects for target beans and weaves aspect logic into them. Core classes are JdkDynamicAopProxy and CglibAopProxy.

public class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
    // ...
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // ... omitted code
        final AdvisedSupport advised = this.advised;
        // ...
        List<Object> chain = advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        // ...
        return invokeJoinpointUsingReflection(target, method, args, targetClass, chain);
    }
    // ...
}

4. Observer Pattern (Event Listener)

Spring's event handling mechanism implements the observer pattern. When an event occurs, all registered listeners are notified.

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    // ...
    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            // invoke listener
            invokeListener(listener, event);
        }
    }
    private void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
        ErrorHandler errorHandler = getErrorHandler();
        if (errorHandler != null) {
            try {
                doInvokeListener(listener, event);
            } catch (Throwable err) {
                errorHandler.handleError(err);
            }
        } else {
            doInvokeListener(listener, event);
        }
    }
    private void doInvokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
        try {
            listener.onApplicationEvent(event);
        } catch (ClassCastException ex) {
            // omitted handling
        }
    }
    // ...
}

5. Chain of Responsibility

Spring uses the chain of responsibility for request interception via HandlerInterceptor and HandlerInterceptorAdapter. Each interceptor processes the request in order until one returns false.

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
    void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
    void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}

public class CustomInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // custom pre‑handle logic
        return true; // continue chain
    }
    // ... other methods
}

6. Template Method

Classes such as JdbcTemplate and HibernateTemplate are classic examples of the template method pattern. They define the skeleton of an algorithm while allowing subclasses to customize specific steps.

public class JdbcTemplate extends JdbcAccessor implements JdbcOperations, BeanFactoryAware {
    // ...
    public <T> T query(String sql, RowMapper<T> rowMapper) {
        return query(sql, new Object[0], rowMapper);
    }
    public <T> T query(String sql, Object[] args, RowMapper<T> rowMapper) {
        return query(sql, args, rowMapper, true);
    }
    // ... actual SQL execution and result‑set handling omitted
}

public class CustomRowMapper implements RowMapper<MyObject> {
    @Override
    public MyObject mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new MyObject(/* mapping logic */);
    }
}

Practical Application

Assume the following business requirements:

User login and authentication

Purchase product and create order

Validate order legality

Record request parameters

Send SMS notification after order confirmation

These can be split into two flows: User Process and Order Process.

User Login Process

The login flow often uses interceptors for authentication checks and logging, employing several design patterns.

Scenario 1 – User Authentication

Chain of Responsibility

Interceptors are executed in order; each decides whether to continue.

public interface Interceptor {
    boolean intercept(AuthenticationContext context);
}

public class UserValidationInterceptor implements Interceptor {
    @Override
    public boolean intercept(AuthenticationContext context) {
        if (isValidUser(context.getUser())) {
            return true;
        }
        return false;
    }
    private boolean isValidUser(User user) {
        // validation logic
        return true;
    }
}

public class AuthorizationInterceptor implements Interceptor {
    @Override
    public boolean intercept(AuthenticationContext context) {
        if (isAuthorized(context.getUser(), context.getCredentials())) {
            return true;
        }
        return false;
    }
    private boolean isAuthorized(User user, Credentials credentials) {
        // authorization logic
        return true;
    }
}

public class InterceptorChain {
    private List<Interceptor> interceptors = new ArrayList<>();
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }
    public boolean execute(AuthenticationContext context) {
        for (Interceptor interceptor : interceptors) {
            if (!interceptor.intercept(context)) {
                // stop chain
                return false;
            }
        }
        return true;
    }
}

Scenario 2 – Record User Login Info

Singleton Pattern

Loggers are typically singletons to ensure a single global instance.

Logger logger = LoggerFactory.getLoggerFactoryInstance().getLogger();
logger.log("This is a user log message");
Order Process

The core order flow is illustrated below:

Scenario 3 – Order Validation

Factory Pattern

An ICheckOrderService interface defines validation, with two implementations: CountCheckOrder and ParamCheckOrder. A CheckOrderFactory creates the appropriate service.

public interface ICheckOrderService {
    boolean checkOrder(Object order);
    String getErrorMessage();
}

public class CountCheckOrder implements ICheckOrderService {
    @Override
    public boolean checkOrder(Object order) {
        int quantity = ((Order) order).getQuantity();
        return quantity > 0;
    }
    @Override
    public String getErrorMessage() {
        return "Purchase quantity must be greater than 0.";
    }
}

public class ParamCheckOrder implements ICheckOrderService {
    @Override
    public boolean checkOrder(Object order) {
        // parameter validation logic
        return true;
    }
    @Override
    public String getErrorMessage() {
        return "Order parameter validation failed.";
    }
}

public class CheckOrderFactory {
    public static ICheckOrderService createCheckOrderService(String type) {
        switch (type) {
            case "count":
                return new CountCheckOrder();
            case "param":
                return new ParamCheckOrder();
            default:
                throw new IllegalArgumentException("Unsupported validation type: " + type);
        }
    }
}

Scenario 4 – SMS Notification

Observer Pattern (Event Listener)

Spring's ApplicationEvent and ApplicationListener are used to publish a PurchaseSuccessEvent and send an SMS.

public class PurchaseSuccessEvent extends ApplicationEvent {
    private final String buyerPhoneNumber;
    private final String orderId;
    public PurchaseSuccessEvent(Object source, String buyerPhoneNumber, String orderId) {
        super(source);
        this.buyerPhoneNumber = buyerPhoneNumber;
        this.orderId = orderId;
    }
    public String getBuyerPhoneNumber() { return buyerPhoneNumber; }
    public String getOrderId() { return orderId; }
}
@Component
public class SmsNotificationListener implements ApplicationListener<PurchaseSuccessEvent> {
    @Override
    public void onApplicationEvent(PurchaseSuccessEvent event) {
        String message = "Dear buyer, your order " + event.getOrderId() + " was successful!";
        sendSms(event.getBuyerPhoneNumber(), message);
    }
    private void sendSms(String phoneNumber, String message) {
        // SMS sending logic
        System.out.println("Sending SMS to " + phoneNumber + ": " + message);
    }
}
@Configuration
public class AppConfig {
    @Bean
    public ApplicationEventPublisher applicationEventPublisher() {
        return new GenericApplicationContext();
    }
}
@Service
public class PurchaseService {
    private final ApplicationEventPublisher applicationEventPublisher;
    @Autowired
    public PurchaseService(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
    public void completePurchase(String buyerPhoneNumber, String orderId) {
        // business logic for completing purchase
        applicationEventPublisher.publishEvent(new PurchaseSuccessEvent(this, buyerPhoneNumber, orderId));
    }
}

Summary

Use design patterns to encapsulate change while preserving stability.

Benefits: improved extensibility, cleaner and more robust code.

Understanding how open‑source frameworks apply patterns enhances architectural insight.

Avoid over‑design; readability should not be sacrificed for pattern usage.

Think like a builder of a cathedral, not just a stone cutter.

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.

Design PatternsJavaSoftware ArchitectureBackend DevelopmentSpring FrameworkLow CouplingHigh Cohesion
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.