Spring Boot vs Micronaut — Which One I'd Pick for a Greenfield Service
by Arif Ikhsanudin, Backend Developer
The cold-start argument is real, but context-dependent
Your Lambda functions are timing out on cold starts. Or your Kubernetes cluster is slow to scale because new pod instances take 8-12 seconds to become healthy. Someone on the team mentions Micronaut or Quarkus. The pitch: sub-200ms startup times, lower memory footprint, GraalVM native image support. It is a real pitch. Here is whether it holds up for your situation.
What Micronaut does differently
Spring Boot's dependency injection is runtime-based. At startup, Spring scans the classpath, builds a bean graph, resolves proxies, and instantiates components. This is powerful and flexible — Spring can wire beans dynamically at runtime based on conditions — but it is also what makes Spring Boot applications take 6-15 seconds to start on typical workloads and consume 250-500MB of heap at idle.
Micronaut moves all of this to compile time. Its annotation processor generates the injection code during the build. At runtime, there is no reflection, no classpath scanning, no proxy generation. The result is startup times in the 100-500ms range and heap usage around 50-100MB idle for a comparable service.
// Micronaut — same annotation style as Spring, different engine underneath
@Singleton
public class PaymentService {
private final PaymentGateway gateway;
private final OrderRepository orders;
// Injected at compile time — no runtime reflection
public PaymentService(PaymentGateway gateway, OrderRepository orders) {
this.gateway = gateway;
this.orders = orders;
}
public PaymentResult process(UUID orderId, PaymentRequest request) {
var order = orders.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
return gateway.charge(order.getTotal(), request.getToken());
}
}
The annotation surface is intentionally similar to Spring. Migrating familiarity is straightforward. Migrating actual Spring code is not — the dependency resolution model is different enough that you cannot drop-in replace Spring dependencies.
The ecosystem gap is the real cost
Spring Boot's ecosystem is 20 years deep. Spring Data JPA, Spring Security, Spring Batch, Spring Cloud, Spring AMQP — these are production-hardened libraries with extensive documentation, Stack Overflow coverage, and known failure modes. When something breaks at 2am, there is an answer somewhere.
Micronaut's ecosystem is functional but thinner. Micronaut Data is good, Micronaut Security covers the common cases, but you will hit edges earlier. Third-party library support varies — if a library assumes Spring's ApplicationContext or uses Spring's @Conditional mechanism, it will not work without adaptation. This is not a blocker but it is a real tax on integration work.
The GraalVM native image story is also more complex than the demos suggest. Native compilation of a Micronaut service works well for greenfield services that control their full dependency tree. The moment you pull in a library that uses reflection internally — and many do — you need to supply GraalVM reflection configuration manually or the native image will fail at runtime in non-obvious ways.
# What native compilation actually looks like
./mvnw package -Dpackaging=native-image
# Build time: 2-5 minutes on a modern laptop
# Result: single binary, ~50MB, starts in <100ms
# Caveat: any dynamic class loading breaks this
Deployment context determines the winner
Where Micronaut wins clearly:
- AWS Lambda or Google Cloud Functions with consistent cold-start SLA requirements. A Micronaut native image that starts in 80ms versus a Spring Boot JAR that starts in 8 seconds is a meaningful user experience difference in serverless contexts.
- High-density Kubernetes deployments where you are trying to run many small service instances per node and memory per pod is a cost line you are actively managing.
- New teams choosing a framework without existing Spring investment — the learning curve difference is smaller than teams with Spring expertise assume.
Where Spring Boot wins:
- Teams with existing Spring expertise. The productivity advantage of a known ecosystem outweighs Micronaut's technical advantages for most delivery timelines.
- Services with complex integration requirements — messaging with Spring AMQP, batch processing with Spring Batch, security with Spring Security's full OAuth2/OIDC support. These are production-proven. The Micronaut equivalents work but require more configuration.
- Long-running services on traditional VMs or containers where JVM warm-up time is paid once and amortized over days of uptime. The startup time difference becomes irrelevant.
What I would actually pick
For a new service on Kubernetes with rolling deployments and pod autoscaling: Spring Boot with Java 21 virtual threads. The startup time improvement from virtual threads (JEP 444) is not as dramatic as Micronaut, but Spring Boot 3.x on Java 21 starts in 3-4 seconds and handles thousands of concurrent connections efficiently. That is good enough for 95% of Kubernetes deployment patterns.
For serverless or any context where cold starts are a product requirement: Micronaut with native image if the team can afford the build complexity, or Spring Boot with GraalVM native support (Spring Boot 3.x has first-class native support via Spring Native) if the team is Spring-native.
Verify your cold-start budget before choosing. Most teams that choose Micronaut for cold-start performance have never actually measured their Spring Boot startup time with the JVM tuned for containerized environments. JAVA_TOOL_OPTIONS=-XX:TieredStopAtLevel=1 alone cuts Spring Boot startup from 8s to 3s on many services.