Spring Boot
Application Development
Low Memory Usage
Java Optimization
Software Efficiency

Developing spring boot application with lower footprint

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Spring Boot makes Java development fast and convenient, but the default configuration often pulls in far more libraries and runtime overhead than your application actually needs. In cloud-native environments where you pay per megabyte of memory and per millisecond of startup time, trimming that footprint matters. This guide walks you through five practical strategies for building Spring Boot applications that start faster, use less memory, and consume fewer resources overall.

Dependency Pruning

The simplest way to shrink your application is to stop shipping code you never call. Every starter you add to pom.xml or build.gradle brings transitive dependencies, and many of those are never exercised at runtime.

Start by auditing what you actually use:

xml
1<!-- Before: pulls in Tomcat, JSON, validation, logging, and more -->
2<dependency>
3  <groupId>org.springframework.boot</groupId>
4  <artifactId>spring-boot-starter-web</artifactId>
5</dependency>
6
7<!-- After: exclude what you do not need -->
8<dependency>
9  <groupId>org.springframework.boot</groupId>
10  <artifactId>spring-boot-starter-web</artifactId>
11  <exclusions>
12    <exclusion>
13      <groupId>org.springframework.boot</groupId>
14      <artifactId>spring-boot-starter-tomcat</artifactId>
15    </exclusion>
16  </exclusions>
17</dependency>
18<!-- Use a lighter server instead -->
19<dependency>
20  <groupId>org.springframework.boot</groupId>
21  <artifactId>spring-boot-starter-undertow</artifactId>
22</dependency>

You can also run mvn dependency:analyze to find declared-but-unused and used-but-undeclared dependencies. Removing even a handful of unnecessary starters can shave tens of megabytes from the final JAR.

Spring Boot Thin Launcher

The standard fat JAR packages every dependency inside the archive, producing files that are often 50 MB or larger. The Spring Boot Thin Launcher changes the packaging model: it builds a small JAR that downloads dependencies on first launch and caches them locally.

xml
1<plugin>
2  <groupId>org.springframework.boot</groupId>
3  <artifactId>spring-boot-maven-plugin</artifactId>
4  <dependencies>
5    <dependency>
6      <groupId>org.springframework.boot.experimental</groupId>
7      <artifactId>spring-boot-thin-layout</artifactId>
8      <version>1.0.31.RELEASE</version>
9    </dependency>
10  </dependencies>
11</plugin>

After building with mvn package, the resulting JAR is only a few hundred kilobytes. This is especially useful for container images where a smaller layer means faster pulls across nodes.

GraalVM Native Image

GraalVM compiles your Spring Boot application ahead of time into a standalone native binary. The result typically starts in under 100 milliseconds and uses a fraction of the memory a JVM process would need.

bash
# Install GraalVM and the native-image tool, then build:
mvn -Pnative native:compile

Spring Boot 3.x includes first-class support for native compilation through the spring-boot-starter-parent. The compiled binary eliminates the JVM warm-up phase entirely, making it ideal for serverless functions and scale-to-zero deployments. Keep in mind that reflection-heavy libraries may require additional configuration hints for native compilation.

JVM Tuning Flags

When running on the traditional JVM, a few flags can dramatically reduce memory consumption:

bash
1java -Xmx128m -Xms64m \
2     -XX:+UseSerialGC \
3     -XX:MaxMetaspaceSize=64m \
4     -jar my-app.jar

Here is what each flag does:

  • -Xmx128m caps the maximum heap at 128 MB instead of the default (usually one quarter of physical RAM).
  • -Xms64m sets the initial heap so the JVM does not over-allocate on startup.
  • -XX:+UseSerialGC selects the serial garbage collector, which has the smallest memory overhead. It is single-threaded, so it works best for applications with small heaps and low throughput requirements.
  • -XX:MaxMetaspaceSize=64m limits the metaspace so class metadata does not grow unbounded.

For containerized workloads, also add -XX:+UseContainerSupport (enabled by default since JDK 10) so the JVM respects cgroup memory limits rather than reading the host machine's total RAM.

Spring WebFlux for Reactive Processing

If your service is I/O-bound (calling databases, external APIs, or message brokers), switching from the traditional Spring MVC stack to Spring WebFlux can reduce the thread count and therefore the per-thread memory overhead.

java
1@RestController
2public class OrderController {
3
4    private final OrderService orderService;
5
6    public OrderController(OrderService orderService) {
7        this.orderService = orderService;
8    }
9
10    @GetMapping("/orders/{id}")
11    public Mono<Order> getOrder(@PathVariable String id) {
12        return orderService.findById(id);
13    }
14
15    @GetMapping("/orders")
16    public Flux<Order> getAllOrders() {
17        return orderService.findAll();
18    }
19}

WebFlux runs on a small, fixed pool of event-loop threads (typically equal to the number of CPU cores) instead of the thread-per-request model that Spring MVC uses. Because each thread consumes roughly 1 MB of stack space, an application that previously needed 200 threads now needs only 4 to 8, saving hundreds of megabytes of memory under load.

Common Pitfalls

  • Leaving spring-boot-devtools in production: Devtools enables automatic restarts and live reload, which increase memory use and startup time. Always scope it to runtime with optional=true or exclude it from the production profile.
  • Setting -Xmx too low without testing: An aggressively low heap causes frequent garbage collection pauses and can trigger OutOfMemoryError under real traffic. Always load-test before choosing a value.
  • Ignoring lazy initialization: Setting spring.main.lazy-initialization=true delays bean creation until first use, which speeds up startup, but the first request to each bean will be slower. Profile both paths.
  • Assuming native image works with every library: Some libraries rely on dynamic proxies, reflection, or runtime class generation that GraalVM cannot resolve at build time. Test thoroughly and supply reachability metadata where needed.
  • Switching to WebFlux without non-blocking drivers: WebFlux only saves resources if every layer of your stack is non-blocking. Using a blocking JDBC driver inside a reactive pipeline will stall the event loop and degrade performance for all requests.

Summary

  • Prune dependencies to remove unused starters and transitive libraries before doing anything else.
  • Use the Thin Launcher to shrink JAR size and speed up container image builds.
  • Compile to a native image with GraalVM for sub-100 ms startup and minimal memory consumption.
  • Tune JVM flags like -Xmx, -Xms, UseSerialGC, and MaxMetaspaceSize to cap resource usage on the traditional JVM.
  • Adopt Spring WebFlux for I/O-bound services to replace the thread-per-request model with a fixed event-loop pool, cutting thread-related memory by an order of magnitude.

Course illustration
Course illustration

All Rights Reserved.