While Vaadin is commonly integrated with Spring, this article explores a powerful new full-stack solution: Vaadin with Quarkus. Quarkus provides a lightning-fast runtime and native images, complementing Vaadin’s Java-first UI, which eliminates JavaScript complexity. Readers will learn how to build scalable, reactive Vaadin Flow apps on Quarkus, covering simplified backend development, enhanced productivity with a unified Java stack, and superior deployment optimization compared to traditional approaches.
Introduction
Vaadin and Quarkus are both modern Java frameworks, each excelling in its domain: Vaadin for building rich web UIs in pure Java, and Quarkus for creating cloud-native, container-first Java applications. Together, they offer a compelling alternative to traditional full-stack Java development, especially for enterprise applications.
This article will guide you through setting up a Vaadin Flow application on Quarkus, demonstrating how to leverage Quarkus’ reactive capabilities and Vaadin’s component-based UI to build scalable, maintainable, and high-performance enterprise apps.
Why Quarkus for Vaadin Developers?
Vaadin applications, by nature, could be memory-heavy because they maintain UI state on the server. Quarkus mitigates this by optimizing the underlying Jakarta EE and MicroProfile stacks.
| Capability | Spring + Vaadin | Quarkus + Vaadin |
| Startup Time | Seconds | Milliseconds |
| Memory Footprint | Medium to High | Very Low |
| Native Images | Possible / Complex | First-class |
| Dev Mode | Good | Exceptional |
| Reactive Core | Optional | Built-in |
Vaadin applications, by nature, could be memory-heavy because they maintain UI state on the server. Quarkus mitigates this by optimizing the underlying Jakarta EE and MicroProfile stacks.
Project Setup
Prerequisites
- JDK 17+
- Maven 3.8+
- Your favourite IDE
- Quarkus CLI (optional)
Create a Quarkus Project
Start by generating your project using the Quarkus CLI:
mvn io.quarkus.platform:quarkus-maven-plugin:3.9.0:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=vaadin-with-quarkus \
-DclassName="com.example.vaadin.MainLayout" \
-Dpath="/" \
-Dextensions="com.vaadin:vaadin-quarkus-extension,quarkus-hibernate-orm,quarkus-hibernate-orm-panache,quarkus-jdbc-h2"
cd vaadin-with-quarkus
quarkus dev
Or use the code.quarkus.io online tool to generate a new Quarkus project with the Vaadin extension:

This will create a standard Quarkus project with a simple landing page and Hibernate dependencies installed.
Note: If the Vaadin Quarkus extension is not available in the registry, you can manually add the following dependencies to your pom.xml.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Vaadin Quarkus Extension - single dependency for full integration -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-quarkus-extension</artifactId>
</dependency>
<!-- Quarkus Panache for simplified JPA -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<!-- H2 in-memory DB for development -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
</dependencies>
Replace the ${quarkus.version} and ${vaadin.version} accordingly.
Run in DEV Mode
quarkus dev
Open http://localhost:8080. You should see the Quarkus Getting Started page.

