Skip to main content

Spring Design Patterns

Design patterns provide standardized solutions to common software development problems. The Spring Framework heavily leverages several design patterns to achieve its goals of creating loosely coupled, maintainable, and testable applications. In this guide, we'll explore the key design patterns used in Spring and how they help create better software.

Introduction

The Spring Framework is built on a foundation of robust design patterns that make it powerful, flexible, and widely adopted. Understanding these patterns not only helps you work effectively with Spring but also improves your overall software design skills.

Design patterns in Spring help solve common programming challenges by:

  • Reducing code complexity
  • Promoting code reuse
  • Enhancing application maintainability
  • Supporting better testing practices
  • Providing proven solutions to recurring problems

Let's dive into the most important design patterns used in the Spring ecosystem.

Inversion of Control (IoC) and Dependency Injection (DI)

The Core Pattern of Spring

Inversion of Control is the fundamental design principle of Spring, with Dependency Injection being its primary implementation technique.

What is Inversion of Control?

IoC inverts the flow of control compared to traditional programming. Instead of your application code controlling the creation and lifecycle of objects, an external container (the Spring IoC container) manages these responsibilities.

Traditional approach:

java
public class TraditionalApplication {
public static void main(String[] args) {
// Application creates and manages its dependencies
DatabaseService dbService = new MySQLDatabaseService();
UserService userService = new UserServiceImpl(dbService);
userService.registerUser("john", "password123");
}
}

With IoC:

java
public class SpringApplication {
public static void main(String[] args) {
// Spring creates and manages objects
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.registerUser("john", "password123");
}
}

Dependency Injection in Action

Dependency Injection is a pattern where required objects (dependencies) are provided to a class rather than created by the class itself. Spring supports several forms of DI:

1. Constructor Injection

java
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final EmailService emailService;

@Autowired
public UserServiceImpl(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}

@Override
public void registerUser(String username, String password) {
User user = new User(username, password);
userRepository.save(user);
emailService.sendWelcomeEmail(user);
}
}

2. Setter Injection

java
@Service
public class ProductServiceImpl implements ProductService {
private ProductRepository productRepository;

@Autowired
public void setProductRepository(ProductRepository productRepository) {
this.productRepository = productRepository;
}

// Service methods
}

3. Field Injection

java
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderRepository orderRepository;

@Autowired
private PaymentService paymentService;

// Service methods
}

Benefits of IoC and DI

  • Loose coupling: Classes don't need to know how their dependencies are created
  • Improved testability: Dependencies can be easily mocked or stubbed for unit tests
  • Modular code: Components can be developed and tested in isolation
  • Simplified configuration: Dependencies are managed centrally

Singleton Pattern

Spring's Default Scope

The Singleton pattern ensures that only one instance of a class is created and provides a global point of access to that instance.

Spring Beans are singletons by default, meaning Spring creates exactly one instance of each bean defined in your application context.

java
@Service
public class UserService {
// Spring will create only one instance of this service
}

To verify this behavior:

java
@Component
public class SingletonDemo {
@Autowired
private ApplicationContext context;

public void checkSingleton() {
UserService service1 = context.getBean(UserService.class);
UserService service2 = context.getBean(UserService.class);

System.out.println("Are services the same instance? " + (service1 == service2));
// Output: Are services the same instance? true
}
}

When Singleton is Not Appropriate

In some cases, you might not want singleton behavior. Spring provides other scopes:

java
@Service
@Scope("prototype")
public class NonSingletonService {
// A new instance will be created each time this bean is requested
}

Factory Pattern

Bean Factory and Application Context

Spring implements the Factory pattern through its BeanFactory and ApplicationContext interfaces.

These factories manage the creation of beans, instantiating them when needed rather than at application startup, which is especially useful for resource-intensive objects.

java
// Basic BeanFactory example
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
UserService userService = factory.getBean("userService", UserService.class);

// Application Context (a more advanced factory)
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);

Spring Boot's Auto-Configuration

Spring Boot takes the Factory pattern to another level with auto-configuration. It automatically creates beans based on the classpath, properties, and other conditions:

java
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

With this simple code, Spring Boot configures dozens of beans automatically using the Factory pattern under the hood.

Proxy Pattern

AOP and Transaction Management

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Spring extensively uses proxies to implement Aspect-Oriented Programming (AOP).

A proxy intercepts calls to the target object, allowing additional behavior to be introduced:

java
@Service
public class TransactionDemoService {
@Transactional
public void performDatabaseOperation() {
// Database operations
System.out.println("Performing database operation");
// Spring creates a proxy around this method to handle transaction management
}
}

When you call performDatabaseOperation(), Spring intercepts the call through a proxy, starts a transaction before the method executes, and commits or rolls back after the method completes.

Implementing Custom AOP

You can create your own aspects using Spring AOP:

java
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethodCall(JoinPoint joinPoint) {
System.out.println("Method called: " + joinPoint.getSignature().getName());
}

@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterMethodCall(JoinPoint joinPoint, Object result) {
System.out.println("Method returned: " + result);
}
}

This creates proxies around your service methods to add logging functionality without modifying the service code.

Template Method Pattern

JdbcTemplate, RestTemplate, and More

The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. Spring implements this pattern in several "Template" classes.

JdbcTemplate Example

java
@Repository
public class JdbcUserRepository implements UserRepository {
private final JdbcTemplate jdbcTemplate;

@Autowired
public JdbcUserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@Override
public User findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT id, username, email FROM users WHERE id = ?",
new Object[]{id},
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("username"),
rs.getString("email")
)
);
}
}

The JdbcTemplate handles the boilerplate code for:

  • Opening and closing database connections
  • Handling exceptions
  • Managing transactions
  • Cleaning up resources

