How to Supercharge Your Spring Boot App with 3 Proven Optimization Techniques

What if I told you that you can improve your Spring Boot app performance by up to 80% with 3 easy optimization techniques?

Eidan Khan
JavaJams
13 min readJan 4, 2024

--

As a Spring Boot developer, you know how powerful and versatile this framework is for building Java applications that are fast, reliable, and easy to maintain. However, even the best Spring Boot applications can suffer from performance issues if they are not optimized properly. Performance issues can affect your app’s response time, scalability, and user experience, which can lead to lower customer satisfaction, retention, and revenue.

In this article, I will show you how to fine-tune your Spring Boot app for high-speed results using 3 proven optimization techniques. By following these tips, you will be able to improve your app’s response time, scalability, and user experience. The 3 optimization techniques are:

  1. Connection Pooling: How to reduce the overhead of creating and closing database connections by using a connection pool?
  2. Caching: How to reduce the number of requests to external systems by caching data using Spring Boot annotations?
  3. Optimizing request and response sizes: How to reduce the network latency and bandwidth consumption by optimizing the size of the requests and responses using techniques such as compression, pagination, filtering, and projection?

By applying these 3 optimization techniques, you can supercharge your Spring Boot app performance and deliver a better user experience.

1. Connection Pooling

Connection pooling is a technique that can improve the performance of your Spring Boot app by reducing the overhead of creating and closing database connections. A connection pool is a collection of reusable connections that are maintained by a connection manager. When your app needs to access the database, it can request a connection from the pool instead of creating a new one. When your app is done with the connection, it can return it to the pool instead of closing it. This way, you can avoid the cost of opening and closing connections, which can be expensive and time-consuming.

To illustrate the benefits of connection pooling, let’s consider a real-world scenario. Suppose you have a Spring Boot app that provides an online shopping service. Your app needs to interact with a database that stores information about products, customers, orders, etc. Your app receives a lot of requests from users who want to browse, search, and buy products. Each request requires a database connection to perform the necessary operations. Without connection pooling, your app would have to create a new connection for each request and close it when the request is completed. This would result in a lot of overhead and performance degradation, especially during peak traffic periods. Your app might also run out of available connections, leading to errors and failures.

With connection pooling, your app can use a connection pool to manage the database connections. Your app can request a connection from the pool when it needs to access the database and return it to the pool when it is done. This way, your app can reuse the existing connections and avoid the overhead of creating and closing them. Your app can also handle more concurrent requests, as the connection pool can provide a connection whenever it is available. Your app can also configure the connection pool to suit your needs, such as setting the maximum number of connections, the idle time, the validation query, etc.

To use connection pooling in your Spring Boot app, you need to configure a connection pool using Spring Boot properties. Spring Boot supports several connection pool implementations, such as HikariCP, Tomcat JDBC, and Commons DBCP2. You can choose the one that best suits your needs and preferences. You can also customize the connection pool properties using the spring.datasource prefix. For example, you can use the following properties to configure a HikariCP connection pool:

spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1

These properties specify that the connection pool type is HikariDataSource, the maximum number of connections is 10, the minimum number of idle connections is 5, the idle timeout is 30 seconds, and the validation query is SELECT 1.

2. Caching

Caching is a technique that can improve the performance of your Spring Boot app by reducing the number of requests to external systems. By caching data, you can avoid repeating costly operations such as database queries or network requests.

To illustrate the benefits of caching, let’s consider a real-world scenario. Suppose you have a Spring Boot app that provides a news service. Your app needs to interact with a third-party API that provides the latest news headlines from various sources. Your app receives a lot of requests from users who want to read the news. Each request requires a network call to the API to fetch the news headlines. Without caching, your app would have to make a network call for each request and wait for the response. This would result in a lot of network latency and bandwidth consumption, especially during peak traffic periods. Your app might also exceed the API rate limit, leading to errors and failures.

With caching, your app can store the news headlines in a cache and reuse them for subsequent requests. Your app can use a caching strategy such as time-to-live (TTL) or time-to-idle (TTI) to determine how long the data should remain in the cache. Your app can also use a cache eviction policy such as least recently used (LRU) or least frequently used (LFU) to determine which data should be removed from the cache when it is full. This way, your app can reduce the number of network calls and improve the response time and user experience.

Here are some examples of how to use the @Cacheable, @CacheEvict, and @CachePut annotations to cache data using Spring Boot:

  • @Cacheable: This annotation indicates that the result of a method invocation should be cached. The annotation takes a value parameter that specifies the name of the cache. The annotation also takes a key parameter that specifies the key for the cached data. If no key is provided, the method parameters are used as the key. For example, suppose you have a method that retrieves a news headline from a third-party API. You can use the @Cacheable annotation to cache the result of this method and avoid hitting the API every time the method is called. You can also specify a cache name and a key to identify the cached data. Here is an example of how to use the @Cacheable annotation:
