Post

Quarkus vs Spring Boot — A Practical Comparison for 2026

An honest side-by-side comparison of Quarkus and Spring Boot in 2026. Covers startup time, memory usage, developer experience, ecosystem, native compilation, and when to choose which — from someone who uses both.

Quarkus vs Spring Boot — A Practical Comparison for 2026

The Question — Which Framework?

If you’re starting a new Java microservice in 2026, you’ve got a genuine choice to make. Spring Boot has been the default for a decade. Quarkus arrived as the “Kubernetes-native” alternative and has matured rapidly. Both target Java 21, both support GraalVM native images, and both have strong communities.

This isn’t a “which is better” post — it’s a “when to choose which” post, backed by code you can compare side by side.


Architecture Differences

The fundamental philosophical difference: when work happens.

flowchart LR
    subgraph Spring Boot
        direction TB
        S1[Application Starts] --> S2[Classpath Scanning]
        S2 --> S3[Bean Discovery]
        S3 --> S4[Proxy Generation]
        S4 --> S5[Dependency Injection]
        S5 --> S6[Ready to Serve]
    end

    subgraph Quarkus
        direction TB
        Q1[Build Time] --> Q2[Classpath Scanning]
        Q2 --> Q3[Bean Discovery]
        Q3 --> Q4[Bytecode Generation]
        Q4 --> Q5[Runtime Start]
        Q5 --> Q6[Minimal DI Wiring]
        Q6 --> Q7[Ready to Serve]
    end

diagram

Spring Boot does most of its heavy lifting at runtime — scanning, proxy generation, condition evaluation. Quarkus moves as much as possible to build time using its extension framework. The result: dramatically faster startup and lower memory at the cost of longer builds.

AspectSpring BootQuarkus
DI resolutionRuntimeBuild time
ConfigurationRuntime conditionsBuild-time augmentation
Proxy generationRuntime (CGLIB/JDK)Build time (bytecode)
ReflectionHeavy useMinimized (records metadata at build)
Hot reloadSpring DevTools (restart)Quarkus Dev Mode (instant)

Side-by-Side Code

Let’s build the same simple REST API in both frameworks: a BookController with CRUD operations.

REST Endpoint

Spring Boot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    public List<Book> findAll() {
        return bookService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> findById(@PathVariable Long id) {
        return bookService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Book create(@Valid @RequestBody Book book) {
        return bookService.save(book);
    }
}

Quarkus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Path("/api/books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {

    private final BookService bookService;

    @Inject
    public BookResource(BookService bookService) {
        this.bookService = bookService;
    }

    @GET
    public List<Book> findAll() {
        return bookService.findAll();
    }

    @GET
    @Path("/{id}")
    public Response findById(@PathParam("id") Long id) {
        return bookService.findById(id)
                .map(book -> Response.ok(book).build())
                .orElse(Response.status(Status.NOT_FOUND).build());
    }

    @POST
    public Response create(@Valid Book book) {
        Book created = bookService.save(book);
        return Response.status(Status.CREATED).entity(created).build();
    }
}

Dependency Injection

Spring Boot:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    // constructor injection — no annotation needed (single constructor)
}

Quarkus:

1
2
3
4
5
6
7
8
9
10
11
12
@ApplicationScoped
public class BookService {

    private final BookRepository bookRepository;

    @Inject
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    // CDI-based — @Inject required on constructor
}

Configuration

Spring Boot (application.yml):

1
2
3
4
5
6
7
8
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/books
    username: ${DB_USER:postgres}
    password: ${DB_PASS:secret}
  jpa:
    hibernate:
      ddl-auto: validate

Quarkus (application.properties):

1
2
3
4
5
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/books
quarkus.datasource.username=${DB_USER:postgres}
quarkus.datasource.password=${DB_PASS:secret}
quarkus.hibernate-orm.database.generation=validate

Data Access

Spring Boot (Spring Data JPA):

1
2
3
public interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findByAuthor(String author);
}

Quarkus (Panache):

1
2
3
4
5
6
@ApplicationScoped
public class BookRepository implements PanacheRepository<Book> {
    public List<Book> findByAuthor(String author) {
        return find("author", author).list();
    }
}

Performance Benchmarks

Tested on: Apple M3 Pro, 18GB RAM, Java 21.0.3, Spring Boot 3.5.0, Quarkus 3.16.0

MetricSpring Boot (JVM)Spring Boot (Native)Quarkus (JVM)Quarkus (Native)
Startup time2.1s0.08s0.9s0.02s
RSS memory (idle)280 MB72 MB145 MB32 MB
RSS memory (load)420 MB148 MB230 MB78 MB
Throughput (req/s)48,20042,80047,50041,200
Build time8s3m 20s12s2m 45s
JAR size42 MBN/A38 MBN/A
Native binary sizeN/A98 MBN/A72 MB

Key takeaways:

  • JVM throughput is comparable — the performance difference is mostly startup + memory.
  • Native images trade throughput for startup/memory — JIT optimizes better over time.
  • Quarkus native compiles faster — less reflection to configure.

