Computer scienceBackendSpring BootSpring Boot (Kotlin)

Patterns in Spring Boot (part 1)

9 minutes read

Introduction

Design patterns are like blueprints for solving common software design problems. They help structure code efficiently, making it easier to read, maintain, and scale. In Spring Boot, these patterns streamline development by handling dependency management, request processing, and business logic execution.

Understanding design patterns is crucial for writing robust, flexible applications. In this topic, you'll learn about three essential patterns: Singleton, Proxy, and Command. These patterns help manage object instances, enhance security, and handle operations dynamically.

The Singleton Pattern

The Singleton pattern ensures that only one instance of a class exists during the application lifecycle. This is crucial when managing shared resources like database connections, configuration settings, and caching mechanisms.

Why Use Singleton?

Imagine you are building a web application that connects to a database. If every request creates a new database connection, your server will quickly run out of resources. Instead, you need a single instance of the database connection manager that all requests can share. The Singleton pattern solves this issue by creating one shared instance that is used throughout the application's lifecycle.

Implementing Singleton in Spring Boot

Spring Boot automatically creates singleton beans when you annotate a class with @Service, @Component, or @Repository. This means Spring manages the instance creation and lifecycle, ensuring a single instance per application context.

Here’s how you can create a Singleton in Spring Boot:

import org.springframework.stereotype.Service

@Service
class SingletonService {
    fun getMessage(): String {
        return "This is a Singleton Service!"
    }
}
import org.springframework.stereotype.Service;

@Service
public class SingletonService {
    public String getMessage() {
        return "This is a Singleton Service!";
    }
}

Now, let’s inject SingletonService into multiple controllers. Regardless of how many controllers use it, the same instance is reused:

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/singleton")
class SingletonController(private val singletonService: SingletonService) {
    @GetMapping
    fun getMessage(): String = singletonService.getMessage()
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/singleton")
public class SingletonController {
    private final SingletonService singletonService;

    public SingletonController(SingletonService singletonService) {
        this.singletonService = singletonService;
    }

    @GetMapping
    public String getMessage() {
        return singletonService.getMessage();
    }
}

The Singleton pattern ensures that only one instance of a service is used across the application. This reduces unnecessary memory allocation, improves performance, and helps manage shared resources efficiently.

The Proxy Pattern

The Proxy pattern allows you to add extra functionality to your classes without modifying their existing structure. In Spring Boot, proxies are widely used with aspect-oriented programming (AOP) to provide logging, security checks, and transaction management in a clean, centralized way.

What is a Proxy?

A proxy acts as an intermediary between a client and an actual object. When a client calls a method on the real object, the proxy can intercept that call, perform additional actions (like logging or permission checks), and then forward the call to the real object.

In Spring Boot, these proxies are often generated automatically when you use AOP. This lets you add common concerns (e.g., logging) to multiple classes without touching their code.

What is Aspect-Oriented Programming (AOP)?

Aspect-Oriented Programming (AOP) is a technique for separating cross-cutting concerns—such as logging, security, and transaction management—without modifying core business logic. Instead of adding this functionality inside multiple classes, AOP allows defining it once and applying it where needed.

AOP works by intercepting method calls and executing additional behavior before, after, or around them. This is done using aspects, which contain reusable logic applied to selected methods through pointcut expressions.

Implementing a Proxy with AOP

In Spring Boot, you can enable method interception using AOP with the @Aspect annotation. Here’s a simple example of a LoggingAspect that logs after any method in the com.example.service package:

import org.aspectj.lang.JoinPoint
import org.aspectj.lang.annotation.AfterReturning
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.springframework.stereotype.Component

@Aspect
@Component
class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    fun logBefore(joinPoint: JoinPoint) {
        val methodName = joinPoint.signature.name
        println("LOG: Method '$methodName' execution started!")
    }

    @AfterReturning("execution(* com.example.service.*.*(..))")
    fun logMethodExecution(joinPoint: JoinPoint) {
        val methodName = joinPoint.signature.name
        println("LOG: Method '$methodName' executed successfully!")
    }
}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("LOG: Method '%s' execution started!%n", methodName);
    }

    @AfterReturning("execution(* com.example.service.*.*(..))")
    public void logMethodExecution(JoinPoint  joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("LOG: Method '%s' executed successfully!%n", methodName);
    }
}
  • @Aspect: Declares this class as an aspect, which defines extra behavior to add around method calls.

  • @Component: Registers the aspect with Spring so it can be applied automatically.

  • Pointcut Expression:

    • "execution(* com.example.service.*.*(..))" means “Intercept any method (*) in any class under com.example.service, with any parameter list ((..)).”

  • @Before: Tells Spring to run logBefore() before a service method starts.

  • @AfterReturning: Tells Spring to run logMethodExecution() after a service method finishes without throwing an error.

How It Works in Practice

  1. Client calls a service method, say taskService.createTask("Clean the house").

  2. Spring wraps taskService in a proxy. When the method is called, the proxy first handles any relevant aspects (logging, security, etc.). For example, our logBefore() advice runs.

  3. Real Method executes, creating the task.

  4. Aspect Code runs again (because we used @AfterReturning), with our logMethodExecution() advice logging a message that the method finished successfully.