RestTemplate Example

Similarly, RestTemplate simplifies REST API calls:

java
@Service
public class WeatherService {
private final RestTemplate restTemplate;

@Autowired
public WeatherService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public WeatherData getWeatherForCity(String city) {
String url = "https://api.weather.example/data?city=" + city;
return restTemplate.getForObject(url, WeatherData.class);
}
}

Observer Pattern

Spring Events

The Observer pattern establishes a one-to-many relationship between objects, where multiple observers are notified when the subject changes state. Spring implements this through its event system.

Publishing Events

java
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;

@Autowired
public UserService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}

public void registerUser(User user) {
// Save the user
// ...

// Publish event
eventPublisher.publishEvent(new UserRegisteredEvent(user));
}
}

// Custom event class
public class UserRegisteredEvent {
private final User user;

public UserRegisteredEvent(User user) {
this.user = user;
}

public User getUser() {
return user;
}
}

Listening for Events

java
@Component
public class EmailNotificationListener {
@EventListener
public void handleUserRegistration(UserRegisteredEvent event) {
User user = event.getUser();
System.out.println("Sending welcome email to: " + user.getEmail());
// Send email logic
}
}

This pattern helps create loosely coupled systems where components can communicate without direct dependencies.

Real-World Application: Building a Blog System

Let's bring these patterns together in a practical example - a simple blogging platform using Spring Boot.

Project Structure

src/main/java/com/example/blog/
├── BlogApplication.java
├── config/
│ └── AppConfig.java
├── controller/
│ └── PostController.java
├── model/
│ └── Post.java
├── repository/
│ └── PostRepository.java
├── service/
│ ├── PostService.java
│ └── PostServiceImpl.java
└── event/
├── PostCreatedEvent.java
└── StatisticsListener.java

Implementation

  1. Model Class
java
public class Post {
private Long id;
private String title;
private String content;
private LocalDateTime createdAt;

// Constructor, getters, setters
}
  1. Repository Interface using Template Pattern
java
@Repository
public class PostRepository {
private final JdbcTemplate jdbcTemplate;

@Autowired
public PostRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void save(Post post) {
jdbcTemplate.update(
"INSERT INTO posts (title, content, created_at) VALUES (?, ?, ?)",
post.getTitle(), post.getContent(), LocalDateTime.now()
);
}

public List<Post> findAll() {
return jdbcTemplate.query(
"SELECT id, title, content, created_at FROM posts ORDER BY created_at DESC",
(rs, rowNum) -> new Post(
rs.getLong("id"),
rs.getString("title"),
rs.getString("content"),
rs.getTimestamp("created_at").toLocalDateTime()
)
);
}
}
  1. Service Layer with Dependency Injection
java
@Service
public class PostServiceImpl implements PostService {
private final PostRepository postRepository;
private final ApplicationEventPublisher eventPublisher;

@Autowired
public PostServiceImpl(PostRepository postRepository, ApplicationEventPublisher eventPublisher) {
this.postRepository = postRepository;
this.eventPublisher = eventPublisher;
}

@Override
@Transactional // Using AOP/Proxy pattern for transaction management
public void createPost(Post post) {
postRepository.save(post);
eventPublisher.publishEvent(new PostCreatedEvent(post)); // Observer pattern
}

@Override
public List<Post> getAllPosts() {
return postRepository.findAll();
}
}
  1. Controller using Dependency Injection
java
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;

@Autowired
public PostController(PostService postService) {
this.postService = postService;
}

@PostMapping
public ResponseEntity<String> createPost(@RequestBody Post post) {
postService.createPost(post);
return ResponseEntity.ok("Post created successfully");
}

@GetMapping
public ResponseEntity<List<Post>> getAllPosts() {
return ResponseEntity.ok(postService.getAllPosts());
}
}
  1. Event Classes (Observer Pattern)
java
public class PostCreatedEvent {
private final Post post;

public PostCreatedEvent(Post post) {
this.post = post;
}

public Post getPost() {
return post;
}
}

@Component
public class StatisticsListener {
@EventListener
public void handlePostCreatedEvent(PostCreatedEvent event) {
System.out.println("Post statistics: New post created with title: " + event.getPost().getTitle());
// Update statistics, analytics, etc.
}
}
  1. Application Class
java
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}

@Bean
public RestTemplate restTemplate() {
return new RestTemplate(); // Factory pattern
}
}

Summary

We've explored the key design patterns that form the foundation of the Spring Framework:

  1. IoC and Dependency Injection: The core patterns that manage object creation and dependencies
  2. Singleton Pattern: Spring's default scope for managing bean instances
  3. Factory Pattern: Implemented through BeanFactory and ApplicationContext for bean creation
  4. Proxy Pattern: Used in AOP for intercepting method calls
  5. Template Method Pattern: Implemented in JdbcTemplate, RestTemplate, and others
  6. Observer Pattern: Implemented through Spring's event system

Understanding these patterns will help you:

  • Make better architectural decisions
  • Write more maintainable and testable code
  • Use Spring's features more effectively
  • Identify common design solutions to recurring problems

Additional Resources

Exercises

  1. Implement a simple application that demonstrates at least three Spring design patterns working together.
  2. Create a custom aspect using Spring AOP to log the execution time of methods in a service class.
  3. Design a notification system using the Observer pattern with Spring events.
  4. Refactor an existing code base to use dependency injection instead of direct object creation.
  5. Create a custom template class following the Template Method pattern for a specific task (e.g., file processing).

Understanding these design patterns will provide you with powerful tools for building robust, maintainable Spring applications. As you continue working with Spring, you'll recognize these patterns in action and learn to leverage them effectively in your own projects.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)