Vaadin + Quarkus: The New Approach for Enterprise Apps

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.

CapabilitySpring + VaadinQuarkus + Vaadin
Startup TimeSecondsMilliseconds
Memory FootprintMedium to HighVery Low
Native ImagesPossible / ComplexFirst-class
Dev ModeGoodExceptional
Reactive CoreOptionalBuilt-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

ConceptSpringQuarkus
Dependency Injection@Autowired@Inject
Scope@Service@ApplicationScoped
StartupReflection-heavyBuild-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 →

Total
0
Shares
Previous Post

OpenFeature – one flag to rule them all!

Next Post

Will AI Replace Me? Understanding Our Value in the AI Era

Related Posts