Result: You see messages like:

LOG: Method 'createTask' execution started!
Task 'Clean the house' created. // This would be output from the service itself
LOG: Method 'createTask' executed successfully!

Notice you didn’t have to edit the taskService.createTask() method to add logging. The proxy handled it behind the scenes.

Why Use the Proxy Pattern?

By using the Proxy pattern through Spring AOP, you can add cross-cutting concerns such as:

  • Logging (who called the method, how long it took, etc.).

  • Security (only “admin” users can delete tasks).

  • Transactions (start/commit a database transaction around your method).

All of these are managed in one place (the aspect), keeping your business logic clean and focused on what it does best: creating, updating, and managing tasks.

The Proxy pattern in Spring Boot, powered by AOP, helps developers add or modify functionality like logging, security checks, and transaction handling without changing the original service code. This makes your application more modular, maintainable, and scalable—as you can inject new behaviors simply by creating or updating an aspect, rather than editing each method individually.

The Command Pattern

The Command Pattern is a behavioral design pattern that encapsulates operations inside objects, allowing you to execute them dynamically without knowing their exact implementation. This pattern is particularly useful when designing systems that need to support flexible and decoupled execution logic, such as task execution, job scheduling, and event-driven processing.

A typical implementation involves an Invoker, which processes user interactions or scheduled events, a Command, which represents an executable action, and a Receiver, which contains the actual business logic. Traditionally, invokers maintain a manual mapping of commands, requiring developers to register and manage them explicitly. However, using dependency injection, we can automate command discovery and execution, eliminating the need for manual registration.

The first step is to define a common interface that ensures all commands follow a standardized structure. This interface will include a single execute() method that all command classes must implement.

interface Command {
    fun execute(): String
}
public interface Command {
    String execute();
}

Each command is then implemented as a separate class, encapsulating a specific action. Instead of registering these commands manually, we use dependency injection to automatically detect them and add them to a map.

import org.springframework.stereotype.Component

@Component("addItem")
class AddItemCommand : Command {
    override fun execute(): String {
        return "Item added successfully!"
    }
}

@Component("removeItem")
class RemoveItemCommand : Command {
    override fun execute(): String {
        return "Item removed successfully!"
    }
}
import org.springframework.stereotype.Component;

@Component("addItem")
public class AddItemCommand implements Command {
    @Override
    public String execute() {
        return "Item added successfully!";
    }
}

@Component("removeItem")
public class RemoveItemCommand implements Command {
    @Override
    public String execute() {
        return "Item removed successfully!";
    }
}

Each command class is marked with @Component("<command_name>"), which registers it as a Spring bean. The command name provided in the annotation acts as the key for automatic command discovery. This allows us to dynamically retrieve and execute commands without manually mapping them.

Instead of manually maintaining a command registry, we inject all available commands into a Map<String, Command> , where the key is the command name and the value is the corresponding command instance.

import com.example.command.Command
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

@Service
class CommandExecutor @Autowired constructor(
    private val commandMap: Map<String, Command>
) {
    fun executeCommand(commandName: String): String {
        return commandMap[commandName]?.execute() ?: "Unknown command: $commandName"
    }
}
import com.example.command.Command;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class CommandExecutor {
    // Spring injects all Command beans into this map
    private final Map<String, Command> commandMap;

    @Autowired
    public CommandExecutor(Map<String, Command> commandMap) {
        this.commandMap = commandMap;
    }

    public String executeCommand(String commandName) {
        Command command = commandMap.get(commandName);
        if (command != null) {
            return command.execute();
        }
        return "Unknown command: " + commandName;
    }
}

When executeCommand(commandName) is called, the correct command is retrieved from commandMap and executed dynamically. If the requested command does not exist, an error message is returned. This eliminates the need for if-else conditions or switch statements, making the code more maintainable and scalable.

By leveraging dependency injection, this implementation allows commands to be added or removed without modifying existing code. New commands can be introduced simply by creating a class and annotating it, ensuring they are automatically registered. This makes the system flexible and extensible while keeping execution logic decoupled from request handling.

This approach is well-suited for task schedulers, event-driven architectures, and dynamic execution systems. It ensures modular design and better code organization while reducing manual overhead. Further improvements can include asynchronous execution, logging for executed commands, and undo functionality by tracking command history.

Conclusion

Design patterns are powerful tools for organizing and streamlining software development in Spring Boot. Here’s a quick recap:

  1. Singleton Pattern

    • Ensures only one instance of a class exists, managed by Spring Boot’s application context.

    • Ideal for shared resources like database connections or configuration managers.

  2. Proxy Pattern

    • Uses AOP to insert additional behavior (e.g., logging, security, transactions) around service methods.

    • Keeps your core business logic clean by centralizing cross-cutting concerns in separate “aspect” classes.

  3. Command Pattern

    • Encapsulates operations into objects, allowing them to be executed dynamically without knowing their exact implementation.

    • Perfect for handling task execution, scheduling, and event-driven processing, where operations need to be handled flexibly.

By understanding and applying these design patterns, you can write robust, modular, and scalable Spring Boot applications that are easy to maintain and extend. They enable you to separate concerns effectively, keep your codebase organized, and respond to changing requirements with minimal friction.

4 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo