Vaadin is the framework many Java teams reach for when they want a web UI without writing JavaScript: you write components in Java, and Vaadin keeps the browser in sync. It runs on any servlet container. Vidocq now has one of its own, Foy, which implements Servlet 6.1. So the question was obvious: does Vaadin run on Vidocq?
It does, and without a single line of glue in your application. It runs under vidocq:dev and vidocq:run, ships
as a vidocq:package distribution, and links into a vidocq:jlink image of 68 MB that brings its own JDK, where the
runtime is ready in about a third of a second. This post shows how to set it up and what each way of running it gives you. It also
tells the story behind it: Vaadin was built for the class path, Vidocq for Java modules, and getting the two to
agree took more fixes than we expected.
The setup
A Vaadin application on Vidocq is an ordinary Vidocq application, with vidocq-runtime-parent as its parent, and
four more pieces: the Servlet extension, the Vaadin extension, the Vaadin BOM (com.vaadin:vaadin-bom, imported in
dependencyManagement), and Vaadin’s Maven plugin to build the frontend. Here is the core of the pom:
<properties>
<!-- The application's Java module: the runtime's own main boots it, no main class needed. -->
<vidocq.mainModule>com.example.app</vidocq.mainModule>
<vidocq.app.layer>all</vidocq.app.layer>
<!-- Vaadin's production mode under vidocq:dev, vidocq:run and in the vidocq:package launch scripts. -->
<vidocq.dev.jvmArgs>-Dvaadin.productionMode=true</vidocq.dev.jvmArgs>
<vidocq.run.jvmArgs>-Dvaadin.productionMode=true</vidocq.run.jvmArgs>
<vidocq.jvmArgs>-Dvaadin.productionMode=true</vidocq.jvmArgs>
</properties>
<dependencies>
<dependency>
<groupId>io.vidocq.runtime</groupId>
<artifactId>vidocq-runtime-core</artifactId>
</dependency>
<dependency>
<groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
<artifactId>vidocq-runtime-foy-servlet-extension</artifactId>
</dependency>
<!-- Lets Vaadin find your AppShellConfigurator (theme, stylesheets) in a jlink image. -->
<dependency>
<groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
<artifactId>vidocq-runtime-vaadin-extension</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
</dependency>
<!-- The precompiled frontend of the core components: production mode without Node. -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-prod-bundle</artifactId>
<version>${vaadin.version}</version>
</dependency>
</dependencies>
On top of that, two plugins run: vaadin-maven-plugin with prepare-frontend and build-frontend, and
vidocq-runtime-maven-plugin with generate. The parent manages every Vidocq version, and the Vidocq plugin puts
Foy’s annotation processors on the compiler’s path by itself: an extension is one dependency, with no
annotationProcessorPaths to write. There is no main class either. The application code is plain Vaadin:
@Route("")
public class MainView extends VerticalLayout {
public MainView() {
TextField name = new TextField("Your name");
Paragraph greeting = new Paragraph();
add(new H1("Vidocq + Vaadin"), name,
new Button("Say hello", e -> greeting.setText("Hello " + name.getValue())), greeting);
}
}
The full pom and the module-info are in the
Vaadin guide of the documentation, and
VidocqTools/vaadin-on-vidocq is a project ready to clone.
Note the one property at the top, vidocq.app.layer=all. We come back to it below, because it is the single setting
Vaadin cannot do without.
Four ways to run it
| Goal | What you get | Start |
|---|---|---|
|
The application in a child JVM, reloaded in place on every change |
~330 ms |
|
The same child JVM, without the reload, as in production |
~310 ms |
|
A distribution with |
~310 ms |
|
A self-contained image with its own JDK, 68 MB |
~330 ms |
The start times are the runtime’s own Started in line, measured on an Apple M5 Max with Temurin 25. The figures for
the distribution and the image are the median of five starts. The JVM adds about 150 ms around them. All of it was
measured in Vaadin’s production mode (-Dvaadin.productionMode=true).
Development: vidocq:dev and vidocq:run
./mvnw clean package
./mvnw vidocq:dev
Build once first: vidocq:dev and vidocq:run neither build Vaadin’s frontend nor process the Vaadin jars, so on an
empty target/ the page answers 404 or 500. The jvmArgs properties of the pom start Vaadin in production mode.
This is the everyday loop. The runtime sits on the module path, and your application and its libraries live in a
layer above it, which vidocq:dev throws away and rebuilds on each change. That is the same in-JVM reload as for any
Vidocq application; Vaadin’s 69 jars ride along.
Shipping: vidocq:package
./mvnw clean package vidocq:package
target/my-app-1.0/bin/my-app.sh
You get a folder you can zip and copy anywhere a JDK 25 is installed. The runtime goes in lib/, and your
application and its libraries go in app/. The script starts the runtime and hands it app/.
Self-contained: vidocq:jlink
./mvnw -Pjlink clean package vidocq:jlink
JDK_JAVA_OPTIONS=-Dvaadin.productionMode=true target/dist/bin/my-app
jlink builds a trimmed JDK that contains exactly the modules your application needs, with your application inside.
The result runs on a machine with no Java installed. The catch is that jlink only accepts named Java modules, and
not one Vaadin jar is a named module. This is where most of the work went, and why the jlink profile adds
vauban:modularize, whose version the parent manages, and two flags, vidocq.jlink.mergeSharedResources and
vauban.modularize.forceExplicit. All three are explained below; the guide has the profile.
The Vaadin extension of the setup matters here. Vaadin finds your AppShellConfigurator, the class that carries a
theme or stylesheets, through ServiceLoader from flow-server, which an image cannot allow: without the extension,
the first request never answers. It changes nothing under the other goals. The guide explains why:
An app shell in a jlink image.
|
Note
|
Update 2026-10-25: this post now shows the 0.4.0 setup. The parent wires the codegen bundles and manages
vauban-maven-plugin, the application needs no main class, vidocq-runtime-vaadin-extension joins the setup, and
the project is built once before vidocq:dev or vidocq:run.
|
Why Vaadin needs vidocq.app.layer=all
Vidocq starts your application in a module layer of its own, above the runtime’s layer. By default, that layer holds your classes only, and your libraries stay with the runtime.
Vaadin does not like that. At startup it looks for your application’s resources, such as
META-INF/VAADIN/webapp/index.html, through the class loader that loaded Vaadin itself. With Vaadin in the lower
layer, your resources in the upper layer are out of its sight. Every request then answers 500: Unable to find
index.html.
vidocq.app.layer=all moves every library outside the runtime into your application’s layer. A servlet container
does the same with WEB-INF/lib: the framework and the application share a loader. Set the property once in the pom
and every goal uses it.
What we had to fix on the way
Making this work took about a dozen fixes across Vidocq, Vauban (our CDI container, which also hosts the module tooling) and Foy. A few of them are worth telling, because they show where a class-path framework and a module-path runtime disagree.
Your dependencies are your code
Vaadin registers itself through a @WebListener and a few ServletContainerInitializer classes inside flow-server.
Foy, like most of Vidocq, discovers such components at build time, with annotation processors that run when your
code compiles. But flow-server is not your code: it was compiled long ago, by Vaadin, without Foy’s processors.
So nothing found the listener, and every page answered 404. Our first spike needed an empty subclass in the
application just to be seen.
The fix is a rule we now apply everywhere: an external dependency is processed like application code.
vidocq:generate runs every brick’s processors over the dependency jars that need them, Foy’s included, and the
startup report shows it:
layer 69 modules, 68 libraries, 2 enriched (Foy 2) (vidocq.app.path, 69 archives)
The output is cached, so only the first build pays for it.
A uses directive nobody wrote
A Java module may only call ServiceLoader for a service it declares with uses in its module-info. Vaadin has
no module-info, so for jlink we generate one. Vauban’s vauban:modularize goal reads the bytecode and works out
the requires, provides and uses.
Vaadin taught that analysis three new tricks. FeatureFlags passes getClass().getClassLoader() to ServiceLoader,
which our scanner mistook for a second class constant, so it dropped the service type. Lookup.lookupAll reaches
ServiceLoader through an interface, which the scanner did not follow. And reactor-core, deep in the dependency
tree, ships service files for two optional libraries that are not there. Each one cost a boot failure, and each one
now has a test.
The two flags
The last two problems are not ours to fix. They come from how Vaadin lays out its jars, which the module system does not allow. Each flag works around one of them, and without the flag the build stops with a message that names the jars.
flow-server and flow-react both ship files in the same directory, com/vaadin/flow/server/frontend/. On the
class path, the two jars simply overlay. For the module system, though, that directory is a package, and a package
belongs to exactly one module. Two modules holding it are a split package. At run time the JVM tolerates it for a
directory of plain resources, but jlink, which has to file every resource under one module, refuses.
vidocq.jlink.mergeSharedResources moves the files into one module of the image. Only the copies staged for jlink
change, and the files involved are frontend templates that only Vaadin’s build tooling reads.
The second problem is subtler. Lookup.lookupAll lives in flow-server and calls ServiceLoader on behalf of other
jars: collaboration-engine and flow-lit-template pass it their own service types. ServiceLoader checks the
module of its caller, so flow-server would have to declare uses for types defined in modules that themselves
depend on flow-server. That needs a dependency in both directions, a cycle, and Java modules forbid cycles. No
legal module-info exists for flow-server in such an application. vauban.modularize.forceExplicit writes one
anyway, without the impossible directives. The image works as long as your application does not use Collaboration
Engine or LitTemplate.
A container that could not see into the image
With all of that in place, the first jlink image still failed at startup. Vaadin’s initializer received null
instead of the classes it had asked for. Foy finds those classes by scanning jars, and inside a jlink image there
are no jars: modules live in the image’s own jrt: file system, which Foy did not walk. Foy now does, and the image
serves its first page.
What we would like from Vaadin
Both flags exist only because of two layouts in Vaadin. We have collected what only Vaadin can change in one issue, before taking it upstream:
-
keep each resource directory in a single jar;
-
let the module that owns a service type call
ServiceLoaderitself, rather thanLookupcalling it for everyone; -
stable module names: an
Automatic-Module-Namein every jar, or better a realmodule-info; -
look up the application’s resources through the context class loader, which would make
vidocq.app.layer=alloptional.
None of this blocks you today. These are the changes that would make Vaadin a first-class citizen of the module path, for us and for everyone else.
Try it
Everything here is on the 0.4.0 development line. Use the
snapshot builds until 0.4.0 is released, and follow the
Vaadin guide. It has the full pom, the module-info, production
mode for each goal, and the known limits. So far we have checked production mode on a single view: the page and its
bundles. Push, websockets and real applications are next, and we would love to hear how yours behaves.