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.

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