Building the Application
We’ll build a small Product Catalogue application that demonstrates Vaadin’s component model, CDI injection, Quarkus services, and Hibernate ORM, all within a single Java project.
The Domain Model
We’ll create Product entity. Using Quarkus Panache, we can simplify the DAO layer by extending PanacheEntity.
src/main/java/com/example/demo/Product.java
package com.example.example;
import java.math.BigDecimal;
import java.util.List;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "products")
public class Product extends PanacheEntity {
// 'id' (Long) is inherited from PanacheEntity
@Column(nullable = false, unique = true)
public String sku;
@Column(nullable = false)
public String name;
@Column(nullable = false, precision = 10, scale = 2)
public BigDecimal price;
@Column(nullable = false)
public int stock = 0;
// --- Named queries as static helpers ---
public static List<Product> listAllOrdered() {
return list("ORDER BY name");
}
}
CDI Service (Backend Logic)
Quarkus uses standard CDI. @ApplicationScoped marks this service as a singleton; @Transactional handles transactions declaratively.
src/main/java/com/example/demo/ProductService.java
package com.example.example;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
@ApplicationScoped
public class ProductService {
public List<Product> findAll() {
return Product.listAllOrdered();
}
@Transactional
public void save(Product product) {
if (product.id == null) {
// New entity — no id yet, safe to persist (INSERT)
product.persist();
} else {
// Detached entity — re-attach and flush changes (UPDATE)
Product.getEntityManager().merge(product);
}
}
@Transactional
public void delete(Long id) {
Product.deleteById(id);
}
@Transactional
public void adjustStock(Long id, int delta) {
Product p = Product.findById(id);
if (p != null) {
p.stock = Math.max(0, p.stock + delta);
}
}
}
The Interactive UI (The Vaadin Side)
Now for the exciting part: the UI. Vaadin views are just Java classes annotated with @Route. No HTML to write, no JavaScript event handlers, no REST endpoints to consume. We will inject our ProductService directly into the view.
src/main/java/com/example/demo/ProductView.java
package com.example.demo;
import java.math.BigDecimal;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.BigDecimalField;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import jakarta.inject.Inject;
@Route("products")
@PageTitle("Products")
public class ProductView extends VerticalLayout {
private final ProductService productService;
private final Grid<Product> grid = new Grid<>(Product.class, false);
@Inject
public ProductView(ProductService productService) {
this.productService = productService;
setSizeFull();
add(new H2("Product Catalogue"));
add(buildToolbar());
add(buildGrid());
refresh();
}
private HorizontalLayout buildToolbar() {
Button addBtn = new Button("Add Product", e -> openDialog(null));
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
return new HorizontalLayout(addBtn);
}
private Grid<Product> buildGrid() {
grid.addColumn(p -> p.sku).setHeader("SKU").setWidth("120px");
grid.addColumn(p -> p.name).setHeader("Name").setFlexGrow(1);
grid.addColumn(p -> "€" + p.price).setHeader("Price");
grid.addColumn(p -> p.stock).setHeader("Stock");
grid.addComponentColumn(p -> {
Button edit = new Button("Edit", e -> openDialog(p));
Button del = new Button("Delete", e -> {
productService.delete(p.id);
refresh();
Notification.show("Deleted " + p.name);
});
Button inc = new Button("+1", e -> {
productService.adjustStock(p.id, 1);
refresh();
});
Button dec = new Button("-1", e -> {
productService.adjustStock(p.id, -1);
refresh();
});
edit.addThemeVariants(ButtonVariant.LUMO_SMALL);
del.addThemeVariants(
ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_SMALL);
inc.addThemeVariants(
ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_SUCCESS);
dec.addThemeVariants(ButtonVariant.LUMO_SMALL);
return new HorizontalLayout(edit, del, inc, dec);
}).setHeader("Actions");
grid.setSizeFull();
return grid;
}
private void openDialog(Product existing) {
Dialog dialog = new Dialog();
dialog.setHeaderTitle(existing == null ? "New Product" :
"Edit Product");
TextField skuField = new TextField("SKU");
TextField nameField = new TextField("Name");
BigDecimalField priceField = new BigDecimalField("Price (€)");
IntegerField stockField = new IntegerField("Initial Stock");
stockField.setMin(0);
if (existing != null) {
skuField.setValue(existing.sku);
nameField.setValue(existing.name);
priceField.setValue(existing.price);
stockField.setValue(existing.stock);
}
dialog.add(new VerticalLayout(skuField, nameField,
priceField, stockField));
Button save = new Button("Save", e -> {
if (skuField.isEmpty() || nameField.isEmpty()
|| priceField.isEmpty()) {
Notification.show("All fields are required");
return;
}
Product p = existing != null ? existing : new Product();
p.sku = skuField.getValue();
p.name = nameField.getValue();
p.price = priceField.getValue() != null ?
priceField.getValue() : BigDecimal.ZERO;
p.stock = stockField.getValue() != null ?
stockField.getValue() : 0;
productService.save(p);
refresh();
dialog.close();
Notification.show("Saved " + p.name);
});
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
dialog.getFooter().add(new Button("Cancel",
e -> dialog.close()), save);
dialog.open();
}
private void refresh() {
grid.setItems(productService.findAll());
}
}
Application Configuration
Quarkus uses application.properties for all runtime configurations. Place this in src/main/resources/:
# Vaadin
vaadin.launch-browser=true
# Quarkus HTTP
quarkus.http.port=8080
quarkus.live-reload.instrumentation=true
# Database (H2 in-memory for demo)
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:products-db
quarkus.hibernate-orm.database.generation=drop-and-create
Running the Application
Development Mode
Quarkus dev mode provides hot reload for Java and Vaadin:
mvn quarkus:dev
Visit http://localhost:8080/products. Changes to services or views are reflected without a restart.



JVM Production Build
Building for production is now a single Maven command. For a standard JVM deployment:
mvn package -Pproduction
java -jar target/quarkus-app/quarkus-run.jar
Native Executable
GraalVM native compilation produces a self-contained executable with near-instant startup:
mvn package -Pnative -Dquarkus.native.container-build=true
./target/vaadin-with-quarkus-1.0.0-SNAPSHOT-runner
A native Quarkus + Vaadin app starts in under 100ms using 80–120MB of heap versus 500–800MB for an equivalent Spring Boot JVM deployment.
Wrap Up
Why This Matters
- No controllers
- No REST boilerplate
- No JavaScript
- Fully type-safe end-to-end Java
CDI vs Spring
| Concept | Spring | Quarkus |
| Dependency Injection | @Autowired | @Inject |
| Scope | @Service | @ApplicationScoped |
| Startup | Reflection-heavy | Build-time optimized |
Dev Mode Experience
- Hot reload for Java and UI
- No restart on view changes
- Browser auto-launch
- Near-instant feedback
Native Image Build
Typical results:
- Startup: < 100ms
- Memory: < 120MB
- Perfect for serverless, edge, and Kubernetes
Key Takeaways
- A unified Java stack from UI to backend services
- Faster startup times and reduced memory usage compared to traditional frameworks
- A simpler dependency injection model using Jakarta CDI
- A powerful live-reload development workflow
- Seamless native image support for cloud-native deployments
Conclusion
Vaadin and Quarkus together provide a modern, efficient, and scalable full-stack Java solution for enterprise applications. By unifying the Java stack, you reduce complexity, improve productivity, and optimize deployment, making it an excellent choice for your next project.

This article is part of the JAVAPRO magazine issue:
Engineering Decisions
Choose the right technology for the right problem.
Explore practical guidance on modern Java, AI, cloud architectures, APIs, and software design to build maintainable, secure, and future-ready applications.
Discover the edition →