DEV Community

Cover image for Understanding Quarkus: Rethinking Java for Cloud-Native Applications
Prabhjeet Singh
Prabhjeet Singh

Posted on

Understanding Quarkus: Rethinking Java for Cloud-Native Applications

Here is my naive attempt at explaining Quarkus and its ecosystem. In the previous post, we discussed what caused the need of a different approach to java web applications.

Before we begin, I want to clarify, that by no means, I mean to impose Quarkus' preference over Spring Boot. Infact, I get amazed by Spring internals, their design philosophy and seamless developer experience. Spring remains the primary choice for various usecases.

One such example is Monoliths and long-running services where initial startup time is less critical than sustained high throughput and stability over extended periods.

Also, Spring remains a developer's top choice, owning to convenience and large ecosystem while Quarkus ecosystem is small but growing.

Now let's comeback to the topic.

Here, you will be introduced to CDI.

CDI stands for Contexts and Dependency Injection. It is a standard Jakarta EE (formerly Java EE) specification that simplifies the development of complex applications and ensures object lifecycle management. Please refer here for more.

While both CDI and the Spring IoC container manage bean lifecycles and dependencies, they differ fundamentally in their underlying philosophy and technical implementation.

CDI is a specification which specifies a set of APIs that different vendors implement. Spring IoC on the other hand is a proprietary framework.

Quarkus uses a hybrid approach and implements its Dependency Injection framework on top of CDI. It is known as ArC. Refer this for more.

ArC is designed for build time analysis. This design choice is central to the Quarkus philosophy of moving as much processing as possible from runtime to build time to achieve extremely fast startup times and low memory usage.

Let's see what does that imply.

The entire dependency graph is analyzed and resolved during the build process and not at application startup.

Instead of using runtime reflection, ArC generates static proxies and direct bytecode invocations during the build, significantly reducing memory overhead and startup time.

This is enough if you want Quarkus to run in JVM mode, but, Quarkus has the option to go beyond build time analysis for optimizations and choose native mode if startup time is critical for an app.

Quarkus supports native mode. To accomplish this, it uses GraalVM.

When you compile to a native binary via GraalVM, all unused code is removed through ahead-of-time(AOT) compilation.

AOT compilation is a technique where your application's source code or bytecode is translated into native machine code before the program is run, typically during the build phase.

It is efficient in comparison to traditional Just in time(JIT) compilation where bytecode is translated to machine code at runtime.

Even though GraalVM's static world assumption is wonderful. At times, it may not be enough. Let's understand it with an example.

In JVM mode, reflection works normally.
In Native mode, reflection metadata must be known at build time.

Below is a CustomEvent class. when an event is received by the consumer, the json string message will need to be parsed to CustomEvent object using ObjectMapper, which requires reflection usage. GraalVM won't be able to use reflection at runtime.

public class CustomEvent {
    public String name;
    public String email;

    // No-args constructor is required for reflection
    public CustomEvent() {}  
}
Enter fullscreen mode Exit fullscreen mode

To handle runtime failures due to reflection, we annotate the above class with @RegisterForReflection, which works fine with a substrate JVM and complete JVM is not needed.

We can safely say, that life comes full circle with GraalVM. Bytecode was introduced to replace native binaries as they were platform dependent and bytecode introduced platform independence. With GraalVM, we go back to native binaries as performance is crucial in cloud platforms.

Eclipse MicroProfile is a set of open-source specifications designed to extend Jakarta EE specifically for microservices. In Quarkus, these specifications are implemented primarily via the SmallRye Project.

Now, why are microprofile or smallrye needed?

The list of projects that Smallrye implements can be seen here. SmallRye implements the MicroProfile specifications (Config, Health, Fault Tolerance)

Below is example of Spring REST