@Service
public class NewsService {

@Autowired
private NewsApiClient newsApiClient;

@Cacheable(value = "news", key = "#source")
public NewsHeadline getNewsHeadline(String source) {
return newsApiClient.getNewsHeadline(source);
}
}

In this example, we are caching the news headline by source in a cache named “news”. The key for the cached data is the source parameter. This means that the method will only execute the API call if the cache does not contain the data for the given source. Otherwise, the method will return the cached data. This way, we can reduce the number of API calls and improve the performance of our app.

  • @CacheEvict: This annotation indicates that an entry should be removed from the cache. The annotation takes a value parameter that specifies the name of the cache. The annotation also takes a key parameter that specifies the key for the cached data. If no key is provided, the method parameters are used as the key. For example, suppose you have a method that updates a news headline in the database. You can use the @CacheEvict annotation to remove the corresponding entry from the cache and ensure that the cache is consistent with the database. You can also specify a cache name and a key to identify the cached data. Here is an example of how to use the @CacheEvict annotation:
@Service
public class NewsService {

@Autowired
private NewsRepository newsRepository;

@CacheEvict(value = "news", key = "#newsHeadline.source")
public void updateNewsHeadline(NewsHeadline newsHeadline) {
newsRepository.save(newsHeadline);
}
}

In this example, we are removing the news headline by source from the cache named “news”. The key for the cached data is the source property of the newsHeadline parameter. This means that the method will delete the cache entry for the given source. This way, we can ensure that the cache is consistent with the database.

  • @CachePut: This annotation indicates that the result of a method invocation should be updated in the cache. The annotation takes a value parameter that specifies the name of the cache. The annotation also takes a key parameter that specifies the key for the cached data. If no key is provided, the method parameters are used as the key. For example, suppose you have a method that creates a news headline in the database. You can use the @CachePut annotation to add the corresponding entry to the cache and ensure that the cache is consistent with the database. You can also specify a cache name and a key to identify the cached data. Here is an example of how to use the @CachePut annotation:
@Service
public class NewsService {

@Autowired
private NewsRepository newsRepository;

@CachePut(value = "news", key = "#newsHeadline.source")
public NewsHeadline createNewsHeadline(NewsHeadline newsHeadline) {
return newsRepository.save(newsHeadline);
}
}

In this example, we are adding the news headline by source to the cache named “news”. The key for the cached data is the source property of the newsHeadline parameter. This means that the method will update the cache entry for the given source. This way, we can ensure that the cache is consistent with the database.

3. Optimizing Request and Response Sizes

Optimizing request and response sizes is a technique that can improve the performance of your Spring Boot app by reducing the network latency and bandwidth consumption. By optimizing request and response sizes, you can avoid sending or receiving unnecessary or redundant data that can slow down your app.

To illustrate the benefits of optimizing request and response sizes, let’s consider a real-world scenario. Suppose you have a Spring Boot app that provides a social media service. Your app needs to interact with a database that stores information about users, posts, comments, likes, etc. Your app receives a lot of requests from users who want to view, create, or update posts. Each request requires a database query to fetch or save the data. Without optimizing request and response sizes, your app would have to send or receive the entire data objects for each request and response. This would result in a lot of network latency and bandwidth consumption, especially for large or complex data objects. Your app might also encounter errors or failures due to the size limit of the requests or responses.

With optimizing request and response sizes, your app can reduce the amount of data that is sent or received for each request and response. You can use techniques such as compression, pagination, filtering, and projection to optimize the request and response sizes. Compression is a technique that reduces the size of the data by using algorithms that remove or replace redundant or irrelevant information. Pagination is a technique that divides the data into smaller chunks or pages that can be requested or sent separately. Filtering is a technique that removes the data that is not relevant or needed for the request or response. Projection is a technique that selects only the data that is relevant or needed for the request or response.

To use these techniques in your Spring Boot app, you need to use the features and frameworks provided by Spring Boot. Spring Boot provides features and frameworks that enable you to optimize the request and response sizes using techniques such as compression, pagination, filtering, and projection. You can use the following features and frameworks to optimize the request and response sizes:

  • Compression:
  • You can enable compression for your requests and responses using the spring.http.encoding properties. You can also use the @EnableCompression annotation to enable compression for specific controllers or methods. For example, you can use the following properties to enable compression for your requests and responses:
spring.http.encoding.enabled=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true