Developer Experience

FeatureSpring BootQuarkus
Hot reloadDevTools (restarts JVM, ~2s)Dev Mode (instant, no restart)
IDE supportExcellent (IntelliJ, Eclipse)Good (IntelliJ plugin improving)
DocumentationExtensive, matureGood, well-organized
Stack Overflow answersVery highGrowing
Testing@SpringBootTest, Testcontainers@QuarkusTest, Dev Services
Dev Services (auto-DB)Spring Boot 3.1+ Docker ComposeBuilt-in since Quarkus 2.0
CLI toolSpring Initializr (web)quarkus CLI (powerful)
Continuous testingSeparate (IDE/CI)Built into Dev Mode
OpenAPI generationSpringDoc (add-on)Built-in extension

The developer experience gap has narrowed significantly. Quarkus Dev Mode still leads for inner-loop speed. Spring Boot’s ecosystem maturity still wins for “I need a library for X.”


Ecosystem & Libraries

NeedSpring BootQuarkus
REST clientsRestClient, WebClient, FeignREST Client (MicroProfile), Vert.x
MessagingSpring Kafka, Spring AMQPSmallRye Reactive Messaging
SecuritySpring SecurityQuarkus Security, Keycloak
GraphQLSpring for GraphQLSmallRye GraphQL
gRPCgrpc-spring-boot-starterquarkus-grpc
Scheduling@Scheduled@Scheduled (quarkus-scheduler)
CachingSpring Cache + any providerquarkus-cache (Caffeine)
Health checksActuatorSmallRye Health
MetricsMicrometerMicrometer (via Quarkus extension)
ObservabilityMicrometer TracingOpenTelemetry (first-class)

Spring Boot wins on breadth — if a Java library exists, there’s probably a Spring integration. Quarkus wins on coherent defaults — extensions are designed to work together with minimal configuration.


Native Compilation

Spring Boot with Spring AOT

1
2
3
4
5
# Build native image
./mvnw -Pnative native:compile

# Or with Buildpacks (no local GraalVM needed)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:native

Spring AOT processes @Conditional annotations at build time and generates reflection configuration. You may still need reflect-config.json for libraries that use reflection heavily.

Quarkus Native

1
2
3
4
5
# Build native image
./mvnw package -Dnative

# With container build (no local GraalVM needed)
./mvnw package -Dnative -Dquarkus.native.container-build=true

Quarkus was designed for native from day one. Extensions register reflection automatically. You rarely need manual configuration.

Comparison

AspectSpring NativeQuarkus Native
Configuration effortMedium (improving)Low
Library compatibility~90% of Spring ecosystemExtensions are native-ready
Build timeLonger (more reflection to process)Shorter
Runtime performanceGoodGood
Debugging native issuesHarderEasier (better error messages)

When to Choose Which

flowchart TD
    A[New Java Microservice] --> B{Team's Spring experience?}
    B -->|Deep| C{Startup/memory critical?}
    B -->|Minimal or none| D{Scale to zero needed?}
    
    C -->|Yes - serverless/edge| E[Quarkus Native]
    C -->|No - long-running| F[Spring Boot JVM]
    
    D -->|Yes| E
    D -->|No| G{Large ecosystem needs?}
    
    G -->|Complex integrations| F
    G -->|Standard patterns| H[Quarkus JVM]

diagram

Decision Matrix

ScenarioRecommendationWhy
Enterprise with Spring teamsSpring BootLeverage existing expertise
Serverless / AWS LambdaQuarkus NativeFastest cold start
Kubernetes scale-to-zeroQuarkus NativeMemory + startup
Long-running monolithSpring BootEcosystem, JIT optimization
New team, greenfieldEither (evaluate both)Both are excellent
Event-driven reactiveQuarkusVert.x integration, SmallRye
Complex security requirementsSpring BootSpring Security maturity
CLI / short-lived processesQuarkus NativeSub-second startup

Can They Coexist?

Yes. In a microservices architecture, there’s no rule that says every service must use the same framework. A practical pattern:

flowchart LR
    subgraph Services
        A[API Gateway<br>Spring Boot] --> B[User Service<br>Spring Boot]
        A --> C[Notification Service<br>Quarkus Native]
        A --> D[Pricing Engine<br>Quarkus JVM]
        A --> E[Reporting Service<br>Spring Boot]
    end
    
    subgraph Shared
        F[Kafka]
        G[PostgreSQL]
        H[Redis]
    end
    
    B --> F
    C --> F
    D --> G
    E --> G
    B --> H

diagram

Guidelines for coexistence:

  • Use language-agnostic communication (REST, gRPC, Kafka) — not framework-specific.
  • Shared libraries should be plain Java (no Spring/CDI annotations).
  • Standardize on OpenTelemetry for observability across both.
  • Use the same build pipeline (Maven/Gradle) — both frameworks support both.

References

This post is licensed under CC BY 4.0 by the author.