Blog
October 6, 20264 min readIbrahim Elkamali

Spring Boot vs Quarkus for a Java API

Choose Spring Boot or Quarkus by what the framework does at build time, the threading model, library fit, and packaging, with the Better Fullstack command for each.

spring-bootquarkusjavabackend
Markdown

Stack decision map: choose an ecosystem, generate, inspect, and keep the code

Spring Boot and Quarkus both turn a set of annotated Java classes into a running HTTP service with configuration, dependency injection, and testing support. They differ in when the framework does its work, how requests are scheduled onto threads, and which programming model your existing code and libraries expect.

In Better Fullstack they also differ sharply in how much is generated, which is covered below.

What the framework does for you

Spring Boot "takes an opinionated view of the Spring platform and third-party libraries" so most applications need little configuration. It lists embedded servers, security, metrics, health checks, and externalized configuration among its non-functional features. Web applications use either the spring-boot-starter-webmvc module or, for reactive applications, spring-boot-starter-webflux (web reference).

Quarkus moves work to build time: "As much processing as possible is done at build time; thus, your application only contains the classes used at runtime." It also tries to avoid reflection. Endpoints are written with Jakarta REST annotations such as @Path and @GET, and the Quarkus about page says it is designed to combine imperative code with the non-blocking reactive style.

Concurrency and runtime model

Spring MVC is the servlet-based stack. Spring WebFlux "does not require the servlet API, is fully asynchronous and non-blocking," and implements Reactive Streams through Reactor. On Java 21 or later, setting spring.threads.virtual.enabled to true makes Spring Boot's auto-configured task executor use virtual threads.

Quarkus REST chooses the thread from the method signature. Methods that return a plain value run on a worker thread; methods that return Uni, Multi, or CompletionStage run on the I/O event loop. @Blocking, @NonBlocking, and @RunOnVirtualThread override that choice.

Both frameworks support blocking and non-blocking code. The question is which style your data access and client libraries use, because blocking calls on an event loop thread hold up other requests.

Ecosystem and library fit

Code written against Spring's APIs (Spring Data repositories, Spring Security filters, Spring configuration properties) moves to Quarkus only through a port. Libraries that rely on runtime reflection or classpath scanning need checking against Quarkus's build-time model. If the team already runs Spring services, that shared knowledge and code is a concrete reason to stay.

Deployment and packaging

Spring Boot applications start with java -jar or deploy as a traditional war, and its documentation includes GraalVM native image support. Quarkus builds a native executable with ./mvnw package -Dnative or quarkus build --native, and -Dquarkus.native.container-build=true builds it inside a container when GraalVM is not installed locally. For development, ./mvnw quarkus:dev runs dev mode, where saved changes take effect without a restart.

What Better Fullstack generates

The two commands below generate a Maven project with each framework:

bash
bun create better-fullstack@latest my-api \
  --ecosystem java \
  --java-web-framework spring-boot \
  --java-build-tool maven \
  --java-orm spring-data-jpa \
  --java-testing-libraries junit5
bash
bun create better-fullstack@latest my-api \
  --ecosystem java \
  --java-web-framework quarkus \
  --java-build-tool maven \
  --java-orm none \
  --java-auth none \
  --java-api none \
  --java-libraries none

Add --dry-run to preview either project without writing files. Both target Java 21 and include the Maven wrapper; --java-build-tool gradle produces a Gradle Kotlin DSL build instead. The outputs are not the same size:

ConcernSpring Boot commandQuarkus command
Platformspring-boot-starter-parent 4.0.6 with spring-boot-starter-webmvcQuarkus BOM 3.35.2 with quarkus-rest
EndpointsGET /health, plus GET and POST /usersGET /hello returning plain text
Data layerAppUser entity, repository, and service with Spring Data JPAnone
Configurationapplication.yml reading PORT and a local .envno application.properties; CORS origin read from config
Testsa @SpringBootTest context-load testquarkus-junit5 dependency, but no test class
Run./mvnw spring-boot:run./mvnw quarkus:dev

Two limits shape this comparison:

  • The Quarkus option is a single REST resource. The CLI accepts --java-orm, --java-auth, --java-api, and --java-libraries only with Spring Boot, which is why the Quarkus command sets each of them to none. Persistence, security, and any other Quarkus extensions are yours to add.
  • The Spring Boot data layer runs on an embedded H2 database. The datasource is an H2 file under ./data in PostgreSQL compatibility mode, with ddl-auto: update and the H2 console enabled. Adding --database postgres does not change that output; add the PostgreSQL JDBC driver and datasource settings yourself before connecting to a real PostgreSQL server.

The Spring-only options are where Spring Boot's generated surface grows: --java-orm offers spring-data-jpa, jooq, and mybatis; --java-auth offers spring-security and keycloak; and --java-api offers spring-graphql, openapi-generator, and grpc.

These are generated projects, not runtime-verified deployments. Run ./mvnw test and start the service before committing to either.

Decision table

If this is the stronger constraintStart with
Existing Spring code, Spring Data, or Spring SecuritySpring Boot
Generated persistence, security, or API options todaySpring Boot
Native executables and build-time processing as a primary goalQuarkus, adding persistence yourself
Reactive endpoints chosen per methodQuarkus, or Spring WebFlux
UnsureBuild one endpoint with real data access in both
GitHub Sponsors