These properties specify that compression is enabled, the charset is UTF-8, and compression is forced for all requests and responses.

  • Pagination: You can use pagination for your requests and responses using the Spring Data framework. Spring Data provides a Pageable interface that represents a page request that contains the page number, the page size, and the sort order. You can use the Pageable interface as a parameter for your repository methods or controller methods to fetch or return a page of data. You can also use the Page interface that represents a page of data that contains the content, the total number of elements, the total number of pages, and the page metadata. You can use the Page interface as a return type for your repository methods or controller methods to return a page of data. For example, suppose you have a method that retrieves a page of posts from the database. You can use the Pageable and Page interfaces to paginate the data. Here is an example of how to use pagination:
  • Filtering: You can use filtering for your requests and responses using the Spring Web framework. Spring Web provides a @JsonFilter annotation that enables you to filter the properties of a data object that are serialized or deserialized. You can use the @JsonFilter annotation on your data classes or methods to specify a filter name. You can also use the MappingJacksonValue class to apply the filter to your data objects. You can also use the SimpleBeanPropertyFilter and FilterProvider classes to create and configure the filter. For example, suppose you have a class that represents a user that contains the id, name, email, password, and role properties. You can use the @JsonFilter annotation to filter the properties of the user object that are serialized or deserialized. Here is an example of how to use filtering:
@Data
@Entity
@Table(name = "users")
@JsonFilter("userFilter")
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name")
private String name;

@Column(name = "email")
private String email;

@Column(name = "password")
private String password;

@Column(name = "role")
private String role;
}

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserRepository userRepository;

// Return a user by id
@GetMapping("/{id}")
public MappingJacksonValue getUserById(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found"));
// Create a filter that excludes the password and role properties
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept("password", "role");
// Create a filter provider that applies the filter to the userFilter
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("userFilter", filter);
// Create a mapping value that wraps the user object and the filter provider
MappingJacksonValue mapping = new MappingJacksonValue(user);
mapping.setFilters(filterProvider);
return mapping;
}
}

In this example, we are using the @JsonFilter annotation on the User class to specify a filter name of userFilter. We are also using the SimpleBeanPropertyFilter and FilterProvider classes to create and configure a filter that excludes the password and role properties. We are also using the MappingJacksonValue class to apply the filter to the user object that is returned by the controller method. This way, we can reduce the amount of data that is serialized or deserialized for each request or response.

  • Projection: You can use projection for your requests and responses using the Spring Data framework. Spring Data provides a projection feature that enables you to select only the properties of a data object that are relevant or needed for the request or response. You can use the projection feature by creating an interface that defines the properties that you want to include or exclude from the data object. You can also use the @Value annotation to derive the properties from the data object. You can also use the projection interface as a parameter for your repository methods or controller methods to fetch or return the projected data. For example, suppose you have a class that represents a post that contains the id, title, content, author, and comments properties. You can use the projection feature to project the properties of the post object that are relevant or needed for the request or response. Here is an example of how to use projection:
@Data
@Entity
@Table(name = "posts")
public class Post {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "title")
private String title;

@Column(name = "content")
private String content;

@ManyToOne
@JoinColumn(name = "user_id")
private User author;

@OneToMany(mappedBy = "post")
private List<Comment> comments;
}

// Define a projection interface that includes only the id and title properties
public interface PostSummary {

Long getId();

String getTitle();
}

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {

// Fetch a list of posts by user id using the projection interface
List<PostSummary> findByUserId(Long userId);
}

@RestController
@RequestMapping("/posts")
public class PostController {

@Autowired
private PostRepository postRepository;

// Return a list of posts by user id using the projection interface
@GetMapping
public List<PostSummary> getPostsByUserId(@RequestParam Long userId) {
return postRepository.findByUserId(userId);
}
}

In this example, we are using the projection feature by creating an interface that defines the properties that we want to include from the post object. We are also using the projection interface as a parameter for the repository method and the controller method to fetch or return the projected data. This way, we can reduce the amount of data that is fetched or returned for each request or response.

In this article, I have shared with you 3 best practices for optimizing your Spring Boot app performance. By using connection pooling, caching, and optimizing request and response sizes, you can improve your app’s response time, scalability, and user experience. By applying these 3 optimization techniques, you can supercharge your Spring Boot app performance and deliver a better user experience. I encourage you to try these optimization techniques in your Spring Boot app and see the results for yourself. You will be amazed by how much faster and smoother your app will run.

Thank you for reading this article and I hope you found it helpful and informative. Please feel free to leave your feedback or questions in the comments section below. I would love to hear from you.

Thanks for Reading!

Please comment with questions and suggestions!! If this blog is helpful for you then, Please hit the clap! Follow on Medium, Please Follow and subscribe to Make Android for prompt updates on Android platform-related blogs.

Thank you!

--

--

Eidan Khan
JavaJams

🚀 Full-stack Dev | Tech Content Creator 📝 For more in-depth articles, tutorials, and insights, visit my blog at JavaJams.org.