@RestController
@RequestMapping("/hello")
public class GreetingController {
    @GetMapping("/{name}")
    public String greet(@PathVariable String name) {
        return "Hello " + name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Below is example of Jakarta REST(Formerly JAX-RS), which is Quarkus Equivalent of above. Refer here for documentation.
Please note, Jakarta REST is a specification which RestEasy Reactive implements and is used by Quarkus.

@Path("/hello") // This makes it a REST controller
public class GreetingResource {
    @GET
    @Path("/{name}")
    public String greet(@PathParam("name") String name) {
        return "Hello " + name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Though, SmallRye implements Eclipse Microprofile for Health, Metrics and Fault tolerance. RestEasy is used to implement Jakarta REST. In order to glue them together, we use RestEasy Rest Client.

Below is an example of Its usage, this can be understood analogous to Feign Client's usage in Spring.

@RegisterRestClient(configKey = "user-api")
public interface UserService {

    @GET
    @Path("/users")
    @Retry(maxRetries = 3, delay = 200) // <--- MicroProfile Fault Tolerance!
    @Fallback(fallbackMethod = "getCachedUsers") // <--- Fallback if all retries fail
    List<User> getByRole(@QueryParam("role") String role);

    // Default response if the external service is down
    default List<User> getCachedUsers(String role) {
        return List.of(new User("Offline User"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Below is an example of Spring bean creation of a class present in the classpath, and not in project source code.

@Configuration //@Configuration is a stereotype behind the scenes.
public class MyConfig {
    @Bean
    public ExternalService externalService() {
        return new ExternalService();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Here is an Quarkus equivalent of the above example.

@ApplicationScoped // Just a quarkus equivalent of a Spring stereotype 
public class MyProducers {
    @Produces
    @ApplicationScoped // The scope of the bean being created
    public ExternalService externalService() {
        return new ExternalService();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Let's mention some trivia. Below are some analogies to make working with Quarkus a little familiar.

Spring Annotations Quarkus Annotations
@Autowired @Inject @Context
@Component @Service @Repository @ApplicationScoped@Singleton @RequestScoped
@Value("${...}") @ConfigProperty(name="...")
@Bean @Produces

While I feel it is already too much to pack in, I would like to touch upon a few other points.

Vert.x is an open-source, event-driven, and non-blocking toolkit for building reactive applications on the JVM.
Vert.x is the underlying reactive engine for Qaurkus. Refer guides on Introduction for Quarkus Reactive and reactive architecture.

The single line above has too much to cover. Let's understand this with a few examples.

  • Kafka traditionally uses a Java client that performs long-polling, which is also a blocking operation.
    The Client: Quarkus uses the Vert.x Kafka Client. This is a non-blocking wrapper of the Kafka protocol designed to run on the event loop.
    The Bridge: SmallRye Reactive Messaging sits on top of this Vert.x client. It uses Vert.x to handle the actual TCP connections and heartbeats to the Kafka brokers.
    The Engine's Job: Vert.x manages the continuous stream of bytes from Kafka topics. It dispatches these as "events" to your @Incoming methods. Because it's Vert.x, you can process thousands of Kafka records simultaneously without spawning thousands of threads.
    Refer here for more details.

  • Standard Hibernate (ORM) uses JDBC, which is fundamentally blocking—one thread per database connection. In a reactive app, this would kill the event loop.
    The Driver: Hibernate Reactive replaces JDBC with Vert.x Reactive SQL Clients (like vertx-pg-client for Postgres).
    The Execution: When you call session.persist(entity), Hibernate translates your Java object into a SQL query and hands it to the Vert.x SQL client.
    The Engine's Job: Vert.x sends the SQL over the network using non-blocking I/O. It doesn't wait for the DB to respond; it immediately releases the thread. When the data eventually comes back, Vert.x triggers the callback that Mutiny then turns back into your result.
    Refer vert.x and quarkus docs for details on the topic.

  • Vert.x EventLoop basically runs a thread to execute requests Non blockingly, and any tasks that need to wait are passed on to worker threads. This improves concurrent performance. Mutiny is a wrapper framework to execute non blocking I/O.
    Spring Equivalent for Vert.x EventLoop is Project Reactor, differs slightly in design philosophy with an additional layer and different concurrency strategy.

To conclude, what is Spring's answer to Quarkus?

Spring has introduced a project called Spring AOT to improve performance in cloud native environments.

While Quarkus was built cloud first, Spting's images are slightly heavier as it was a project retrofitted to support cloud environments, poles apart from their autoconfiguration magic.

Finally, I hope you find it useful. Happy learning!

Top comments (0)