Inversion of Control and Dependency Injection
Inversion of Control (IoC) functions as an architectural principle rather than a concrete implementation pattern. Its primary objective is to decouple object creation and dependency resolution from business logic by delegating these responsibilities to a dedicated container. Dependency Injection (DI) serves as the practical mechanism that fulfills the IoC principle. Instead of components manually instantiating their collaborators using the new operator, the container resolves required dependencies and injects them at runtime. This shifts dependency acquisition from an active, hardcoded process to a passive, container-managed workflow, dramatically reducing tight coupling and simplifying lifecycle management across complex service layers.
Factory Pattern
Spring abstracts component instantiation through factory implementations. The core BeanFactory interface provides lazy initialization, meaning beans are only constructed when explicitly requested. This approach conserves memory and accelerates application startup. ApplicationContext extends this foundation by eagerly instantiating all singleton beans during context bootstrap. It also introduces enterprise-grade capabilities such as event broadcasting, internationalization, and resource abstraction. Common implementations include ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, and XmlWebApplicationContext.
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class ContainerBootstrap {
public static void main(String[] args) {
String configPath = "C:/app/config/service-registry.xml";
ApplicationContext registry = new FileSystemXmlApplicationContext(configPath);
PaymentProcessor processor = registry.getBean("paymentProcessor", PaymentProcessor.class);
processor.executeTransaction();
}
}
Singleton Pattern
Restricting a class to a single instance prevents resource contention, ensures consistent state, and reduces garbage collection overhead. Spring defaults to the singleton scope for all managed components. Alternative scopes include prototype (fresh instance per injection), request, session, and the legacy global-session. Internally, Spring maintains a bean registry using a thread-safe ConcurrentHashMap. The retrieval mechanism employs synchronization to verify cache presence, instantiate via a factory callback if missing, and register the final instance.
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class BeanRegistry {
private final Map<String, Object> instanceCache = new ConcurrentHashMap<>(128);
private static final Object PLACEHOLDER = new Object();
public Object resolveInstance(String identifier, InstanceFactory factory) {
synchronized (this.instanceCache) {
Object cached = this.instanceCache.get(identifier);
if (cached == null) {
Object newInstance = factory.create();
registerInstance(identifier, newInstance);
return newInstance;
}
return (cached != PLACEHOLDER) ? cached : null;
}
}
private void registerInstance(String identifier, Object instance) {
synchronized (this.instanceCache) {
this.instanceCache.put(identifier, (instance != null) ? instance : PLACEHOLDER);
}
}
@FunctionalInterface
public interface InstanceFactory {
Object create();
}
}
Proxy Pattern
Aspect-Oriented Programming (AOP) in Spring relies heavily on dynamic proxies to intercept method executions and apply cross-cutting concerns. When a target component implements an interface, Spring leverages JDK dynamic proxies. For concrete classes without interfaces, it falls back to CGLIB to generate a subclass at runtime. This proxy layer enables declarative application of transactions, security checks, and logging. While Spring AOP performs runtime weaving, AspectJ operates at compile-time or load-time via bytecode manipulation, offering deeper interception capabilities at the cost of increased configuration complexity.
Template Method Pattern
This behavioral pattern outlines an algorithm's skeleton in a base class while delegating specific steps to subclasses or callbacks. Spring applies this extensively in its data access modules (e.g., JdbcTemplate, HibernateTemplate). Rather than forcing rigid inheritance hierarchies, Spring combines the template method with callback interfaces. The template handles resource acquisition, exception translation, and connection cleanup, while the callback executes the domain-specific data operation.
public abstract class DataAccessTemplate {
public final <T> T executeQuery(DataCallback<T> callback) {
Connection conn = null;
try {
conn = acquireConnection();
return callback.doInDatabase(conn);
} catch (Exception e) {
throw translateException(e);
} finally {
releaseConnection(conn);
}
}
protected abstract Connection acquireConnection();
protected abstract RuntimeException translateException(Exception e);
protected abstract void releaseConnection(Connection conn);
@FunctionalInterface
public interface DataCallback<T> {
T doInDatabase(Connection conn) throws Exception;
}
}
Observer Pattern
Spring’s event publishing mechanism is a direct implementation of the Observer pattern, effectively decoupling event producers from consumers. The architecture comprises three core components: ApplicationEvent (the payload), ApplicationListener (the consumer), and ApplicationEventPublisher (the broadcaster). The framework provides built-in lifecycle events like ContextRefreshedEvent and ContextClosedEvent. Under the hood, ApplicationEventMulticaster handles the actual dispatching to registered listeners.
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 1. Event Definition
public class InventoryUpdateEvent extends ApplicationEvent {
private final String sku;
private final int quantity;
public InventoryUpdateEvent(Object source, String sku, int quantity) {
super(source);
this.sku = sku;
this.quantity = quantity;
}
public String getSku() { return sku; }
public int getQuantity() { return quantity; }
}
// 2. Listener Implementation
@Component
public class SearchIndexRebuilder implements ApplicationListener<InventoryUpdateEvent> {
@Override
public void onApplicationEvent(InventoryUpdateEvent event) {
System.out.println("Rebuilding index for SKU: " + event.getSku());
}
}
// 3. Publisher Component
@Component
public class InventoryService {
@Autowired
private ApplicationContext context;
public void updateStock(String sku, int qty) {
context.publishEvent(new InventoryUpdateEvent(this, sku, qty));
}
}
Adapter Pattern
The Adapter pattern bridges incompatible interfaces, allowing disparate components to collaborate. In Spring MVC, DispatcherServlet delegates request handling to HandlerAdapter. This eliminates cumbersome type-checking logic against various controller implementations, strictly adhering to the Open-Closed Principle. Similarly, Spring AOP uses AdvisorAdapter to translate different advice types (before, after, throws) into a unified MethodInterceptor chain, standardizing how cross-cutting logic is woven into the execution flow.
public interface RequestHandlerAdapter {
boolean supports(Object handler);
ModelAndView handleRequest(Object handler, HttpServletRequest req, HttpServletResponse res) throws Exception;
}
// Avoids rigid if-else chains in the dispatcher
public class AnnotationControllerAdapter implements RequestHandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler.getClass().isAnnotationPresent(RestController.class);
}
@Override
public ModelAndView handleRequest(Object handler, HttpServletRequest req, HttpServletResponse res) {
// Route to annotated method
return null;
}
}
Decorator Pattern
This structural pattern attaches additional responsibilities to an object dynamically without altering its underlying code. Spring utilizes wrappers extensively, particularly around DataSource implementations. By wrapping a core data source, the framework can transparently inject connection pooling, dynamic routing, or performance monitoring capabilities. This approach offers significantly greater flexibility than static inheritance, mirroring the layered design found in Java’s standard I/O stream libraries.