<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <title>Vaadin.com Blog</title>
    <link>https://blog.vaadin.com</link>
    <description>Vaadin's blog for tips, tutorials, and insights on web app development, UI design, and Java innovations. Stay updated on trends and best practices.</description>
    <language>en</language>
    <pubDate>Wed, 03 Jun 2026 09:55:37 GMT</pubDate>
    <dc:date>2026-06-03T09:55:37Z</dc:date>
    <dc:language>en</dc:language>
    <item>
      <title>How South Tyrol modernized government software with Java and Vaadin</title>
      <link>https://blog.vaadin.com/how-south-tyrol-modernized-government-software-with-java-and-vaadin</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/how-south-tyrol-modernized-government-software-with-java-and-vaadin" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/sudtirolergemeinschaft.png" alt="How South Tyrol modernized government software with Java and Vaadin" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span style="color: #666666;"&gt;Südtiroler Gemeindenverband Genossenschaft replaced a legacy Delphi monolith with a modern cloud-native eGovernment platform built with Java, Kubernetes, and Vaadin. With a small in-house team, they delivered 16 production modules supporting municipalities across South Tyrol while preparing the platform for long-term public-sector reuse.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;span style="color: #666666;"&gt;Südtiroler Gemeindenverband Genossenschaft replaced a legacy Delphi monolith with a modern cloud-native eGovernment platform built with Java, Kubernetes, and Vaadin. With a small in-house team, they delivered 16 production modules supporting municipalities across South Tyrol while preparing the platform for long-term public-sector reuse.&lt;/span&gt;&lt;/p&gt;  
&lt;p&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-29-2026-09-07-20-3701-AM-1.png?width=364&amp;amp;height=117&amp;amp;name=undefined-May-29-2026-09-07-20-3701-AM-1.png" width="364" height="117" alt="Suedtiroler gemeindernverban genossenschaft logo" style="height: auto; max-width: 100%; width: 364px; margin-left: auto; margin-right: auto; display: block;"&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Supporting the digital infrastructure of 116 municipalities&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;a href="https://www.gvcc.net/de"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Südtiroler Gemeindenverband Genossenschaft&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; is the IT services consortium for South Tyrol’s local government and one of the few in-house organizations in Italy responsible for centralizing software development for an entire regional public administration.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Its platform, Goffice 2.0, supports:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;116 municipalities&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;7 district communities&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;roughly 5,000 daily users&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;The project is delivered by a small in-house team consisting of:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;4 Full stack Java developers: &lt;/strong&gt;Simon Nogler, Walter Zöggeler, Domingo Sacristan, and Roberto Cappuccio&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Business analyst:&lt;/strong&gt; Sara Tumiati&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Architect: &lt;/strong&gt;Renzo Poli&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Scrum Master: &lt;/strong&gt;Edoardo Scepi&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;EU-funding project manager:&lt;/strong&gt; Tarcisio Coianiz&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;Together, they build and maintain software used daily across a large public administration ecosystem.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/Michelefinal.png?width=1600&amp;amp;height=900&amp;amp;name=Michelefinal.png" width="1600" height="900" alt="Michelefinal" style="height: auto; max-width: 100%; width: 1600px; margin-left: auto; margin-right: auto; display: block;"&gt;&lt;br&gt;&lt;/span&gt;&lt;span style="color: #111827; font-family: nb_international_pro, ui-sans-serif, 'system-ui', -apple-system, 'system-ui', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; font-size: 1.66667em; letter-spacing: -0.02em; background-color: transparent;"&gt;Replacing a legacy Delphi monolith without disrupting public services&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Every working day, civil servants across South Tyrol rely on Goffice to manage taxes, permits, invoicing, digital signatures, and other core government processes.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;But the Delphi-based system behind the platform was becoming increasingly difficult to maintain and modernize. The old platform had become harder to evolve at a time when municipalities were under growing pressure to digitize public services and integrate with national eGovernment systems.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The application was built as a traditional client-server monolith with:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;no responsive web interface&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;limited interoperability&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;growing maintenance complexity&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;difficult integration with modern public-sector infrastructure&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;Replacing it was unavoidable. The challenge was how to modernize critical government software without disrupting public services, while working within fixed public-sector budgets and a small in-house development team.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Instead of outsourcing the project, Südtiroler Gemeindenverband Genossenschaft rebuilt the platform themselves.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;span style="font-weight: bold;"&gt;In five years, the team delivered 16 production modules on a modern cloud-native architecture using Java, Kubernetes, and Vaadin&lt;/span&gt;. The result is &lt;/span&gt;&lt;a href="https://goffice.it/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Goffice 2.0, an open-source eGovernment platform&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; designed to support South Tyrol’s municipalities through at least 2030.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 624px; height: 340px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-29-2026-09-07-19-8958-AM.png?width=1882&amp;amp;height=1026&amp;amp;name=undefined-May-29-2026-09-07-19-8958-AM.png" width="1882" height="1026" style="margin: 53px auto 25px; display: block;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;  
&lt;p style="font-size: 16px;"&gt;&lt;em&gt;The Goffice 2.0 platform helps municipalities manage taxes, billing, and public services through a modern web interface built with Vaadin.&lt;/em&gt;&lt;/p&gt;  
&lt;h2&gt;&lt;span style="letter-spacing: -0.02em; background-color: transparent;"&gt;Building a modern eGovernment platform with a seven-person team&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The modernization effort required more than rewriting software. The organization restructured from vertical silos into horizontal Scrum teams and hired a dedicated architect to guide the transition to full stack and cloud-native development.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Developers also received internal training to accelerate the move into the modern Java ecosystem without relying heavily on external consulting.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Migration happened incrementally. The team started with smaller, lower-risk modules first, building reusable patterns and confidence before tackling more complex systems.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;One of the most important technical decisions was investing early in a shared framework layer for reusable forms, grids, views, authentication, and common UI components. That internal investment helped the team maintain consistency across modules while staying productive with a relatively small development organization.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Today, the platform includes 16 production modules running in a cloud-native environment across 116 public administrations.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 624px; height: 336px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-29-2026-09-07-18-6738-AM.png?width=1906&amp;amp;height=1026&amp;amp;name=undefined-May-29-2026-09-07-18-6738-AM.png" width="1906" height="1026" style="margin: 53px auto 31px; display: block;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="font-size: 16px;"&gt;&lt;em&gt;Screenshot of the Goffice 2.0 invoicing module showing a multi-step invoice creation workflow with customer information, billing details, document metadata, and electronic transmission fields.&lt;/em&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Why the team chose full stack Java and Vaadin&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The team evaluated Angular during the planning phase but quickly realized the frontend complexity would be difficult to manage for such a small group working across a large and constantly evolving public-sector domain.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;There was also a practical human challenge. Most developers had spent more than a decade working in Delphi, a component-based and event-driven development environment. At the same time, the organization was already transitioning to Kubernetes, microservices, cloud-native architecture, and modern DevOps workflows.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The team needed a UI framework that would not force a complete cognitive reset on top of an already demanding modernization effort.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Vaadin provided a development model that felt familiar enough to Delphi to keep developers productive while modernizing the rest of the stack. A nearby company that had already used Vaadin to modernize a comparable client-server application also helped validate the decision before the team committed.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;At the same time, the project had strict European Regional Development Fund (ERDF) requirements. The platform needed to be:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;cloud-native&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;modular&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;open source&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;interoperable with national systems&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;sustainable for long-term in-house maintenance&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;It also required integrations with key Italian public-sector infrastructure, including SPID, PagoPA, FatturaPA, and national digital identity services.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/op3.png?width=1600&amp;amp;height=900&amp;amp;name=op3.png" width="1600" height="900" alt="op3" style="height: auto; max-width: 100%; width: 1600px; margin: 32px 0px 16px;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Modernizing one of the region’s most complex public-sector systems&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The most technically demanding migration so far has been the tax module, which manages tax and fee processes for all 116 municipalities.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The system includes waste management, water services, electronic invoicing, SEPA integrations, PagoPA integrations, and national registry integrations, making it the team’s most complex Delphi-to-Vaadin migration to date.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 582px; height: 314px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-29-2026-09-07-19-4744-AM.png?width=1915&amp;amp;height=1027&amp;amp;name=undefined-May-29-2026-09-07-19-4744-AM.png" width="1915" height="1027" style="margin: 32px auto 18px; display: block;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="font-size: 16px;"&gt;&lt;em&gt;Screenshot of the Goffice 2.0 client management module showing a form for creating and maintaining citizen records, including personal information, address details, contact information, and administrative data.&lt;/em&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;From legacy monolith to modern public platform&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Today, Goffice 2.0 runs 16 production modules across 116 public administrations in a cloud-based environment with near-24/7 availability.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Michele Tais estimates that &lt;span style="font-weight: bold;"&gt;delivering the same scope using the previous Delphi stack would have taken up to 30% longer&lt;/span&gt;, which made a real difference for a small public-sector team operating under fixed budgets and EU funding timelines.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The platform has also supported seven successful NextGen EU funding applications, helping municipalities secure substantial public investment for digital modernization initiatives.&lt;/span&gt;&lt;/p&gt;   
&lt;p&gt;For the roughly 5,000 civil servants using the platform daily, the improvements are visible in everyday work. End-user feedback consistently describes the new system as:&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;Cleaner&lt;/li&gt; 
 &lt;li&gt;More responsive&lt;/li&gt; 
 &lt;li&gt;Easier to navigate&lt;/li&gt; 
 &lt;li&gt;More modern than the previous platform&lt;/li&gt; 
&lt;/ul&gt;  
&lt;p&gt;&lt;span&gt;Goffice 2.0 has also been registered in Italy’s national software reuse catalogue (AGID), allowing other public administrations across the country to adopt the platform directly.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/02.jpg?width=1882&amp;amp;height=1026&amp;amp;name=02.jpg" width="1882" height="1026" alt="02" style="height: auto; max-width: 100%; width: 1882px; margin: 32px 0px 22px;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="font-size: 16px;"&gt;&lt;em&gt;The Goffice 2.0 citizen portal gives residents secure access to contracts, documents, and property information, reducing administrative overhead for municipalities.&lt;/em&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 624px; height: 340px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-29-2026-09-07-19-1704-AM.jpeg?width=1882&amp;amp;height=1026&amp;amp;name=undefined-May-29-2026-09-07-19-1704-AM.jpeg" width="1882" height="1026" style="margin: 53px auto 36px; display: block;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;  
&lt;p style="font-size: 16px;"&gt;&lt;em&gt;The Goffice 2.0 citizen portal enables residents to access municipal services and manage payments online.&lt;/em&gt;&lt;/p&gt;  
&lt;h2&gt;&lt;span&gt;Building public-sector software designed for reuse&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The next major migration projects include building management and business licensing systems, planned over the next three to four years. The team continues to build new modules with Vaadin as part of its long-term platform strategy.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;In 2027, Südtiroler Gemeindenverband Genossenschaft plans to publish the full Goffice 2.0 architecture and source code publicly, allowing municipalities across Italy to reuse and contribute to the platform.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Planning a public-sector modernization project?&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Learn how Vaadin helps teams replace legacy systems with modern full-stack Java applications.&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fhow-south-tyrol-modernized-government-software-with-java-and-vaadin&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>java</category>
      <category>fullstack</category>
      <category>application modernization</category>
      <category>success stories</category>
      <category>industry/Public Administration</category>
      <category>category/customer stories</category>
      <category>Vaadin 25</category>
      <pubDate>Fri, 29 May 2026 13:12:04 GMT</pubDate>
      <author>lilli@vaadin.com (Lilli Salo)</author>
      <guid>https://blog.vaadin.com/how-south-tyrol-modernized-government-software-with-java-and-vaadin</guid>
      <dc:date>2026-05-29T13:12:04Z</dc:date>
    </item>
    <item>
      <title>A more Playwright-like API for Vaadin browserless tests</title>
      <link>https://blog.vaadin.com/a-more-playwright-like-api-for-vaadin-browserless-tests</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/a-more-playwright-like-api-for-vaadin-browserless-tests" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/A%20more%20Playwright-like%20API%20for%20Vaadin%20browserless%20tests.png" alt="A more Playwright-like API for Vaadin browserless tests" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;In &lt;/span&gt;&lt;a href="https://vaadin.com/blog/vaadin-25-1-release"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin 25.1&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; the browserless-test module became part of the Apache 2 licensed open core framework. We believe it's on track to become a key part of the agentic-coding setup for Vaadin apps. In the upcoming Vaadin 25.2 we're bringing in API additions that make its usage more flexible and intuitive.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;span&gt;In &lt;/span&gt;&lt;a href="https://vaadin.com/blog/vaadin-25-1-release"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin 25.1&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; the browserless-test module became part of the Apache 2 licensed open core framework. We believe it's on track to become a key part of the agentic-coding setup for Vaadin apps. In the upcoming Vaadin 25.2 we're bringing in API additions that make its usage more flexible and intuitive.&lt;/span&gt;&lt;/p&gt;  
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;drops the mandatory test base class &lt;/strong&gt;— your test class extends whatever your project conventions ask for (Spring, JUnit base of your own, nothing at all).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;collapses query and interaction into one step&lt;/strong&gt; — &lt;code&gt;findButton()&lt;/code&gt; returns a ready-to-use type for testing instead of a raw component reference you have to wrap with &lt;code&gt;test(...)&lt;/code&gt;.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;makes multi-user and multi-window first-class&lt;/strong&gt; — the three-level context structure (&lt;code&gt;app → user → ui&lt;/code&gt;) is the same surface a real Vaadin app has, so simulating two users sharing a session or one user opening two windows is just driving the UIs via two different handles.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;The existing API continues to work; adoption of the new mode can happen iteratively. API can be tested using &lt;span style="font-weight: bold;"&gt;Vaadin 25.2.0-beta1&lt;/span&gt;. This post walks through what the new style API looks like and why we think this direction is worth committing to.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;A short example&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Here's a small test in the new shape — no class hierarchy, the &lt;code&gt;try-with-resources&lt;/code&gt; owns the Vaadin environment for the test:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;@Test&lt;/span&gt;&lt;br&gt;&lt;span&gt;void signIn_validCredentials_navigatesHome() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; try (var app = BrowserlessApplicationContext.create("com.acme.myapp")) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var ui = app.newUser().newWindow();&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.navigate(LoginView.class);&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; ui.findTextField().withLabel("Email").setValue("ada@example.com");&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.findTextField().withLabel("Password").setValue("hunter2");&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.findButton().withCaption("Sign in").click();&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; Assertions.assertInstanceOf(HomeView.class, ui.getCurrentView());&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;A few things to notice: no &lt;code&gt;extends BrowserlessTest&lt;/code&gt;, no field-grabbing through a view reference, no &lt;code&gt;test(component).setValue(...)&lt;/code&gt; wrapping step between the query and the interaction. The two key types that make this work are introduced next.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Contexts&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;A Browserless test now has a clear three-level structure that mirrors what a real Vaadin app has:&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse; width: 100.047%; border-color: #ffffff; height: 339.078px;"&gt;
  &lt;colgroup&gt;
   &lt;col style="width: 14.463%;" width="107"&gt;
   &lt;col style="width: 40.0717%;" width="268"&gt;
   &lt;col style="width: 45.5119%;" width="249"&gt;
  &lt;/colgroup&gt; 
  &lt;thead&gt; 
   &lt;tr style="height: 33.1953px;"&gt; 
    &lt;th style="vertical-align: top; border: 1.33333px solid #000000; text-align: center; height: 33.1953px; border-color: #FFFFFF;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px; font-weight: bold;"&gt;Concept&lt;/p&gt; &lt;/th&gt; 
    &lt;th style="vertical-align: top; border: 1.33333px solid #ffffff; text-align: center; height: 33.1953px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px; font-weight: bold;"&gt;Type&lt;/p&gt; &lt;/th&gt; 
    &lt;th style="vertical-align: top; border: 1.33333px solid #ffffff; text-align: center; background-color: #ffffff; height: 33.1953px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px; font-weight: bold;"&gt;What it represents&lt;/p&gt; &lt;/th&gt; 
   &lt;/tr&gt; 
  &lt;/thead&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 185.32px;"&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #ffffff; height: 185.32px; text-align: left;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Application&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #ffffff; height: 185.32px; text-align: left;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;code&gt;&lt;span&gt;BrowserlessApplicationContext&lt;/span&gt;&lt;/code&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #000000; height: 185.32px; border-color: #FFFFFF;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;the Vaadin service + routes — shared across users. There are &lt;code&gt;SpringBrowserlessApplicationContext&lt;/code&gt; and &lt;code&gt;QuarkusBrowserlessApplicationContext&lt;/code&gt; factories to help working with the common dependency injection frameworks.&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 63.3906px;"&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #ffffff; height: 63.3906px; text-align: left;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;User&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #ffffff; height: 63.3906px; text-align: left;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;code&gt;&lt;span&gt;BrowserlessUserContext&lt;/span&gt;&lt;/code&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #000000; height: 63.3906px; border-color: #FFFFFF;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;one user session with their own security state&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 57.1719px;"&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #ffffff; height: 57.1719px; text-align: left;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;UI&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #ffffff; height: 57.1719px; text-align: left;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;code&gt;&lt;span&gt;BrowserlessUIContext&lt;/span&gt;&lt;/code&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1.33333px solid #000000; height: 57.1719px; border-color: #FFFFFF;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;one open browser window for that user&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;A test reaches them through a straight three-step chain:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;try (var app = BrowserlessApplicationContext.create(routes)) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var ui = app.newUser().newWindow();&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.navigate(MyView.class);&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; // test code uses `ui` to query and act&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;&lt;code&gt;ui&lt;/code&gt; (a &lt;code&gt;BrowserlessUIContext&lt;/code&gt;) is the surface tests interact with: it's where you call &lt;code&gt;findButton(...)&lt;/code&gt;, &lt;code&gt;findTextField(...)&lt;/code&gt;, &lt;code&gt;navigate(...)&lt;/code&gt;, &lt;code&gt;fireShortcut(...)&lt;/code&gt;. It's also where Vaadin's thread-locals are activated under the hood when you switch between two ui instances — so multi-user tests don't have to think about thread-local state at all.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span style="color: #666666;"&gt;Multi-user is just two windows&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The three-level structure makes scenarios that previously required either manual session juggling or an actual browser become natural:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;try (var app = SpringBrowserlessApplicationContext.createSecured(routes, ctx)) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var alice = app.newUser("alice", "ADMIN").newWindow();&lt;/span&gt;&lt;br&gt;&lt;span&gt; var bob = app.newUser("bob", "USER").newWindow();&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="background-color: transparent; font-size: 1.125rem; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;alice.navigate(AdminView.class); // OK — has the role&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; // Navigating to admin view as regular user should through an exception&lt;/span&gt;&lt;br&gt;&lt;span&gt; Assertions.assertThrows(SecurityException.class,&lt;/span&gt;&lt;br&gt;&lt;span&gt; () -&amp;gt; bob.navigate(AdminView.class));&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; // Interleave freely — calling any DSL method on `alice` or `bob`&lt;/span&gt;&lt;br&gt;&lt;span&gt; // re-activates that user's security context automatically.&lt;/span&gt;&lt;br&gt;&lt;span&gt; alice.findButton().withText("Add user").click();&lt;/span&gt;&lt;br&gt;&lt;span&gt; bob.findTextField().withLabel("Search users").setValue("alice");&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;The same pattern lets a single user open multiple windows:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;var alice = app.newUser();&lt;/span&gt;&lt;br&gt;&lt;span&gt;var primary = alice.newWindow();&lt;/span&gt;&lt;br&gt;&lt;span&gt;var secondary = alice.newWindow();&lt;/span&gt;&lt;br&gt;```&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;You can use this even when "multi-user" or "multi-window" isn't the point of the test — it just turns out to be the cleanest way to compose a fresh isolated environment per test method.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span style="color: #666666;"&gt;No mandated superclass&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Notice what isn't in the snippets above: there's no &lt;code&gt;extends BrowserlessTest&lt;/code&gt;, no &lt;code&gt;@ViewPackages&lt;/code&gt; annotation requiring you to pre-list your views. The contexts handle their own lifecycle, so your test class can extend whatever your test conventions ask for (Spring's &lt;code&gt;@SpringBootTest&lt;/code&gt;, your own base class, nothing at all) without fighting the test framework. The library's lifecycle assumptions stay inside the &lt;code&gt;try-with-resources&lt;/code&gt;.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Locators&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Once you have a &lt;code&gt;ui&lt;/code&gt;, the locator API replaces the older two-step "find then wrap" pattern. The old shape looked like:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;// Old: query returns a Component, test() wraps it for interaction&lt;/span&gt;&lt;br&gt;&lt;span&gt;TextField field = $(TextField.class).withLabel("Email").first();&lt;/span&gt;&lt;br&gt;&lt;span&gt;test(field).setValue("a@b.com");&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;The new shape collapses that into one chain:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;// New: query returns a ready-to-use tester&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;TextFieldLocator emailFieldLocator = ui.findTextField().withLabel("Email");&lt;/span&gt;&lt;br&gt;&lt;span&gt;emailFieldLocator.setValue("a@b.com");&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;// Or, equivalently, as a one-liner:&lt;/span&gt;&lt;br&gt;&lt;span&gt;ui.findTextField().withLabel("Email").setValue("a@b.com");&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;&lt;code&gt;findTextField()&lt;/code&gt; returns a &lt;code&gt;TextFieldLocator&lt;/code&gt;, which is the test surface for &lt;code&gt;TextField&lt;/code&gt;. No &lt;code&gt;test()&lt;/code&gt; wrapping step. The tester methods that used to live on &lt;code&gt;TextFieldTester&lt;/code&gt; are now method calls directly on the locator. Same for every other component: &lt;code&gt;findButton()&lt;/code&gt;, &lt;code&gt;findGrid(...)&lt;/code&gt;, &lt;code&gt;findCheckbox()&lt;/code&gt;, etc.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Similarly to Playwright, in case the locator is not specific, e.g. matches multiple components, it will throw a runtime exception in case a user interaction is simulated. You can specify the target component with additional rules or e.g. with the atIndex&lt;code&gt;(int)&lt;/code&gt; method.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Components with parameterized testers (Grid, ComboBox, RadioButtonGroup) take a class witness once:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;Person first = ui.findGrid(Person.class).getRow(0);&lt;/span&gt;&lt;br&gt;&lt;span&gt;ui.findComboBox(User.class).select(user);&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span style="color: #666666;"&gt;Filter chain reads like a sentence&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The filter methods on a locator compose into a focused find:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;ui.findButton().withText("Save").click();&lt;/span&gt;&lt;br&gt;&lt;span&gt;ui.findButton().withAriaLabel("Reset form").click();&lt;/span&gt;&lt;br&gt;&lt;span&gt;ui.findTextField().withLabel("Email").setValue("a@b.com");&lt;/span&gt;&lt;br&gt;&lt;span&gt;ui.findButton().withClassName("primary").withTextContaining("Submit").click();&lt;/span&gt;&lt;br&gt;```&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;&lt;code&gt;withLabel(...)&lt;/code&gt; is new in the locator API and worth a sentence: it resolves both the component's own &lt;code&gt;label&lt;/code&gt; property and a separate &lt;code&gt;&amp;lt;label for="..."&amp;gt;&lt;/code&gt; element (e.g. a &lt;code&gt;NativeLabel&lt;/code&gt; you attached explicitly). That's the pattern you'd reach for in Playwright with &lt;code&gt;getByLabel(...)&lt;/code&gt; — now it's first-class here too.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span style="color: #666666;"&gt;The escape hatch is one call away&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Sometimes the tester surface doesn't cover what an assertion needs. &lt;code&gt;component()&lt;/code&gt; gives you the underlying component instance:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;TextField email = ui.findTextField().withLabel("Email").component();&lt;/span&gt;&lt;br&gt;&lt;span&gt;Assertions.assertEquals("a@b.com", email.getValue());&lt;/span&gt;&lt;br&gt;&lt;span&gt;Assertions.assertTrue(email.isInvalid());&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;This is the steam-valve for things the tester abstraction doesn't expose. Most tests won't need it for interaction, but it's there for the assertion side. The locator stays the primary surface; the component is the fallback.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span style="color: #666666;"&gt;Why the wrap step matters&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;It would be tempting to say "well, you can interact directly with the Component API — why bother with testers at all?" The reason testers exist (and now locators) is that direct Component-level interaction &lt;span style="font-weight: bold;"&gt;doesn't simulate what the user does&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Calling &lt;code&gt;textField.setValue("x")&lt;/code&gt; server-side works. But it bypasses the "value came from the client" event flow, doesn't fire the right change listeners with &lt;code&gt;isFromClient() == true&lt;/code&gt;, doesn't enforce that the field is enabled or visible, and won't catch a bug where the form silently ignores readonly fields. The tester (and now the locator) wraps these calls with the "as a user would" semantics. That's the win you used to opt into with &lt;code&gt;test(...)&lt;/code&gt;; in the new API you can't not get it.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;So the rule of thumb: &lt;span style="font-weight: bold;"&gt;interact through the locator, assert through either.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;A small end-to-end&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Putting all of the above together — a realistic form-submission test:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;@Test&lt;/span&gt;&lt;br&gt;&lt;span&gt;void personForm_submit_recordsExpectedEntry() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; try (var app = BrowserlessApplicationContext.create(routes)) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var ui = app.newUser().newWindow();&lt;/span&gt;&lt;br&gt;&lt;span&gt; var view = ui.navigate(PersonFormView.class);&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.findTextField().withLabel("Full name").setValue("Ada Lovelace");&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.findTextField().withLabel("Email").setValue("ada@example.com");&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.findButton().withCaption("Submit").click();&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; // Assert against the component directly when that's the natural shape.&lt;/span&gt;&lt;br&gt;&lt;span&gt; Assertions.assertEquals(1, view.savedEntries().size());&lt;/span&gt;&lt;br&gt;&lt;span&gt; Assertions.assertEquals("Ada Lovelace",&lt;/span&gt;&lt;br&gt;&lt;span&gt; view.savedEntries().get(0).name());&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Compare against the older shape: no &lt;code&gt;extends BrowserlessTest&lt;/code&gt;, no &lt;code&gt;test(field).setValue(...)&lt;/code&gt; two-step, no per-test base-class configuration to manage cleanup.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Write your own locators - scale up with page objects&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;If your project has more than a handful of tests, consider applying the &lt;span style="font-weight: bold;"&gt;page object pattern&lt;/span&gt; to your browserless tests just like you would for TestBench or Playwright. The locator API was designed with this in mind: any custom &lt;code&gt;Locator&lt;/code&gt; subclass IS a page object, and the existing tests in this repo already include one as a working template.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;code&gt;PersonFormLocator&lt;/code&gt; in the test sources is a tiny but representative example — it wraps a &lt;code&gt;PersonForm&lt;/code&gt; composite and exposes intent-named methods instead of leaking field IDs into the test:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;public class PersonFormLocator extends Locator&amp;lt;PersonForm, PersonFormLocator&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; public PersonFormLocator() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; super(PersonForm.class);&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; public PersonFormLocator fillIn(String name, String email) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; new TextFieldLocator().withLabel("Name").inside(this).setValue(name);&lt;/span&gt;&lt;br&gt;&lt;span&gt; new TextFieldLocator().withLabel("Email").inside(this).setValue(email);&lt;/span&gt;&lt;br&gt;&lt;span&gt; return this;&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; public void submit() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; new ButtonLocator().withText("Submit").inside(this).click();&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;&lt;/span&gt;&lt;span&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Two patterns to take away:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Subclass &lt;code&gt;Locator&amp;lt;MyComposite&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;MyCompositeLocator&amp;gt;&lt;/code&gt; &lt;strong&gt;with the recursive self-type&lt;/strong&gt;. Filter steps inherited from &lt;code&gt;Locator&lt;/code&gt; (&lt;code&gt;withLabel&lt;/code&gt;, &lt;code&gt;withText&lt;/code&gt;, &lt;code&gt;inside&lt;/code&gt;, etc.) stay chainable and keep returning your concrete locator type.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Compose built-in locators inside the page object and scope them with &lt;code&gt;inside(this)&lt;/code&gt;.&lt;/strong&gt; Sub-queries only see descendants of the resolved composite, so two &lt;code&gt;PersonForms&lt;/code&gt; on the same screen don't interfere.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;Tests then read at a higher level:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;ui.find(PersonFormLocator::new).fillIn("Ada", "ada@example.com");&lt;/span&gt;&lt;br&gt;&lt;span&gt;ui.find(PersonFormLocator::new).submit();&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;Assertions.assertEquals("Submitted: Ada &amp;lt;ada@example.com&amp;gt;",&lt;/span&gt;&lt;br&gt;&lt;span&gt; ui.findSpan().withId("echo").getText());&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Notice the test no longer mentions field IDs or the inner structure of the form — those are encapsulated in &lt;code&gt;PersonFormLocator&lt;/code&gt;. When the form gets a new field, you change the page object once; every test keeps working.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The same pattern scales to whole views (&lt;code&gt;LoginPageLocator&lt;/code&gt;, &lt;code&gt;CheckoutFlowLocator&lt;/code&gt;), to recurring widgets across views (a custom date range picker, a hotel search bar), and to vendor components that ship as black-box &lt;code&gt;Composite&lt;/code&gt;s. For the simple "click this button" cases the built-in &lt;code&gt;findButton()&lt;/code&gt; / &lt;code&gt;findTextField()&lt;/code&gt; are enough — but once you find yourself repeating the same field navigation in three tests, lifting it into a custom locator is usually the right move.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;A good rule of thumb: &lt;span style="font-weight: bold;"&gt;whenever you ship a custom component, ship a tiny locator next to it.&lt;/span&gt; Even if it starts as just a few intent-named methods over the built-in locators (&lt;code&gt;fillIn&lt;/code&gt;, &lt;code&gt;submit&lt;/code&gt;, &lt;code&gt;selectRow&lt;/code&gt;), having it in place means every test that uses your component automatically benefits from the abstraction — and the locator grows naturally alongside the component as it gains features.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Why we think this is worth defaulting to&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Composability over inheritance.&lt;/strong&gt; Tests pick what they extend; the library only owns the &lt;code&gt;try-with-resources&lt;/code&gt;.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;One verb to interact&lt;/strong&gt;. No "now wrap it" step between query and action. The locator is the action surface.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Multi-user is free&lt;/strong&gt;. Same shape as single-user, just two &lt;code&gt;ui&lt;/code&gt; variables. No thread-local plumbing to think about.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Familiar to Playwright users&lt;/strong&gt;. Same mental model, mapped to Vaadin.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Better defaults&lt;/strong&gt;. "Set this value" via a locator goes through the same path a real user click would. Direct component access stays available as an escape hatch — and is the right tool for assertions — but isn't the path of least resistance for interaction anymore.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Status and where to follow along&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The existing API continues to work unchanged, so adopting the new shape is per-test, not project-wide. Naming is still settling in a few places, and the published docs trail the source a little — work-in- progress docs are visible here:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/docs/next/flow/testing/browserless/locators"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Locator API&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/docs/next/flow/testing/browserless/multi-user"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Multi-user testing&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;Some examples in those docs still use older names; the spirit is right, the surface is what's described above.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Feedback — especially on naming and on missing locator types for components you actually test against — is the most useful signal we can get right now. If you try the new API on a real test class, please open an issue or PR with what felt rough.&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fa-more-playwright-like-api-for-vaadin-browserless-tests&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>product release</category>
      <category>category/product</category>
      <category>Vaadin 25</category>
      <pubDate>Thu, 28 May 2026 10:25:01 GMT</pubDate>
      <author>matti@vaadin.com (Matti Tahvonen)</author>
      <guid>https://blog.vaadin.com/a-more-playwright-like-api-for-vaadin-browserless-tests</guid>
      <dc:date>2026-05-28T10:25:01Z</dc:date>
    </item>
    <item>
      <title>How To Ensure Serializable Sessions in Vaadin 25 Applications</title>
      <link>https://blog.vaadin.com/how-to-ensure-serializable-sessions-in-vaadin-25-applications</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/how-to-ensure-serializable-sessions-in-vaadin-25-applications" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/How%20To%20Ensure%20Serializable%20Sessions%20in%20Vaadin%2025%20Applications.png" alt="How To Ensure Serializable Sessions in Vaadin 25 Applications" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;In many real-world Vaadin 25 applications, strict session serialization isn’t always required. However, it becomes important in clustered deployments, failover setups, session passivation scenarios, and environments with frequent restarts.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;span&gt;In many real-world Vaadin 25 applications, strict session serialization isn’t always required. However, it becomes important in clustered deployments, failover setups, session passivation scenarios, and environments with frequent restarts.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The good news is that this is manageable when you apply a few disciplined patterns.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This tutorial covers the key patterns for Vaadin 25 (Flow), including Kubernetes Kit tooling, and explains &lt;code&gt;UnserializableComponentWrapper&lt;/code&gt; for components that cannot be made serializable directly.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Why This Matters&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Vaadin stores server-side UI state in &lt;code&gt;VaadinSession&lt;/code&gt;, which is backed by &lt;code&gt;HttpSession&lt;/code&gt;. If that session is serialized, every object reachable from the UI graph must also be serializable, or serialization/deserialization will fail.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Common symptoms when this isn’t handled well:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Random session loss&lt;/strong&gt; after restart, rollout, or node switch. Users get logged out or lose their UI state with no obvious error&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Serialization exceptions &lt;/strong&gt;caused by hidden object references to non-serializable objects buried deep in the object graph&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Deserialization failures &lt;/strong&gt;caused by captured lambdas, anonymous listener classes, or runtime state that doesn't survive the round trip&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;The underlying reason these bugs are hard to detect is that Java's &lt;code&gt;Serializable&lt;/code&gt; interface is a marker, and it makes no guarantees about the objects you reference. A class that implements &lt;code&gt;Serializable&lt;/code&gt; can still blow up at runtime if any of its non-transient fields are not serializable. You don't find out until serialization is actually attempted.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;The Core Rule&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Think in object graphs, not individual classes. &lt;/span&gt;&lt;span&gt;A class can implement &lt;code&gt;Serializable&lt;/code&gt;, but if it references a non-serializable object that is not marked &lt;code&gt;transient&lt;/code&gt;, session replication still fails.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The patterns below are all applications of this one principle.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 1: Make UI Graph Objects Serializable&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Any object that becomes part of the live UI graph; views, presenters, component models, event handlers attached to UI components, needs to implement &lt;code&gt;Serializable&lt;/code&gt;. This is the starting baseline. &lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;A minimal example:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;public class PurchasesApprovalsPresenter implements Serializable {&lt;/span&gt;&lt;br&gt;&lt;span&gt; // durable state + behavior&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;This is necessary but not sufficient. Marking the class &lt;code&gt;Serializable&lt;/code&gt; only tells the JVM to attempt serialization; it doesn't guarantee that all reachable state is actually serializable. That's what the remaining patterns address.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 2: Mark Runtime-Only Dependencies as transient&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Service handles, executors, futures, and other runtime-only resources should not be serialized.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Minimal example:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;private transient PurchaseService purchaseService;&lt;/span&gt;&lt;br&gt;&lt;span&gt;private transient CompletableFuture&amp;lt;Void&amp;gt; updateTask;&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Why this helps:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Many service objects (especially Spring-proxied beans) are not serializable by design.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Even if they happen to be serializable, serializing them would capture stale infrastructure state (database connections, thread pools) that's meaningless after deserialization.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;It keeps your serialized session lean. Accidentally serializing a service that holds a large cache can silently balloon session sizes and hurt replication performance.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 3: Rehydrate transient Fields Lazily&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;After deserialization, transient fields are &lt;code&gt;null&lt;/code&gt;. If your code doesn't account for this, you'll get &lt;code&gt;NullPointerExceptions&lt;/code&gt; in methods that previously worked fine, because they were never called on a freshly-deserialized instance before. The safest pattern is lazy rehydration through accessor methods:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;private PurchaseService getPurchaseService() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; if (purchaseService == null) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; purchaseService = PurchaseService.get();&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; return purchaseService;&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;In Spring + Kubernetes Kit setups, transient Spring-bean fields can be reinjected automatically via &lt;code&gt;@SpringComponent&lt;/code&gt; and the Kubernetes Kit transient injection mechanism, removing the need for manual lazy initialization in many cases. See the&lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/tools/kubernetes"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt; Kubernetes Kit documentation&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; for how to configure this.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 4: Use Serializable Callback Types&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Java's standard &lt;code&gt;java.util.function.*&lt;/code&gt; interfaces, &lt;code&gt;Consumer&lt;/code&gt;, &lt;code&gt;Supplier&lt;/code&gt;, &lt;code&gt;Runnable&lt;/code&gt;, &lt;code&gt;Function&lt;/code&gt;, do not implement &lt;code&gt;Serializable&lt;/code&gt;. If you store instances of these in your component state, you'll get a serialization failure.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Vaadin provides serializable equivalents for the most common functional types:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;public interface DecisionConfirmedListener extends Serializable {&lt;/span&gt;&lt;br&gt;&lt;span&gt; void onConfirmed(String comment);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;private SerializableConsumer&amp;lt;Product&amp;gt; onSuccess;&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Avoid plain &lt;code&gt;java.util.function.*&lt;/code&gt; types for callback state stored in the UI graph.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 5: Clean Up on Detach&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Detach is where you break references to active runtime work.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Minimal example:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;@Override&lt;/span&gt;&lt;br&gt;&lt;span&gt;protected void onDetach(DetachEvent detachEvent) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; super.onDetach(detachEvent);&lt;/span&gt;&lt;br&gt;&lt;span&gt; if (updateTask != null) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; updateTask.cancel(true);&lt;/span&gt;&lt;br&gt;&lt;span&gt; updateTask = null;&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;/span&gt;&lt;span&gt;What belongs in &lt;code&gt;onDetach&lt;/code&gt;:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Cancelling in-flight async tasks (&lt;code&gt;CompletableFuture&lt;/code&gt;, &lt;code&gt;ScheduledFuture&lt;/code&gt;, etc.)&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Unregistering backend event listeners&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Dropping strong references to any external runtime resources that the component was holding&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 6: Prefer Explicit Listener Lifecycle&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Event listeners deserve their own pattern because they're easy to get wrong in ways that don't immediately surface as bugs.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;An unregistered listener keeps the listener object (and everything it transitively references) alive for as long as the event source lives, which may be the entire application lifetime.&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;private transient Registration busRegistration;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;@Override&lt;/span&gt;&lt;br&gt;&lt;span&gt;protected void onAttach(AttachEvent attachEvent) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; super.onAttach(attachEvent);&lt;/span&gt;&lt;br&gt;&lt;span&gt; busRegistration = eventBus.register(this::handleEvent);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;@Override&lt;/span&gt;&lt;br&gt;&lt;span&gt;protected void onDetach(DetachEvent detachEvent) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; if (busRegistration != null) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; busRegistration.remove();&lt;/span&gt;&lt;br&gt;&lt;span&gt; busRegistration = null;&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; super.onDetach(detachEvent);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Use explicit register/unregister, weak-listener approaches, or both.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 7: Enable Kubernetes Kit Session Replication Debug Tool&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Vaadin 25 and Kubernetes Kit adds a development-time debug tool that serializes and deserializes the session after every request, then reports any failures to the log. This turns what would otherwise be a silent production failure into a loud, immediate development-time error.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;span style="font-weight: bold;"&gt;Important:&lt;/span&gt; This tool is for development and diagnostics only. Do not enable it in production, it adds significant overhead to every request.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;In Spring Boot projects with Kubernetes Kit, the tool is auto-configured. You can also register it explicitly&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;7.1 Enable in application properties&lt;/span&gt;&lt;/h3&gt; 
&lt;pre&gt;&lt;span&gt;```properties&lt;br&gt;vaadin.devmode.sessionSerialization.enabled=true&lt;/span&gt;&lt;br&gt;&lt;span&gt;vaadin.serialization.transients.include-packages=com.example.application&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Notes:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;code&gt;vaadin.devmode.sessionSerialization.enabled&lt;/code&gt; turns on the debug tool in dev mode.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;code&gt;vaadin.serialization.transients.include-packages&lt;/code&gt; narrows transient-field inspection to your app packages.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;7.2 Enable extended Java serialization diagnostics&lt;/span&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The JVM's built-in extended debug output for serialization gives you much more detail about exactly which class in the graph caused a failure. Enable it via Maven plugin JVM arguments:&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```xml&lt;br&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;configuration&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;jvmArguments&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; --add-opens java.base/java.io=ALL-UNNAMED&lt;/span&gt;&lt;br&gt;&lt;span&gt; -Dsun.io.serialization.extendedDebugInfo=true&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;/jvmArguments&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;/configuration&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;&lt;br&gt;```&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Optional timeout tuning:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```properties&lt;br&gt;vaadin.serialization.timeout=60000&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;7.3 Optional explicit registration (as in this demo)&lt;/span&gt;&lt;/h3&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;@Bean&lt;/span&gt;&lt;br&gt;&lt;span&gt;VaadinServiceInitListener serializationDebugInitListener() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; return new SerializationDebugRequestHandler.InitListener();&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;@Bean&lt;/span&gt;&lt;br&gt;&lt;span&gt;@Order(Integer.MIN_VALUE)&lt;/span&gt;&lt;br&gt;&lt;span&gt;FilterRegistrationBean&amp;lt;SerializationDebugRequestHandler.Filter&amp;gt; sessionSerializationDebugToolFilter() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; FilterRegistrationBean&amp;lt;SerializationDebugRequestHandler.Filter&amp;gt; registration =&lt;/span&gt;&lt;br&gt;&lt;span&gt; new FilterRegistrationBean&amp;lt;&amp;gt;(new SerializationDebugRequestHandler.Filter());&lt;/span&gt;&lt;br&gt;&lt;span&gt; registration.setAsyncSupported(true);&lt;/span&gt;&lt;br&gt;&lt;span&gt; return registration;&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;7.4 Reading results&lt;/span&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Typical outcomes in logs are &lt;code&gt;SUCCESS&lt;/code&gt;, &lt;code&gt;SERIALIZATION_FAILED&lt;/code&gt;, &lt;code&gt;DESERIALIZATION_FAILED&lt;/code&gt;, and &lt;code&gt;NOT_SERIALIZABLE_CLASSES&lt;/code&gt;.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;For &lt;code&gt;SerializedLambda&lt;/code&gt; failures, read the &lt;code&gt;BEST CANDIDATES&lt;/code&gt; section from bottom to top to find the captured class and method.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 8: Debug Session Sizes During Serialization&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;In addition to pass/fail outcomes, Kubernetes Kit can help you measure serialized session size. This is useful when sessions become large and replication slows down.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;8.1 Enable debug logging&lt;/span&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;This demo app enables Kubernetes Kit debug logging in &lt;code&gt;application.properties&lt;/code&gt;:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```properties&lt;br&gt;logging.level.com.vaadin.kubernetes=debug&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;8.2 Read size from serializer log lines&lt;/span&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Look for serializer lines that end with elapsed time and payload size in bytes.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Example:&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;Serializer : Serialization of attributes&lt;br&gt;[com.vaadin.flow.server.VaadinSession.springServlet,&lt;br&gt;springServlet.lock,&lt;br&gt;org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN,&lt;br&gt;clusterKey, SPRING_SECURITY_CONTEXT]&lt;br&gt;&lt;br&gt;for session DEBUG-SERIALIZE-F159D9419778EFF9F4DD004B415083A3&lt;br&gt;&lt;br&gt;with distributed key 4f1af8e8-b432-4cdc-ab98-376c8b188b99_SOURCE:F159D9419778EFF9F4DD004B415083A3&lt;br&gt;&lt;br&gt;completed in 31ms (124599 bytes)&lt;span&gt;&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Key signal:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;The value in parentheses (for example &lt;code&gt;124599 bytes&lt;/code&gt;) is the serialized session size for that cycle.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;8.3 Practical usage&lt;/span&gt;&lt;/h3&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Compare size before and after opening heavy views.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Track growth over navigation flows; sudden jumps often reveal large objects captured into session state.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Use this together with serialization debug outcomes to prioritize fixes.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Disable debug-level Kubernetes logging after investigation to reduce log noise.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;Pattern 9: Wrap Naturally Non-Serializable Components&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Some third-party UI components are not fully serializable by nature. Vaadin Kubernetes Kit provides &lt;code&gt;UnserializableComponentWrapper&amp;lt;S, T&amp;gt;&lt;/code&gt; for these cases.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Use this only when making the component itself serializable is not feasible.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;span style="color: #434343;"&gt;Spreadsheet example (Apache POI workbook state)&lt;/span&gt;&lt;/h3&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;record SpreadsheetState(byte[] workbookBytes) implements Serializable {&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;span&gt;Spreadsheet spreadsheet = new Spreadsheet();&lt;/span&gt;&lt;br&gt;&lt;span&gt;var wrapper = new UnserializableComponentWrapper&amp;lt;SpreadsheetState, Spreadsheet&amp;gt;(&lt;/span&gt;&lt;br&gt;&lt;span&gt; spreadsheet,&lt;/span&gt;&lt;br&gt;&lt;span&gt; sheet -&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; try (var out = new ByteArrayOutputStream()) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; sheet.getWorkbook().write(out);&lt;/span&gt;&lt;br&gt;&lt;span&gt; return new SpreadsheetState(out.toByteArray());&lt;/span&gt;&lt;br&gt;&lt;span&gt; } catch (IOException e) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; throw new UncheckedIOException(e);&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; },&lt;/span&gt;&lt;br&gt;&lt;span&gt; state -&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; try (var in = new ByteArrayInputStream(state.workbookBytes())) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var workbook = new XSSFWorkbook(in);&lt;/span&gt;&lt;br&gt;&lt;span&gt; var restored = new Spreadsheet();&lt;/span&gt;&lt;br&gt;&lt;span&gt; restored.setWorkbook(workbook);&lt;/span&gt;&lt;br&gt;&lt;span&gt; return restored;&lt;/span&gt;&lt;br&gt;&lt;span&gt; } catch (IOException e) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; throw new UncheckedIOException(e);&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; });&lt;/span&gt;&lt;br&gt;&lt;span&gt;add(wrapper);&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Important wrapper behavior:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Serializer must extract only durable, serializable state.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Deserializer rebuilds the component from scratch after graph restoration.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;You must restore needed behavior (for example, listeners or runtime callbacks).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Wrapped component attach/detach listeners are still triggered.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;Use Linters as an Early Warning System&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Static analysis (for example SonarQube) is useful to detect classes implementing &lt;code&gt;Serializable&lt;/code&gt; but holding non-serializable non-transient fields.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;What this gives you:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Fast feedback in development and code review.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Early detection before runtime replication tests.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;What it does not give you:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;It cannot determine developer intent (should this field be serialized, or made &lt;code&gt;transient&lt;/code&gt; and rehydrated?).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;It won't catch failures that only manifest through object graph wiring — a field might look fine in isolation but cause failures when wired together with collaborators.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;The Most Important Verification: Serialize the View Graph in Tests&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Do not rely only on marker-interface checks or static analysis. The only reliable verification is serializing and deserializing a real, fully-wired view graph in an actual test.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This demo uses &lt;code&gt;SerializationDebugUtil&lt;/code&gt; in UI unit tests:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;@Test&lt;/span&gt;&lt;br&gt;&lt;span&gt;void viewGraphIsSerializable() throws IOException {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var view = navigate(AddressesView.class);&lt;/span&gt;&lt;br&gt;&lt;span&gt; SerializationDebugUtil.assertSerializable(view);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;Why this is more powerful than static analysis:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;It validates wiring of view + presenter + collaborators in the same configuration used at runtime..&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;It catches real graph failures, not theoretical ones.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;It helps pinpoint offending references quickly.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;What Level of Coverage Is Enough?&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;A practical threshold for most applications:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Every major view has at least one serialization assertion covering its initial render.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Views with significant interaction states (forms, dialogs, multi-step flows) also have assertions covering those intermediate states (not just the initial render).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;There are no alternate wiring paths (different constructors, feature flags, user-type variations) bypassing tested code.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;A Quick Checklist You Can Use Today&lt;/span&gt;&lt;/h2&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;UI graph collaborators implement &lt;code&gt;Serializable&lt;/code&gt; where appropriate.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Runtime-only fields are &lt;code&gt;transient&lt;/code&gt;.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Transient fields are rehydrated safely.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Callback and listener state uses serializable interfaces.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;code&gt;onDetach&lt;/code&gt; cleanup removes runtime work and listeners.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Kubernetes Kit debug tool is enabled in development.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Kubernetes Kit debug logging is used to track serialized session size when investigating session growth.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;code&gt;vaadin.serialization.transients.include-packages&lt;/code&gt; is scoped to app packages.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Non-serializable third-party components use UnserializableComponentWrapper with explicit serializer/deserializer.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;View-level tests assert serializability after key interactions.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;span&gt;Final Takeaway&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Serializable sessions in Vaadin 25 are not a single trick you apply once. They're the result of a consistent set of habits applied across the entire development lifecycle:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Object-graph discipline&lt;/strong&gt; at design time: think about what belongs in a durable session state and what belongs in transient runtime resources.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Explicit lifecycle management&lt;/strong&gt; for listeners and async work, with clean attach/detach pairs.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Continuous verification&lt;/strong&gt; through the &lt;a href="https://vaadin.com/docs/latest/tools/kubernetes/session-replication-debug-tool"&gt;Kubernetes Kit debug tool&lt;/a&gt; during development and through serialization assertions in tests.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;If you follow these patterns consistently, most production-grade session replication issues are caught early, while still keeping development ergonomics reasonable.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Happy coding!&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fhow-to-ensure-serializable-sessions-in-vaadin-25-applications&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>session replication</category>
      <category>kubernetes</category>
      <category>category/development</category>
      <category>kubernetes kit</category>
      <pubDate>Thu, 21 May 2026 14:08:49 GMT</pubDate>
      <author>tatu@vaadin.com (Tatu Lund)</author>
      <guid>https://blog.vaadin.com/how-to-ensure-serializable-sessions-in-vaadin-25-applications</guid>
      <dc:date>2026-05-21T14:08:49Z</dc:date>
    </item>
    <item>
      <title>How Eduten Built a High-Usage Learning Platform with Vaadin</title>
      <link>https://blog.vaadin.com/how-eduten-built-a-high-usage-learning-platform-with-vaadin</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/how-eduten-built-a-high-usage-learning-platform-with-vaadin" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/eduten-May-12-2026-09-41-30-3105-AM.png" alt="How Eduten Built a High-Usage Learning Platform with Vaadin" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;Built on nearly two decades of research at the University of Turku, &lt;/span&gt;&lt;a href="https://eduten.com/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Eduten is a browser-based learning platform used in classrooms&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; as part of weekly classroom teaching. Today, it’s used in over 50 countries and adopted in more than 70% of schools in Finland, with recognition from organizations like UNESCO and UNICEF.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;span&gt;Built on nearly two decades of research at the University of Turku, &lt;/span&gt;&lt;a href="https://eduten.com/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Eduten is a browser-based learning platform used in classrooms&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; as part of weekly classroom teaching. Today, it’s used in over 50 countries and adopted in more than 70% of schools in Finland, with recognition from organizations like UNESCO and UNICEF.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Over 3 million users and up to 1 million monthly active users&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; puts the Eduten platform firmly at national rollout scale, with high usage peaks, long lifecycles, and little room for friction in everyday workflows.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: center;"&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 486px; height: 233px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-07-2026-12-24-43-6108-PM.png?width=2048&amp;amp;height=986&amp;amp;name=undefined-May-07-2026-12-24-43-6108-PM.png" width="2048" height="986"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;The Problem&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Eduten’s challenge was turning research into something that works reliably in classrooms and homes, consistently, at scale.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Unlike many EdTech tools, Eduten is designed as &lt;span style="font-weight: normal;"&gt;a &lt;/span&gt;&lt;/span&gt;&lt;span style="font-weight: normal;"&gt;curriculum-first platform&lt;/span&gt;&lt;span&gt;&lt;span style="font-weight: normal;"&gt;. Teachers r&lt;/span&gt;ely on it regularly to assign exercises, track progress, and guide students. &lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The system has to work in real classroom conditions:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Under time pressure&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Across different devices&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;With unreliable connectivity &lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;From an engineering perspective, this created familiar enterprise challenges. Usage isn’t random and it follows predictable peaks during class hours. The platform supports multiple roles at once, with teachers coordinating learning, students actively interacting, and parents checking progress. And because it’s used frequently, even small issues in UX or stability become obvious immediately.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 624px; height: 303px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-May-07-2026-12-24-44-2198-PM.png?width=2048&amp;amp;height=992&amp;amp;name=undefined-May-07-2026-12-24-44-2198-PM.png" width="2048" height="992"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;The Solution&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Eduten built its platform using Vaadin’s full-stack approach with Hilla, keeping development in Java while still delivering a modern browser-based experience. Having one stack reduced the need to manage separate frontend and backend layers, so the team could focus more on the product itself.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="font-weight: normal;"&gt;Designing for real classroom use was just as important. The team focused on creating a digital math learning platform that works for different age groups, across countries, and on a wide range of devices.&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;With support from Vaadin’s design team, &lt;/span&gt;&lt;a href="https://vaadin.com/showcase/eduten"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Eduten developed a gamified learning experience&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; tailored to each student’s needs balancing engagement with focus.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This resulted in a platform that is:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Mobile-first&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; – optimized for smartphones and tablets used in classrooms&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Playful &lt;/span&gt;&lt;/strong&gt;&lt;span&gt;– interactive design that keeps students motivated&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Guided &lt;/span&gt;&lt;/strong&gt;&lt;span&gt;– step-by-step workflows that support independent learning&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;This balance between playfulness and task-oriented focus ensures the platform remains engaging without compromising learning outcomes.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/eduten-3-1.webp?width=2432&amp;amp;height=1165&amp;amp;name=eduten-3-1.webp" width="2432" height="1165" alt="eduten-3-1" style="height: auto; max-width: 100%; width: 2432px;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;blockquote&gt; 
 &lt;p&gt;&lt;span&gt;&lt;span style="color: #222222; background-color: #ffffff;"&gt;We had very specific requirements: we needed offline &lt;/span&gt;&lt;span style="color: #222222;"&gt;support, but we also wanted&lt;/span&gt;&lt;span style="color: #222222; background-color: #ffffff;"&gt; a robust Java backend. With Hilla, we've &lt;/span&gt;&lt;span style="color: #222222;"&gt;been able to deliver&lt;/span&gt;&lt;span style="color: #222222; background-color: #ffffff;"&gt; a much better user experience while also reducing our cloud hosting costs compared to our previous application.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;/blockquote&gt; 
&lt;p style="font-weight: normal; text-align: right;"&gt;– &lt;span style="font-weight: bold;"&gt;Einari Kurvinen&lt;/span&gt;, Head of Pedagogy, Eduten&lt;/p&gt; 
&lt;p&gt;&lt;span style="font-weight: bold;"&gt;Note:&lt;/span&gt; &lt;a href="https://vaadin.com/blog/merging-hilla-into-flow" style="font-weight: normal;"&gt;Hilla was merged into Vaadin Flow starting with Vaadin 25.&lt;/a&gt;&lt;span style="font-weight: normal;"&gt;.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;The Result&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The Eduten platform is now embedded in everyday classroom use, &lt;/span&gt;&lt;strong&gt;&lt;span&gt;supporting hundreds of thousands of active users&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; with predictable, recurring demand tied to school schedules. In Mongolia alone, students completed &lt;/span&gt;&lt;strong&gt;&lt;span&gt;over 2 billion math exercises in 2024&lt;/span&gt;&lt;/strong&gt;&lt;span&gt;, illustrating not just adoption, but sustained, high-frequency use.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;More broadly, Eduten is:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Used in over 70% of schools in Finland&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Deployed in more than 50 countries&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Used by millions over time&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;From a technical perspective, the outcome is not just that the system works, but that it remains maintainable. The team can continue improving the product without introducing instability into daily classroom use.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Looking ahead&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;Eduten’s platform was built to last, so with a stable foundation in place, the team can focus on improving the product and gradually adding new features over time. &lt;a href="https://eduten.com/"&gt;Learn more about Eduten.&lt;/a&gt;&lt;/p&gt; 
&lt;h2 style="font-weight: normal;"&gt;&lt;span style="background-color: transparent; color: #374151; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;Have questions about building at scale with Vaadin?&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="font-weight: normal;"&gt;&lt;span style="background-color: transparent; color: #374151; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;Our team is happy to help you evaluate the right approach for your project.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fhow-eduten-built-a-high-usage-learning-platform-with-vaadin&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>scalability</category>
      <category>success stories</category>
      <category>category/customer stories</category>
      <pubDate>Tue, 12 May 2026 09:51:24 GMT</pubDate>
      <author>lilli@vaadin.com (Lilli Salo)</author>
      <guid>https://blog.vaadin.com/how-eduten-built-a-high-usage-learning-platform-with-vaadin</guid>
      <dc:date>2026-05-12T09:51:24Z</dc:date>
    </item>
    <item>
      <title>Printing and Saving PDFs From Web Apps</title>
      <link>https://blog.vaadin.com/printing-and-saving-pdfs-from-web-apps</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/printing-and-saving-pdfs-from-web-apps" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/printingandsavingpdfsfromwebapps.png" alt="Printing and Saving PDFs from Java Web Apps" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Printing from a browser looks trivial — until you try it. A modern web UI is built around a scrolling viewport, virtualized lists, fixed drawers and other overlays that the user never thinks about, but that a printer happily reproduces on A4. This post walks through the four options I reach for when a Vaadin app needs to produce paper (or PDFs), with runnable snippets taken from the companion &lt;/span&gt;&lt;a href="https://w.virit.in/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;wwcd demo project&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; and notes on when each approach earns its keep.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p style="text-align: left;"&gt;&lt;span&gt;Printing from a browser looks trivial — until you try it. A modern web UI is built around a scrolling viewport, virtualized lists, fixed drawers and other overlays that the user never thinks about, but that a printer happily reproduces on A4. This post walks through the four options I reach for when a Vaadin app needs to produce paper (or PDFs), with runnable snippets taken from the companion &lt;/span&gt;&lt;a href="https://w.virit.in/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;wwcd demo project&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; and notes on when each approach earns its keep.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/1-basic-print.webp?width=3022&amp;amp;height=1894&amp;amp;name=1-basic-print.webp" width="3022" height="1894" alt="Printing a demo view from Safari. Same DOM, one @media print stylesheet apart." style="height: auto; max-width: 100%; width: 3022px; margin: 32px 0px 17px;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left; font-size: 16px;"&gt;&lt;em&gt;Printing a demo view from Safari. Same DOM, one &lt;code&gt;@media print&lt;/code&gt; stylesheet apart.&lt;/em&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span style="background-color: transparent; font-size: 1.125rem; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;The four approaches we'll cover:&lt;/span&gt;&lt;/p&gt; 
&lt;ol style="text-align: left;"&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Print the live DOM&lt;/strong&gt;, tuned with a &lt;code&gt;@media print&lt;/code&gt; stylesheet.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Render a second, print-optimized view&lt;/strong&gt; for content that the live UI can't reasonably paginate (virtualized Grids, certain charts, video).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Generate a PDF on the server&lt;/strong&gt; and hand it to the user.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Bypass the OS print dialog altogether&lt;/strong&gt;, either via Web Bluetooth in the browser or by pushing the job to a service-connected printer.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;h2 style="text-align: left;"&gt;&lt;span&gt;1. Just print and style with CSS&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Every browser has an OS-native print dialog and a well-understood &lt;code&gt;Ctrl/Cmd-P.&lt;/code&gt; You can trigger it from JS with &lt;code&gt;window.print()&lt;/code&gt;, but most users already know the shortcut, so a button is usually redundant.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The real problem is that a web page was not designed for paper. Modern browsers support &lt;code&gt;@media print &lt;/code&gt;well, and that applies to Vaadin apps just like any other HTML. If printing is a common action in your app, spend fifteen minutes on a print stylesheet — remove irrelevant chrome, adjust margins, flatten backgrounds.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;In a Vaadin app, the most common obstacle is &lt;code&gt;vaadin-app-layout&lt;/code&gt;. It's a textbook example of a layout designed for scrolling, not paper. It works fine once you know where to poke it:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```css&lt;br&gt;@media print {&lt;/span&gt;&lt;br&gt;&lt;span&gt; /* Hide anything the developer marks as screen-only. */&lt;/span&gt;&lt;br&gt;&lt;span&gt; .noprint { display: none; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; /* Hide navbar and drawer — they waste space and break page flow. */&lt;/span&gt;&lt;br&gt;&lt;span&gt; vaadin-app-layout::part(navbar),&lt;/span&gt;&lt;br&gt;&lt;span&gt; vaadin-app-layout::part(drawer) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; display: none;&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; /* app-layout precomputes padding for the drawer and doesn't recalculate&lt;/span&gt;&lt;br&gt;&lt;span&gt; when we hide it. Force padding to 0 and let content overflow. */&lt;/span&gt;&lt;br&gt;&lt;span&gt; vaadin-app-layout {&lt;/span&gt;&lt;br&gt;&lt;span&gt; padding-left: 0;&lt;/span&gt;&lt;br&gt;&lt;span&gt; overflow: visible;&lt;/span&gt;&lt;br&gt;&lt;span&gt; height: auto;&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; /* Flatten backgrounds so page 2 doesn't inherit a body gradient. */&lt;/span&gt;&lt;br&gt;&lt;span&gt; html,&lt;/span&gt;&lt;br&gt;&lt;span&gt; vaadin-app-layout vaadin-vertical-layout {&lt;/span&gt;&lt;br&gt;&lt;span&gt; background: none;&lt;/span&gt;&lt;br&gt;&lt;span&gt; border: none;&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; /* Make sure the dev-mode overlays never hit the page. */&lt;/span&gt;&lt;br&gt;&lt;span&gt; copilot-main, vaadin-dev-tools { display: none; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;If you take the CSS media query route, you'll probably want a helper class for components that shouldn't end up on paper. My demo uses &lt;code&gt;.noprint&lt;/code&gt; (see the first rule in the block above), applied for example to the "Print" button itself — which doesn't help anyone on paper 😁:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt; add(new Button("Print") {{&lt;/span&gt;&lt;br&gt;&lt;span&gt; // See css rule below, ignore when printing using media query&lt;/span&gt;&lt;br&gt;&lt;span&gt; addClassName("noprint");&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; getElement().executeJs("""&lt;/span&gt;&lt;br&gt;&lt;span&gt; // opens print the OS print dialog&lt;/span&gt;&lt;br&gt;&lt;span&gt; this.onclick = () =&amp;gt; window.print();&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Media queries can even target specific paper sizes, but resist the urge. A single layout that survives A4, Letter and the occasional label printer beats a fork per format. Rules can quickly become more difficult than your whole UI code.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="text-align: left;"&gt;&lt;span&gt;1.b Print and use JS hook for preparations&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;With the Aura theme there's one extra quirk: the left padding is set via a CSS custom property with an animated transition, and Chromium captures the pre-transition value before the &lt;code&gt;@media print&lt;/code&gt; rules kick in. Overriding it purely from CSS turned out to be too much for my limited 3 decades of web development experience, but hooking the &lt;code&gt;beforeprint&lt;/code&gt; / &lt;code&gt;afterprint&lt;/code&gt; events and nudging the padding from JS works around it:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;window.addEventListener("beforeprint", () =&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; document.querySelector("vaadin-app-layout")&lt;/span&gt;&lt;br&gt;&lt;span&gt; .shadowRoot.querySelector("[content]").style.paddingLeft = "0";&lt;/span&gt;&lt;br&gt;&lt;span&gt;});&lt;/span&gt;&lt;br&gt;&lt;span&gt;window.addEventListener("afterprint", () =&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; document.querySelector("vaadin-app-layout")&lt;/span&gt;&lt;br&gt;&lt;span&gt; .shadowRoot.querySelector("[content]").style.paddingLeft = "";&lt;/span&gt;&lt;br&gt;&lt;span&gt;});&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The above is not needed with the good old Lumo theme. File under "things you discover only when someone prints the page for real." Events can be handy especially if you want to slightly modify also the DOM before printing it. For Java developers, tricks below suit better in most cases though.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="text-align: left;"&gt;&lt;span&gt;2. Construct a print-optimized page&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Some components are architecturally incompatible with printing. The canonical example in Vaadin is the virtualized &lt;code&gt;Grid&lt;/code&gt;: it only keeps the visible rows in the DOM and loads the rest on scroll. There is no "dear browser, please materialize all 50 000 rows before you paginate" hook. There are several other similar components out there.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;When CSS can't save you, but you still need to check "printable" in your requirement docs, render a second view tailored for paper. In the demo app I keep it in the same class and branch on a constructor flag. Best approach is probably application specific.&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;@Route // dedicated print route, no MainLayout&lt;/span&gt;&lt;br&gt;&lt;span&gt;public class PrintOptimizedCard extends Card {&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; public PrintOptimizedCard() { this(true); } // route entry — print version&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; public PrintOptimizedCard(boolean optimizeForPrinting) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; setTitle("Printing a virtualized Grid");&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; if (!optimizeForPrinting) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; // Screen version: fast, virtualized Vaadin Grid&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new Grid&amp;lt;String&amp;gt;() {{&lt;/span&gt;&lt;br&gt;&lt;span&gt; setHeight("10em");&lt;/span&gt;&lt;br&gt;&lt;span&gt; setItems(rows);&lt;/span&gt;&lt;br&gt;&lt;span&gt; addColumn(s -&amp;gt; s).setHeader("Str");&lt;/span&gt;&lt;br&gt;&lt;span&gt; addColumn(String::length).setHeader("Len");&lt;/span&gt;&lt;br&gt;&lt;span&gt; }});&lt;/span&gt;&lt;br&gt;&lt;span&gt; setHeaderSuffix(new PrintRouteButton(PrintOptimizedCard.class));&lt;/span&gt;&lt;br&gt;&lt;span&gt; } else {&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; // Print version: plain &amp;lt;table&amp;gt; with every row&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new NativeTable() {{&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new NativeTableRow() {{&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new NativeTableHeaderCell("Str"));&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new NativeTableHeaderCell("Len"));&lt;/span&gt;&lt;br&gt;&lt;span&gt; }});&lt;/span&gt;&lt;br&gt;&lt;span&gt; rows.forEach(s -&amp;gt; add(new NativeTableRow() {{&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new NativeTableCell(s));&lt;/span&gt;&lt;br&gt;&lt;span&gt; add(new NativeTableCell(s.length() + ""));&lt;/span&gt;&lt;br&gt;&lt;span&gt; }}));&lt;/span&gt;&lt;br&gt;&lt;span&gt; }});&lt;/span&gt;&lt;br&gt;&lt;span&gt; getStyle().set("font-family", "Helvetica");&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The "Print" button on the screen version opens the dedicated route in a hidden iframe and calls &lt;code&gt;print()&lt;/code&gt; on it — same hack as the PDF case below, different payload:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;public static class PrintRouteButton extends VButton {&lt;/span&gt;&lt;br&gt;&lt;span&gt; public PrintRouteButton(Class&amp;lt;? extends Component&amp;gt; routeClass) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; setIcon(VaadinIcon.PRINT.create());&lt;/span&gt;&lt;br&gt;&lt;span&gt; addClickListener(e -&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; var iframe = new Element("iframe");&lt;/span&gt;&lt;br&gt;&lt;span&gt; iframe.getStyle().setDisplay(Style.Display.NONE);&lt;/span&gt;&lt;br&gt;&lt;span&gt; iframe.setAttribute("src",&lt;/span&gt;&lt;br&gt;&lt;span&gt; RouteConfiguration.forSessionScope().getUrl(routeClass));&lt;/span&gt;&lt;br&gt;&lt;span&gt; getElement().appendChild(iframe);&lt;/span&gt;&lt;br&gt;&lt;span&gt; iframe.executeJs("""&lt;/span&gt;&lt;br&gt;&lt;span&gt; this.onload = () =&amp;gt; setTimeout(() =&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; this.focus();&lt;/span&gt;&lt;br&gt;&lt;span&gt; this.contentWindow.print();&lt;/span&gt;&lt;br&gt;&lt;span&gt; }, 500);&lt;/span&gt;&lt;br&gt;&lt;span&gt; """);&lt;/span&gt;&lt;br&gt;&lt;span&gt; });&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 624px; height: 221px;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/undefined-Apr-29-2026-12-18-02-9488-PM.png?width=1871&amp;amp;height=664&amp;amp;name=undefined-Apr-29-2026-12-18-02-9488-PM.png" width="1871" height="664" style="margin: 32px 0px 24px;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;em&gt;&lt;span style="font-size: 16px;"&gt;Virtually paged Grid is handy in UI, but for printed version you probably want all the rows.&lt;/span&gt;&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="text-align: left;"&gt;&lt;span&gt;3. Generate a PDF&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;For anything that has to look identical on every device (invoices, tickets, certificates, ID cards), the industry standard is a server-generated PDF. You pay for the extra moving part with exact control over paper size, margins, fonts and colors — including CMYK / spot colors that a browser's RGB-to-printer pipeline would happily mangle.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The Java ecosystem has plenty of PDF libraries. In the demo I reach for Apache PDFBox; &lt;/span&gt;&lt;a href="https://github.com/LibrePDF/OpenPDF"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;OpenPDF&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;, &lt;/span&gt;&lt;a href="https://itextpdf.com/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;iText&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; (check the license), and &lt;/span&gt;&lt;a href="https://github.com/flyingsaucerproject/flyingsaucer"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Flying Saucer&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; (HTML → PDF) are reasonable alternatives depending on your needs.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;A minimal PDFBox document:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;private static void writePDF(String content, OutputStream out) throws IOException {&lt;/span&gt;&lt;br&gt;&lt;span&gt; try (PDDocument document = new PDDocument()) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; PDPage page = new PDPage(PDRectangle.A4);&lt;/span&gt;&lt;br&gt;&lt;span&gt; document.addPage(page);&lt;/span&gt;&lt;br&gt;&lt;span&gt; try (PDPageContentStream stream = new PDPageContentStream(document, page)) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; stream.beginText();&lt;/span&gt;&lt;br&gt;&lt;span&gt; stream.newLineAtOffset(20, 700);&lt;/span&gt;&lt;br&gt;&lt;span&gt; stream.setFont(new PDType1Font(Standard14Fonts.FontName.COURIER), 12);&lt;/span&gt;&lt;br&gt;&lt;span&gt; stream.showText(content);&lt;/span&gt;&lt;br&gt;&lt;span&gt; stream.endText();&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; document.save(out);&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;```&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;There are essentially two ways to hand that PDF to the user in a web app.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;&lt;span style="font-weight: bold;"&gt;Option A: a plain download link.&lt;/span&gt; My default. Many users want the file for their digital archive, not a physical copy; a download doubles as a print preview; and it sidesteps every browser quirk in the print dialog.&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;add(new Anchor() {{&lt;/span&gt;&lt;br&gt;&lt;span&gt; setText("Download 'Hello world' as PDF");&lt;/span&gt;&lt;br&gt;&lt;span&gt; setDownload(true);&lt;/span&gt;&lt;br&gt;&lt;span&gt; setHref(download -&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; download.setContentType("application/pdf");&lt;/span&gt;&lt;br&gt;&lt;span&gt; writePDF("Hello world from PDF!", download.getOutputStream());&lt;/span&gt;&lt;br&gt;&lt;span&gt; });&lt;/span&gt;&lt;br&gt;&lt;span&gt;}});&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;&lt;span style="font-weight: bold;"&gt;Option B: the hidden-iframe print trick, &lt;/span&gt;for a desktop-app feel where clicking "Print invoice" opens the OS print dialog directly with the PDF loaded. The Viritin add-on ships it as &lt;code&gt;PrintPdfButton&lt;/code&gt;:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;add(new PrintPdfButton(out -&amp;gt; writePDF("This is directly printed PDF", out)));&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The downside of Option B: a user who only wants to archive the document has to go through the OS dialog's "Save as PDF" path. If archiving is a common case, stick with Option A.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="text-align: left;"&gt;&lt;span&gt;4. Skip the OS print dialog entirely&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Sometimes the OS dialog is in the way — you want automation, or the printer is physically elsewhere. Classic use cases:&lt;/span&gt;&lt;/p&gt; 
&lt;ul style="text-align: left;"&gt; 
 &lt;li&gt;&lt;span&gt;A hotel self check-in kiosk dropping a receipt on a lobby printer.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;A warehouse picker scanning items and printing labels on a fixed Dymo.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;A field service tech submitting a job from their phone to a printer back at the office.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The great thing in this approach is that the print job no more needs to be triggered by a direct user interaction in the computer. They can be scheduled or automatically triggered by another hardware interaction. In a recent hobby app I'm initiang a label printing automatically when a Bluetooth scale (also accessed via WebBluetooth) gets a solid reading.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The job naturally has to reach the device some other way. Two example paths:&lt;/span&gt;&lt;/p&gt; 
&lt;h3 style="text-align: left;"&gt;&lt;span style="color: #434343;"&gt;4a. Talk to the printer from the browser (Web Bluetooth)&lt;/span&gt;&lt;/h3&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Chromium-based browsers can pair with some Bluetooth LE printers directly via the &lt;/span&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Web Bluetooth API&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;. The browser handles pairing; your code writes bytes to a characteristic. Good fit for handheld label printers where each user has their own device.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;In &lt;/span&gt;&lt;a href="https://w.virit.in/printing"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;the demo app&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;, &lt;code&gt;DymoLetraTag200B&lt;/code&gt; add-on wraps the pairing + characteristic writes as a hidden Vaadin component. The server side is mercifully thin:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;var name = new VTextField("Text");&lt;/span&gt;&lt;br&gt;&lt;span&gt;name.setValue("Vaadin");&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;var printer = new DymoLetraTag200B();&lt;/span&gt;&lt;br&gt;&lt;span&gt;add(printer); // must live in the component tree&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;add(new HorizontalFloatLayout(name, new Button("Print using Dymo LetraTag 200B",&lt;/span&gt;&lt;br&gt;&lt;span&gt; e -&amp;gt; printer.print("}&amp;gt; " + name.getValue()))));&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/dymo-letra-web-ble.webp?width=1500&amp;amp;height=633&amp;amp;name=dymo-letra-web-ble.webp" width="1500" height="633" alt="The Chrome Bluetooth pairing dialog listing a &amp;quot;LetraTag&amp;quot; device and the label printer in action" style="height: auto; max-width: 100%; width: 1500px; margin: 32px 0px 19px;"&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;&lt;em&gt;&lt;span style="font-size: 16px;"&gt;The Chrome Bluetooth pairing dialog listing a "LetraTag" device and the label printer in action&lt;/span&gt;&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Two older community examples worth a look:&lt;/span&gt;&lt;/p&gt; 
&lt;ul style="text-align: left;"&gt; 
 &lt;li&gt;&lt;code&gt;&lt;a href="https://github.com/Wnt/flow-bluetooth-printer"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;flow-bluetooth-printer&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/code&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://github.com/vaadin/point-of-sales-ai-demo"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin Point-of-Sale AI demo&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — a more recent, vibe-coded exploration.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;&lt;span style="font-weight: bold;"&gt;Limits: &lt;/span&gt;Firefox and Safari don't ship Web Bluetooth, the pairing UX surprises first-time users, and every printer model needs its own protocol wrapper. Works great if you control the hardware and the browser — kiosk, warehouse tablet, company-issued phones. Less great as a general public-internet feature.&lt;/span&gt;&lt;/p&gt; 
&lt;h3 style="text-align: left;"&gt;&lt;span style="color: #434343;"&gt;4b. Print from the server (or a custom service)&lt;/span&gt;&lt;/h3&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The most undervalued option. If your users are on known networks, or if "the receipt shows up within a minute" is good enough, server-side printing is almost always simpler to operate than client-side Bluetooth.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;From Linux, the brute-force approach is still fine:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```bash&lt;br&gt;lp /tmp/generated.pdf&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;One &lt;code&gt;Runtime.exec&lt;/code&gt;, one temp file, done — provided the printer is configured and reachable from the server. For anything more structured, &lt;/span&gt;&lt;a href="https://github.com/harwey/cups4j"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;cups4j&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; lets you submit jobs, query queues, and list printers from Java. Many modern office printers expose a CUPS endpoint themselves, so no intermediate print server is required — just a hole in the firewall.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;For printers that can't reach the server, flip the connection: let the printer side open a socket to the server instead. In the demo, a small Raspberry Pi talks BLE to the same Dymo as above (it could just as well drive a regular office printer) and registers itself over a WebSocket. The server broadcasts jobs on that socket; the Pi does the BLE write locally.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;A minimal implementation — one Spring &lt;code&gt;@Service&lt;/code&gt; that doubles as a &lt;code&gt;TextWebSocketHandler&lt;/code&gt;:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;@Service&lt;/span&gt;&lt;br&gt;&lt;span&gt;public class RemotePrinterService extends TextWebSocketHandler {&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; private final Set&amp;lt;WebSocketSession&amp;gt; printers = new CopyOnWriteArraySet&amp;lt;&amp;gt;();&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; public boolean printerAvailable() { return !printers.isEmpty(); }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; public void print(String text) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; TextMessage msg = new TextMessage(text);&lt;/span&gt;&lt;br&gt;&lt;span&gt; for (WebSocketSession s : printers) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; try {&lt;/span&gt;&lt;br&gt;&lt;span&gt; synchronized (s) { s.sendMessage(msg); }&lt;/span&gt;&lt;br&gt;&lt;span&gt; } catch (IOException e) { printers.remove(s); }&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; @Override public void afterConnectionEstablished(WebSocketSession s) { printers.add(s); }&lt;/span&gt;&lt;br&gt;&lt;span&gt; @Override public void afterConnectionClosed(WebSocketSession s, CloseStatus c) { printers.remove(s); }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;@Configuration&lt;/span&gt;&lt;br&gt;&lt;span&gt;@EnableWebSocket&lt;/span&gt;&lt;br&gt;&lt;span&gt;class PrinterWebSocketConfig implements WebSocketConfigurer {&lt;/span&gt;&lt;br&gt;&lt;span&gt; private final RemotePrinterService service;&lt;/span&gt;&lt;br&gt;&lt;span&gt; PrinterWebSocketConfig(RemotePrinterService s) { this.service = s; }&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry r) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; r.addHandler(service, "/ws/printer").setAllowedOriginPatterns("*");&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;From a Vaadin view you don't see the WebSocket at all — it's just a method call:&lt;/span&gt;&lt;/p&gt; 
&lt;pre style="text-align: left;"&gt;&lt;span&gt;```java&lt;br&gt;if (remotePrinterService.printerAvailable()) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; remotePrinterService.print(LocalDateTime.now() + " @ server time");&lt;/span&gt;&lt;br&gt;&lt;span&gt; Notification.show("Print job sent");&lt;/span&gt;&lt;br&gt;&lt;span&gt;} else {&lt;/span&gt;&lt;br&gt;&lt;span&gt; Notification.show("Printer offline");&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The Raspberry Pi side is beside the point of this post, but if you're curious &lt;/span&gt;&lt;a href="https://github.com/viritin/webbluetooth-drivers/tree/main/rpi-letra-example"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;the Java sources are on GitHub&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; (yes, modern Java runs great even on a $15 Pi Zero 2W with 512mb of memory 😎).&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left; font-weight: bold;"&gt;Architecture at a glance:&lt;/p&gt; 
&lt;p style="text-align: left; font-weight: bold;"&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/Screenshot%202026-04-29%20at%2015.32.45.png?width=3184&amp;amp;height=560&amp;amp;name=Screenshot%202026-04-29%20at%2015.32.45.png" width="3184" height="560" alt="Screenshot 2026-04-29 at 15.32.45" style="height: auto; max-width: 100%; width: 3184px;"&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;The key detail: the WebSocket is dialed &lt;span style="font-weight: bold;"&gt;from the Pi outward&lt;/span&gt;, not from the server inward. The printer can sit behind any NAT / firewall the office already has — no inbound ports, no VPN, no static IP. The server only ever &lt;em&gt;answers&lt;/em&gt; connections; it doesn't have to find the printer.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;Nothing about this pattern is BLE-specific. Swap the Pi for a workstation on an office LAN, the BLE write for a lp call, and you have the same design for any USB or network printer — without needing any firewall adjustments. Just be sure to complete the PoC with some security adjustments or I'll be printing your secret documents with my Dymo 🤓.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="text-align: left;"&gt;&lt;span&gt;Picking an approach&lt;/span&gt;&lt;/h2&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse; width: 100.047%; height: 368.711px;"&gt;
  &lt;colgroup&gt;
   &lt;col style="width: 31.5232%;" width="238"&gt;
   &lt;col style="width: 36.0265%;" width="272"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 44.1953px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; text-align: center; height: 44.1953px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px; font-weight: bold;"&gt;Need&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; text-align: center; height: 44.1953px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px; font-weight: bold;"&gt;Reach for&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 57.1719px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 57.1719px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Occasional print of an existing view&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 57.1719px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;&lt;code&gt;@media print&lt;/code&gt; + &lt;code&gt;.noprint&lt;/code&gt;&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 51px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 51px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Printing a virtualized Grid or chart&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 51px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Dedicated print route + iframe trick&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 51px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 51px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Pixel-perfect documents, archival&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 51px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Generated PDF + download link&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 57.1719px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 57.1719px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Desktop-app feel, one-click print&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 57.1719px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Generated PDF + &lt;code&gt;PrintPdfButton&lt;/code&gt;&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 51px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 51px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Direct-to-label, controlled hardware&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 51px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Web Bluetooth&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 57.1719px;"&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 57.1719px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Fixed printers, automation, remote locations&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 1px solid #d1d9e0; height: 57.1719px;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;Server-side (&lt;code&gt;lp&lt;/code&gt; / cups4j / WebSocket bridge)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p style="text-align: left;"&gt;&lt;span&gt;None of these is wrong — they optimize for different things. Start from the user's actual need ("do they want the paper, the file, or the automation?") and the integration pressure falls into place.&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fprinting-and-saving-pdfs-from-web-apps&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>category/development</category>
      <pubDate>Wed, 29 Apr 2026 14:06:46 GMT</pubDate>
      <author>matti@vaadin.com (Matti Tahvonen)</author>
      <guid>https://blog.vaadin.com/printing-and-saving-pdfs-from-web-apps</guid>
      <dc:date>2026-04-29T14:06:46Z</dc:date>
    </item>
    <item>
      <title>The Definitive Guide to Modernizing Java Swing Applications in 2026</title>
      <link>https://blog.vaadin.com/the-definitive-guide-to-modernizing-java-swing-applications</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/The%20Definitive%20Guide%20to%20Modernizing%20Java%20Swing%20Applications.png" alt="The Definitive Guide to Modernizing Java Swing Applications in 2026" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;Java Swing has been the workhorse of enterprise desktop applications for more than two decades. It powered internal tools, financial platforms, logistics systems, and ERP front-ends across industries where reliability mattered more than aesthetics. And for a long time, that bargain held.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;It no longer does.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;span&gt;Java Swing has been the workhorse of enterprise desktop applications for more than two decades. It powered internal tools, financial platforms, logistics systems, and ERP front-ends across industries where reliability mattered more than aesthetics. And for a long time, that bargain held.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;It no longer does.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;Table of Contents&lt;/h3&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications"&gt;Introduction&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#why-swing-applications-need-modernization-now"&gt;Why Swing Applications Need Modernization Now&lt;/a&gt; 
  &lt;ul&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#the-talent-gap-is-already-here"&gt;The Talent Gap Is Already Here&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#user-experience-expectations-have-shifted-permanently"&gt;User Experience Expectations Have Shifted Permanently&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#security-risks-are-expanding-not-shrinking"&gt;Security Risks Are Expanding, Not Shrinking&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#accessibility-and-regulatory-compliance-are-no-longer-optional"&gt;Accessibility and Regulatory Compliance Are No Longer Optional&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#cloud-native-architecture-demands-web-based-clients"&gt;Cloud-Native Architecture Demands Web-Based Clients&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#the-cost-of-waiting-is-compounding"&gt;The Cost of Waiting Is Compounding&lt;/a&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#the-three-migration-approaches"&gt;The Three Migration Approaches&lt;/a&gt; 
  &lt;ul&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#approach-1-lift-and-shift-run-swing-in-the-browser"&gt;Approach 1: Lift-and-Shift (Run Swing in the Browser)&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#approach-2-incremental-migration-screen-by-screen-modernization"&gt;Approach 2: Incremental Migration (Screen-by-Screen Modernization)&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#approach-3-full-rewrite"&gt;Approach 3: Full Rewrite&lt;/a&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#decision-framework-choosing-your-approach"&gt;Decision Framework: Choosing Your Approach&lt;/a&gt; 
  &lt;ul&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#factor-1-application-size-and-complexity"&gt;Factor 1: Application Size and Complexity&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#factor-2-quality-of-existing-business-logic"&gt;Factor 2: Quality of Existing Business Logic&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#factor-3-time-to-value"&gt;Factor 3: Time to Value&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#factor-4-team-composition"&gt;Factor 4: Team Composition&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#factor-5-budget-and-risk-tolerance"&gt;Factor 5: Budget and Risk Tolerance&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#decision-matrix"&gt;Decision Matrix&lt;/a&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#architecture-what-the-incremental-migration-looks-like"&gt;Architecture: What the Incremental Migration Looks Like&lt;/a&gt; 
  &lt;ul&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#phase-1-browser-based-access"&gt;Phase 1: Browser-Based Access&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#phase-2-screen-by-screen-replacement"&gt;Phase 2: Screen-by-Screen Replacement&lt;/a&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#phase-3-completion-and-swing-removal"&gt;Phase 3: Completion and Swing Removal&lt;/a&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#common-objections-and-how-to-address-them"&gt;Common Objections and How to Address Them&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#risk-matrix"&gt;Risk Matrix&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#timeline-estimates-by-application-size"&gt;Timeline Estimates by Application Size&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#what-a-successful-migration-project-looks-like"&gt;What a Successful Migration Project Looks Like&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#getting-started-a-practical-first-step"&gt;Getting Started: A Practical First Step&lt;/a&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/the-definitive-guide-to-modernizing-java-swing-applications#key-takeaways"&gt;Key Takeaways&lt;/a&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;Oracle stopped active development of Swing years ago. The framework still receives security patches and critical bug fixes, but&lt;/span&gt;&lt;a href="https://vaadin.com/blog/the-status-of-java-swing-and-why-consider-modernization"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;no new capabilities are being built&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;. Meanwhile, every force shaping enterprise software—cloud-native architecture, remote workforces, mobile-first expectations, and a generational shift in the developer talent pool—is pulling in the opposite direction from a desktop-only UI toolkit.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;If you are responsible for a Java Swing application in 2026, the question is not whether to modernize. It is how to modernize without destroying the business logic, domain expertise, and institutional knowledge embedded in your existing codebase.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This guide lays out the three primary migration approaches, provides a decision framework for choosing between them, and walks through the timelines, risks, and architectural considerations that determine whether a Swing modernization project succeeds or stalls.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Why Swing Applications Need Modernization Now&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Before diving into strategies, it is worth understanding the converging pressures that have made Swing modernization urgent rather than aspirational. These are not theoretical concerns—they are operational realities that compound with each year of delay.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Talent Gap Is Already Here&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The developer ecosystem has moved decisively to web-based frameworks. Computer science programs do not teach Swing. Bootcamps do not cover AWT layout managers. Every developer entering the workforce in 2026 has never written a line of Swing code and has no interest in starting.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;For organizations maintaining large Swing codebases, this creates an acute hiring problem. The pool of experienced Swing developers is shrinking through retirement and attrition. Replacements are expensive to find and even more expensive to retain, because those developers know their skills are scarce. Every year you wait, the talent market gets worse.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;User Experience Expectations Have Shifted Permanently&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Enterprise users in 2026 use Microsoft 365, Salesforce, SAP, and ServiceNow, Figma, Slack in their daily workflows. They interact with polished, responsive web interfaces that adapt to their screen size, support dark mode, and provide real-time collaboration. Then they switch to an internal Swing application that looks and feels like it was designed in 2005—because it was.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This is not vanity. Poor UX in internal tools reduces adoption, increases training costs, drives workarounds in spreadsheets, and quietly erodes data quality. When a logistics coordinator avoids the "official" tool because the web-based alternative is faster, you have a modernization problem masquerading as a data governance problem.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Operational Overhead of Desktop Swing Applications&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Desktop Swing applications carry significant operational overhead that web-based applications simply do not. Every deployment requires installing and managing a Java runtime on each user's machine. Every version update must be pushed to every endpoint. IT teams must maintain compatibility across different operating systems, hardware configurations, and Java versions. Remote access requires VPN or remote desktop infrastructure—itself a cost and security consideration.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;As organizations move to cloud platforms like AWS, Azure, or GCP, the operational gap becomes even more visible. Web applications deploy instantly, scale automatically, and require zero client-side installation. Swing applications do none of these things. The overhead of managing desktop installations across a workforce—patching, version control, OS compatibility—represents a meaningful cost that compounds with every passing year.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Security Risks Are Expanding, Not Shrinking&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Swing desktop applications carry a security profile that is increasingly difficult to defend in a modern threat landscape. Every client machine runs a full JVM with broad default permissions—access to the local file system, network, and external processes—creating a wide attack surface that must be secured and patched on every individual device. Oracle continues to issue Critical Patch Updates for Java SE quarterly, addressing vulnerabilities like CVE-2025-50106, CVE-2025-30749, and CVE-2025-30761 in recent cycles alone. Each patch must be deployed to every client installation, and any machine that falls behind becomes a potential entry point.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The architectural patterns common in Swing applications compound the problem. Many Swing clients hold database credentials locally or establish direct JDBC connections to back-end databases, bypassing the API layers and authentication gateways that modern&lt;/span&gt;&lt;a href="https://vaadin.com/blog/java-web-application-security-best-practices"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;web application security architectures&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; rely on. Connection strings embedded in client-side configuration files, unencrypted local caches of sensitive data, and JVM processes running with unrestricted permissions are not theoretical risks—they are the reality of how many Swing applications were built in an era when every user sat on a trusted corporate network.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Centralizing the application on the server—as both Phase 1 of the Vaadin Swing Modernization Toolkit and a full web migration achieve—eliminates the client-side JVM entirely. Database connections stay behind the firewall. Credentials never leave the server. Patching happens once, centrally, instead of across hundreds or thousands of client machines. The security posture improves immediately and structurally, not just incrementally.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Accessibility and Regulatory Compliance Are No Longer Optional&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The regulatory landscape has shifted decisively. The&lt;/span&gt;&lt;a href="https://www.levelaccess.com/compliance-overview/european-accessibility-act-eaa/"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;European Accessibility Act (EAA)&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;, enforceable since June 28, 2025, requires businesses operating in the EU digital market to meet EN 301 549 accessibility standards—which incorporate WCAG 2.1 Level AA. In the US, Section 508 and ADA-related litigation continue to expand the scope of digital accessibility obligations. Organizations that provide software to government agencies, financial institutions, healthcare systems, or public-facing services are increasingly required to demonstrate accessibility compliance as a condition of procurement.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Java Swing was never designed with web accessibility standards in mind. Swing components have their own accessibility API (the Java Accessibility API), but it does not map cleanly to WCAG criteria, ARIA roles, or the assistive technology ecosystem that has standardized around web content. A Swing application cannot produce a VPAT (Voluntary Product Accessibility Template) that meets WCAG 2.1 AA in any meaningful sense, because the framework does not generate the semantic HTML, keyboard navigation patterns, or screen reader hooks that the standard requires.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This is not a cosmetic gap. For organizations selling into the EU market, serving government clients, or operating in regulated industries, a Swing front end is becoming a compliance liability. Modern Java web frameworks like&lt;/span&gt;&lt;a href="https://vaadin.com/accessibility"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin provide WCAG 2.1 AA certified components&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; out of the box—migrating to web views does not just improve the user experience, it resolves a regulatory exposure that will only grow more acute as enforcement matures.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Cost of Waiting Is Compounding&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Legacy maintenance is not a flat cost. It compounds. Each year of deferred modernization means more code written against deprecated APIs, more institutional knowledge concentrated in fewer people, and&lt;/span&gt;&lt;a href="https://vaadin.com/blog/java-swing-tech-debt-modernization"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;more technical debt layered onto a foundation that is not receiving investment&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;. The longer you stay on Swing, the more expensive any future change becomes—whether that is hiring, migrating, or simply maintaining what you already have.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;The Three Modernization Approaches&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Every Swing modernization project falls into one of three categories. Each has distinct cost profiles, risk characteristics, and timelines. Understanding the trade-offs is essential before committing resources.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 1057px; height: 580px;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/Java%20Swing%20Modernization%20Approaches.jpg?width=1980&amp;amp;height=1089&amp;amp;name=Java%20Swing%20Modernization%20Approaches.jpg" width="1980" height="1089" alt="Java Swing Modernization Approaches" style="height: auto; max-width: 100%; width: 1980px;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Approach 1: Lift-and-Shift (Run Swing in the Browser)&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The fastest path to web-based access is to run the existing Swing application on a server and stream its rendered output to a web browser. The&lt;/span&gt;&lt;a href="https://vaadin.com/swing"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin Swing Modernization Toolkit&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; supports this as its first phase, acting as a bridge between the Java application and the browser. The Swing UI renders on the server and is delivered to the client via the browser, with user interactions relayed back transparently.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;How it works:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The application continues to run as a standard Swing application on a server-side JVM. A rendering layer captures the visual output and streams it to the browser. Mouse clicks, keystrokes, and other input events are captured in the browser and sent back to the server. From the application's perspective, it is running on a desktop—it simply does not know the "desktop" is actually a remote browser session. Minor modifications are needed to support multi-tenancy—ensuring the application can safely serve multiple concurrent users from a single server process—and to handle web-appropriate session lifecycle (timeouts, browser navigation).&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Advantages:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Minimal source code modification required. The existing application runs largely as-is, though some changes are needed to enable multi-tenancy (serving multiple users from a single JVM).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Fastest time to browser-based access—often measurable in hours or days, not weeks or months, depending on the complexity of multi-tenancy adaptations. Ongoing server infrastructure costs scale with concurrent users.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Eliminates the need to install or manage a Java runtime (JVM) on every user's machine—users access the application through any standard web browser.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Enables remote access and centralized deployment immediately.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Limitations if you stop here:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;The visual design and interaction model do not improve. The application still looks and behaves like a desktop application—rendered in a browser window but not redesigned for it. The deployment model changes (from a locally installed desktop app to a browser-accessible service), which is itself a meaningful improvement, but the UI does not adapt to screen sizes, support modern interaction patterns, or feel like a native web application.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Server resource requirements are significant. The JVM moves from each client machine to a centralized server—but each concurrent user session still runs a full Swing application instance on that server, consuming CPU, memory, and network bandwidth proportional to the complexity of the UI.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;The deployment model changes—from a locally installed desktop application to a centrally deployed, browser-accessible service. This brings real operational benefits: easier updates, reduced IT overhead, and remote access. However, the underlying application modernization remains untouched. The technology debt is still there; only the delivery mechanism has changed.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;The critical distinction is whether lift-and-shift is your end state or your starting point. Some tools treat browser-based rendering as the final destination, leaving you with a streamed desktop UI and no path forward. The&lt;/span&gt;&lt;a href="https://vaadin.com/swing"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin Swing Modernization Toolkit&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; treats it as Phase 1—a foundation that unlocks incremental screen-by-screen modernization (Approach 2 below). You get browser access in weeks, then continue modernizing without starting over.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Best suited for:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Organizations that need browser access urgently (regulatory, remote access mandates) as a first milestone, with the option to continue modernizing incrementally. Also appropriate for applications with a limited remaining lifespan where deeper investment is not justified.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Typical timeline:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Hours to days for browser-based access, depending on the complexity of multi-tenancy adaptations. Ongoing server infrastructure costs scale with concurrent users.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Approach 2: Incremental Migration (Screen-by-Screen Modernization)&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The incremental approach replaces individual Swing views with modern web views one screen at a time, while keeping the rest of the application functional. This is the strategy that balances risk reduction with genuine modernization—you are improving the application continuously rather than waiting for a big-bang replacement.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;How it works:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; If you have already completed a lift-and-shift (Approach 1), you are ready to begin. If not, the process starts the same way: the application is adapted to run on a server with its UI accessible in a browser. Then, individual screens are rebuilt using a modern Java web framework. During the transition period, users interact with a mix of legacy Swing views and new web views within the same application. Business logic, services, and domain models remain intact throughout—only the presentation layer changes.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;span style="width: 1068px; height: 712px;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;a href="https://vaadin.com/swing"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin's Swing Modernization Toolkit&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; is purpose-built for this two-phase approach. Phase 1 delivers browser access. Phase 2 provides automation to help convert Swing UIs into modern Java web views built with&lt;/span&gt;&lt;a href="https://vaadin.com/flow"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;. Both Swing views and new web views coexist during the transition, so the application remains fully functional at every stage.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Advantages:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Risk is distributed across the project timeline. Each screen migration is a small, bounded deliverable. If priorities shift, you can pause the migration and still have a partially modernized, fully functional application.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Business logic is preserved. The existing Java services, domain models, and back-end integrations do not need to be rewritten. Only the UI layer changes.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;User experience improves incrementally. High-traffic or high-pain screens can be prioritized first, delivering visible value early.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;The team learns as it goes. Lessons from early screen migrations inform later ones, reducing the error rate and increasing velocity over time.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Automation reduces manual effort. Vaadin's toolkit&lt;/span&gt;&lt;a href="https://vaadin.com/blog/from-swing-to-web-one-view-at-a-time"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;automates the conversion of many common Swing UI patterns&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; to web equivalents, preserving logic, structure, and even code comments.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Disadvantages:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Requires adaptation of the existing application for server-side multi-tenancy and browser-based rendering before incremental screen replacement can begin.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;The coexistence of Swing and web views means maintaining two rendering paradigms during the transition period, which adds some complexity.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Total project duration is longer than a clean rewrite in theory (though shorter in practice, because rewrites almost always exceed their estimates).&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Best suited for:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Organizations with large, business-critical Swing applications that cannot tolerate downtime or a prolonged parallel-run period. Also ideal when the existing Java business logic is sound and the primary modernization need is in the UI layer.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Typical timeline:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Phase 1 (browser access) in 4–8 weeks. Phase 2 (screen-by-screen migration) runs over 6–24 months depending on application size and team capacity. Most teams migrate 2–4 screens per sprint once they hit their stride.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Approach 3: Full Rewrite&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The full rewrite discards the existing codebase and rebuilds the application from the ground up using a modern technology stack. This is the approach that gets proposed most often in executive strategy meetings and succeeds least often in practice.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;How it works:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; A new application is designed and built to replace the existing Swing application entirely. The technology stack is chosen freely—React, Angular, or a Java web framework like&lt;/span&gt;&lt;a href="https://vaadin.com/flow"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; on the front end; Spring Boot, Quarkus, or Micronaut on the back end. The existing Swing application continues to run in production until the replacement is feature-complete and validated, at which point users are cut over.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Advantages:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Complete architectural freedom. You can adopt modern patterns (microservices, event-driven architecture, API-first design) without being constrained by legacy decisions.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;No legacy code to carry forward. The new codebase starts clean.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Opportunity to rethink workflows, not just replicate them. A rewrite can simplify overly complex screens, eliminate unused features, and redesign processes around current business needs.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Disadvantages:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;High risk of failure. Industry data consistently shows that large-scale rewrites take 2–3 times longer than estimated, cost 2–4 times more than budgeted, and frequently fail to reach feature parity with the application they are replacing. The Standish Group's research on large IT projects reinforces that the larger and longer the project, the lower its chance of success.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Loss of embedded domain knowledge. Legacy codebases contain years of business rule refinements, edge case handling, and integration workarounds that are not documented anywhere except in the code itself. Rewrites lose this knowledge, and the loss usually surfaces as defects in production.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Extended parallel-run period. The existing Swing application must remain operational and maintained throughout the rewrite, doubling the team's maintenance burden.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Opportunity cost. Every developer working on the rewrite is not working on new features, integrations, or improvements that could deliver business value today.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;API layer overhead. A rewrite using a JavaScript front end (React, Angular) requires designing and maintaining a REST or GraphQL API between the front end and back end—a significant ongoing engineering cost that did not exist in the original Swing architecture and does not exist in a Vaadin Flow migration.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Best suited for:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Applications that are small enough to rewrite within 6–12 months, applications where the business logic itself needs to change fundamentally (not just the UI), or situations where the existing codebase is so deteriorated that incremental migration is not feasible.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Typical timeline:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; 12–36 months for enterprise applications. Budget for 2x the initial estimate as a realistic baseline.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Decision Framework: Choosing Your Approach&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;The right approach depends on your specific constraints. Use the following factors to guide your decision.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Factor 1: Application Size and Complexity&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Small applications (under 50 screens, limited integrations) can tolerate a rewrite. The risk is manageable because the scope is bounded. Large applications (hundreds of screens, deep integrations with ERP, databases, and external systems) are almost always better served by incremental migration. The rewrite risk for a large application is simply too high for most organizations.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Factor 2: Quality of Existing Business Logic&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;If your Java business logic is well-structured—clean service layers, reasonable separation of concerns, tested domain models—then preserving it through incremental migration is the efficient choice. You are leveraging years of refinement rather than recreating it from scratch.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;If your business logic is tangled into Swing event handlers with no separation of concerns, incremental migration is harder (though still possible with refactoring). A rewrite may be more appropriate if the codebase is fundamentally unsalvageable.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Factor 3: Time to Value&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;How quickly do you need to show results?&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Hours to days:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Lift-and-shift gives you browser access immediately.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Weeks to months:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Incremental migration delivers visible improvements on a continuous basis, with browser access as the first milestone.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Months to years:&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; A full rewrite delivers nothing until it is finished, and it is not finished until it reaches feature parity.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;Most stakeholders and sponsors have a limited appetite for long projects with no intermediate deliverables. Incremental migration aligns better with how organizations actually fund and govern IT initiatives.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Factor 4: Team Composition&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;If your team is primarily Java developers, an approach that preserves the Java stack is significantly less disruptive. Incremental migration with a&lt;/span&gt;&lt;a href="https://vaadin.com/blog/comparing-java-gui-frameworks-vaadin-javafx-and-swing"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Java web framework like Vaadin&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; means the team can be productive immediately without learning an entirely new language and ecosystem.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;If you are building a new team or the existing team is already fluent in modern JavaScript frameworks, a rewrite using React or Angular may be viable—but factor in the back-end transition cost as well.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Factor 5: Budget and Risk Tolerance&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Lift-and-shift has the lowest upfront cost but does not solve the underlying problem. Incremental migration has a moderate and predictable cost that scales linearly with the number of screens migrated. Full rewrites have the highest cost, the widest variance in actual-vs-estimated spend, and the highest risk of failure.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Decision Matrix&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;div&gt; 
 &lt;table style="border-collapse: collapse; border: medium none currentcolor;"&gt;
  &lt;colgroup&gt;
   &lt;col width="151"&gt;
   &lt;col width="213"&gt;
   &lt;col width="146"&gt;
   &lt;col width="141"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Factor&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Lift-and-Shift&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Incremental Migration&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Full Rewrite&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Time to browser access&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Hours–days&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;4–8 weeks&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;12–36 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;UX improvement&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;None&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Continuous&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;All at once (if completed)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Business logic preserved&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Yes (untouched)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Yes (reused)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;No (rewritten)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Risk level&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low–Medium&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;High&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Cost predictability&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;High&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;High&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Team disruption&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Minimal&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Moderate&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Significant&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Long-term technical debt&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Unchanged (unless you continue to Phase 2)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Eliminated incrementally&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Eliminated (if completed)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Path to full modernization&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Yes, via incremental migration&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Built in&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Yes (if completed)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Scales to large apps&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Yes&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Yes&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Poorly&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Architecture: What the Incremental Migration Looks Like&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Because incremental migration is the recommended approach for most enterprise Swing applications, it is worth understanding the architecture in more detail.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Phase 1: Browser-Based Access&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;The existing Swing application is deployed to a server environment. The&lt;/span&gt;&lt;a href="https://vaadin.com/swing"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Swing Modernization Toolkit&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; handles rendering the Swing UI in the browser and managing user sessions. Minor modifications are required to support multi-tenancy (multiple users running concurrent sessions) and web-appropriate behaviors (session timeout, browser navigation, etc.).&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;At the end of Phase 1, users access the application through a URL instead of a locally installed client. The application looks the same as before—but it now runs centrally, can be accessed from any device with a browser, and no longer requires a JVM on client machines.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This phase is not cosmetic. It solves real operational problems immediately. IT teams can deploy updates centrally instead of pushing installers to every client machine. Access control moves to URL-based authentication rather than per-machine configuration. Users working remotely—a permanent reality in most organizations—can access the application from any browser without VPN tunneling to a specific desktop.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Phase 2: Screen-by-Screen Replacement&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;With browser access established, the team begins replacing Swing views with modern web views. The toolkit provides automation that converts many Swing UI patterns—layouts, input fields, tables, dialogs—to their Vaadin web equivalents. Code structure, comments, and logic flow are preserved during the conversion.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Each migrated screen goes through a standard workflow: automated conversion, manual review and refinement, testing, and deployment. The application in production always contains a mix of remaining Swing views and completed web views. Users experience a progressively modernized interface with each release.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Teams typically prioritize screens based on a combination of user traffic, business criticality, and migration complexity. The highest-value, lowest-complexity screens go first, building team confidence and establishing patterns that accelerate later migrations.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This is where the Java advantage becomes decisive. Because&lt;/span&gt;&lt;a href="https://vaadin.com/flow"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin Flow is a server-side Java framework&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;, your existing Java developers write the new web views in the same language, on the same build system, using the same IDE they already know. There is no context-switching to JavaScript, no REST API layer to design between front end and back end, and no separate front-end build pipeline to maintain. The migration is a Java-to-Java transformation, which fundamentally changes the The toolkit integrates with standard Maven and Gradle builds, so it fits into existing CI/CD pipelines without requiring a parallel front-end toolchain. skills equation compared to approaches that require adopting an entirely new technology stack.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Phase 3: Completion and Swing Removal&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Once all screens have been migrated, the Swing rendering layer is removed entirely. The application is now a standard modern Java web application running on Vaadin Flow, with all the advantages of a web-native architecture: responsive layouts,&lt;/span&gt;&lt;a href="https://vaadin.com/accessibility"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;accessibility compliance&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;,&lt;/span&gt;&lt;a href="https://vaadin.com/components"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;modern component libraries&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;, and standard web deployment.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;At this point, the application is indistinguishable from one that was built as a web application from the start. It can be&lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/tools/kubernetes"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;deployed in containers, scaled horizontally&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; behind load balancers, and integrated into CI/CD pipelines using standard tooling. The Swing dependency—and all the operational and talent constraints that came with it—is gone.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Common Objections and How to Address Them&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Enterprise modernization projects inevitably face internal resistance. Here are the objections that surface most frequently and how to think about them.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;"Our Swing application works fine. Why change it?"&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;It works fine today, with the current team. The question is whether it will work fine in three years when two of your senior Swing developers have retired and the remaining team spends 60% of its time on maintenance rather than improvements. Modernization is not about fixing something broken—it is about preventing a predictable failure before it becomes an emergency.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;"We tried a rewrite before, and it failed."&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Many organizations carry the scars of a previous rewrite attempt. This is actually an argument for incremental migration, not against modernization. The incremental approach was designed specifically to avoid the failure modes of big-bang rewrites: unbounded scope, loss of business logic, and extended periods with no deliverable output. Every sprint produces a working, improved application.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;"Can we just use AI to rewrite everything?"&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;AI-assisted code transformation is a legitimate tool in the modernization toolkit, and some vendors are exploring this space. However, the challenge with Swing modernization is not primarily a code generation problem. It is an architecture problem—how do you transition a desktop application to a web deployment model while preserving business logic and maintaining continuity of service? Automated code translation can accelerate individual screen conversions (and the Vaadin toolkit uses automation for exactly this purpose), but it does not replace the architectural strategy, phased deployment plan, and organizational change management that determine whether a modernization project succeeds.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;"We should skip Java web frameworks and go straight to React or Angular."&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;This is viable if you are willing to rewrite your back end as well, hire or retrain your team in JavaScript/TypeScript, and accept the timeline and risk profile of a full rewrite. For organizations whose core competency is Java—and whose team knows Java—staying in the Java ecosystem for the front end eliminates an enormous category of project risk and dramatically shortens the timeline to first results. (For a detailed comparison, see&lt;/span&gt;&lt;a href="https://vaadin.com/blog/4-java-swing-alternatives-for-building-modern-web-apps-vaadin"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Java Swing alternatives for building modern web apps&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;.) Beyond the language switch, the JavaScript ecosystem introduces its own complexity: npm dependency management, frequent framework version upgrades, bundler configuration, and a front-end build pipeline that must be maintained alongside your Java back end. For teams accustomed to the relative stability of the Java ecosystem, this is not a trivial operational change.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Risk Matrix&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Every modernization approach carries risks. Identifying them early allows you to mitigate them structurally rather than reactively.&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-collapse: collapse; border: medium none currentcolor;"&gt;
  &lt;colgroup&gt;
   &lt;col width="200"&gt;
   &lt;col width="104"&gt;
   &lt;col width="141"&gt;
   &lt;col width="206"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Risk&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Lift-and-Shift&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Incremental&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Full Rewrite&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Project failure / cancellation&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;High&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Loss of business logic&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;None&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;None&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Significant&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Scope creep&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Very High&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Extended timeline&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Very High&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;User disruption during transition&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low (gradual)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;High (big-bang cutover)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Talent dependency on legacy skills&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Unchanged&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Decreasing over time&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Eliminated (but new skills required)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Integration breakage&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;None&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Low (APIs preserved)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;High (new integrations)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Timeline Estimates by Application Size&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;These estimates assume a typical enterprise context: a dedicated team of 3–6 developers, standard code review and QA processes, and normal organizational decision-making cadence.&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-collapse: collapse; border: medium none currentcolor;"&gt;
  &lt;colgroup&gt;
   &lt;col width="174"&gt;
   &lt;col width="103"&gt;
   &lt;col width="162"&gt;
   &lt;col width="212"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Application Size&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Lift-and-Shift&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Incremental Migration&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; padding-left: 24px;"&gt;&lt;strong&gt;&lt;span&gt;Full Rewrite&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Small (10–30 screens)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;1–3 weeks&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;3–6 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;6–12 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Medium (30–100 screens)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;2–6 weeks&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;6–12 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;12–24 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Large (100–300 screens)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;4–8 weeks&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;12–24 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;24–36+ months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 52.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;Very Large (300+ screens)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;6–12 weeks&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;18–36 months&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="padding-left: 24px;"&gt;&lt;span&gt;36–60+ months (high failure risk)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;Note that for incremental migration, these timelines include Phase 1 (browser access). Visible modernization begins within the first few months and continues on a predictable cadence throughout the project.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;What a Successful Migration Project Looks Like&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Having guided numerous organizations through Swing modernization, a pattern emerges in the projects that succeed.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Weeks 1–4: Assessment and architecture.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The team inventories the existing application—screens, integrations, business logic layers, and technical debt. They select the migration approach, define the Phase 1 scope, and set up the server infrastructure for browser-based access.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Weeks 4–8: Phase 1 delivery.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The existing Swing application goes live in the browser. Users get remote access. IT gets centralized deployment. The team has now delivered a tangible result to stakeholders, which builds confidence and sustains funding for Phase 2.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Months 3–6: First wave of screen migrations.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The team migrates the highest-priority screens—typically the ones that users interact with most frequently or that cause the most support tickets. These early migrations establish the team's conversion patterns, testing strategies, and deployment cadence. Velocity typically doubles between the first and third screen migration as the team internalizes the toolkit's automation capabilities.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Months 6–18+: Steady-state migration.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The team settles into a predictable rhythm of 2–4 screens per sprint. Each release is a small improvement, each sprint reduces the remaining Swing surface area, and each month the application looks and feels more modern. The organization can adjust pace, reprioritize screens, or pause without losing progress.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Final milestone: Swing removal.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The last Swing view is replaced, the rendering bridge is removed, and the application is a fully modern Java web application. For many organizations, this moment is anticlimactic—by the time it arrives, 90% of the application has already been modernized and running smoothly in production for months. (For a real-world example of this pattern, see&lt;/span&gt;&lt;a href="https://vaadin.com/blog/izums-migration-from-swing-to-web"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;how IZUM migrated over 1,000 Swing views with 90% code reuse&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;.)&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Getting Started: A Practical First Step&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;If you are evaluating how to modernize a Java Swing application, the most productive first step is an assessment of your current codebase. This assessment should answer:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;How many screens does the application have, and which are most heavily used?&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;How cleanly is business logic separated from the Swing UI layer?&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;What external integrations exist, and how are they accessed?&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;What is the current server infrastructure, and what constraints does it impose?&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;What is the team's familiarity with modern Java web frameworks?&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;These inputs feed directly into the decision framework above and allow you to estimate timelines, costs, and risks with reasonable accuracy.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;You do not need to answer these questions alone. Vaadin offers a&lt;/span&gt;&lt;a href="https://vaadin.com/solutions/migrate-from-swing"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Swing modernization assessment&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; that evaluates your application against these criteria and provides a concrete recommendation with a preliminary migration plan, including timeline estimates and resource requirements. If you are managing a Swing application and considering modernization, this is the fastest way to move from "we should do something" to "here is what we should do, how long it will take, and what it will cost."&lt;/span&gt;&lt;a href="https://vaadin.com/swing"&gt;&lt;span style="color: #1155cc;"&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fthe-definitive-guide-to-modernizing-java-swing-applications&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>category/business</category>
      <pubDate>Fri, 24 Apr 2026 13:36:43 GMT</pubDate>
      <guid>https://blog.vaadin.com/the-definitive-guide-to-modernizing-java-swing-applications</guid>
      <dc:date>2026-04-24T13:36:43Z</dc:date>
      <dc:creator>Roman Kałkowski</dc:creator>
    </item>
    <item>
      <title>How To Style Vaadin Components for Both Aura and Lumo</title>
      <link>https://blog.vaadin.com/how-to-style-vaadin-components-for-both-aura-and-lumo</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/how-to-style-vaadin-components-for-both-aura-and-lumo" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/stylingvaadincomponentsforauranadlumo.png" alt="How to style vaadin components for the Aura and Lumo themes." class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;span&gt;By default, &lt;a href="https://vaadin.com/docs/latest/components"&gt;Vaadin components&lt;/a&gt; are rendered with minimal base styles. These provide a neutral foundation—useful when you want to build a custom theme from scratch or create a look that differs from the built-in options.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;span&gt;By default, &lt;a href="https://vaadin.com/docs/latest/components"&gt;Vaadin components&lt;/a&gt; are rendered with minimal base styles. These provide a neutral foundation—useful when you want to build a custom theme from scratch or create a look that differs from the built-in options.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Vaadin comes with two main themes: &lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/styling/themes/lumo"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Lumo&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; and &lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/styling/themes/aura"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Aura&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;. Lumo is the original theme, built around a flexible set of design tokens and supporting variants like dark mode and compact density. Aura, introduced more recently, offers a more modern and polished visual style out of the box.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/lumo-light-and-dark.VV7Yj_MG_Z11aym0.webp?width=2018&amp;amp;height=636&amp;amp;name=lumo-light-and-dark.VV7Yj_MG_Z11aym0.webp" width="2018" height="636" alt="lumo-light-and-dark.VV7Yj_MG_Z11aym0" style="height: auto; max-width: 100%; width: 2018px; margin: 32px auto 17px; display: block;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="font-size: 16px;"&gt;&lt;em&gt;Lumo theme in light and dark mode.&lt;/em&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span style="background-color: transparent; font-size: 1.125rem; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;Styling for just &lt;/span&gt;&lt;span style="background-color: transparent; font-size: 1.125rem; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;one of these themes is usually straightforward. The challenge begins when you need your custom components to work seamlessly across both.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;That’s where things can start to drift. Components may look correct in isolation, but small differences in spacing, focus styles, or states can make the overall UI feel inconsistent.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This guide presents a practical, production-ready approach for styling Vaadin components so they work cleanly in both Aura and Lumo. It focuses on three real integration styles from this project:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/directory/component/colorpicker"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Color Picker&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; (custom Lit web component)&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/directory/component/beantable"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Bean Table&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; (server-side component with custom DOM)&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/directory/component/pivottable-for-vaadin"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Pivot Table&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; (third-party JavaScript widget through connector)&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;The main goal is visual cohesion: custom components should feel native next to standard Vaadin components in both Aura and Lumo.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;What visual cohesion means in practice&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Visual cohesion isn’t about pixel-perfect matching—it’s about consistency in how components behave and communicate state.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;In practice, users don’t consciously compare components. Instead, they pick up on subtle UX cues. When those cues are inconsistent, the UI feels fragmented—even if every individual component looks fine on its own.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;In practice, cohesion is measured by whether custom and built-in fields share the same UX cues:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Required indicator behavior&lt;/strong&gt; – How required fields are marked (asterisk, placement, spacing). Inconsistencies here make forms feel uneven and harder to scan.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Focus treatment&lt;/strong&gt; – The visual language for focus—ring, border, shadow, or combination. This is critical for both usability and accessibility, and mismatches are immediately noticeable.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Read-only and disabled affordances &lt;/strong&gt;– How non-interactive fields are presented. Differences in opacity, contrast, or styling can confuse users about what is editable.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Invalid state contrast and border handling&lt;/strong&gt; – Error colors, border styles, and contrast. These need to feel consistent across components to maintain clarity and trust.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Dialog/popup surfaces, radii, and elevation&lt;/strong&gt; – Background colors, border radius, and elevation (shadows). These define the overall “material feel” of the UI and are especially noticeable when mixing components.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;If these elements behave consistently, custom and built-in components feel like part of the same system. If they don’t, the UI starts to feel stitched together, even if the differences are subtle.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;img src="https://blog.vaadin.com/hs-fs/hubfs/color-scheme-light-dark.B0JqeDpW_Za4Vcr.webp?width=3024&amp;amp;height=1714&amp;amp;name=color-scheme-light-dark.B0JqeDpW_Za4Vcr.webp" width="3024" height="1714" alt="color-scheme-light-dark.B0JqeDpW_Za4Vcr" style="height: auto; max-width: 100%; width: 3024px; margin: 32px 0px 19px;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;div style="color: #232e3c; background-color: #ffffff;"&gt; 
 &lt;p style="color: #576b85; text-align: left; font-size: 16px;"&gt;&lt;span style="color: #232e3c; font-family: nb_international_pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; letter-spacing: 0.01em;"&gt;&lt;em&gt;The global color scheme for Aura is either light or dark, depending on the operating system preference.&lt;/em&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;/div&gt; 
&lt;h2&gt;&lt;span&gt;Core architecture rule&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;To make this work in practice, you need a clear separation between what stays the same and what changes across themes. &lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;The guiding principle is simple: use one behavior path and one structural DOM path, and let the theme handle only the visual layer.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;That means:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;The base structure and behaviour of your component stay the same regardless of theme Theme detection is thin and explicit.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Styling is token-driven through internal component variables.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;This approach keeps maintenance predictable as both themes evolve.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Pattern map by component type&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;How you apply this pattern depends on the type of component you’re working with – keep it simple and choose the smallest pattern that fits your component architecture.&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;For Lit-based web components):&lt;/strong&gt; Use &lt;code&gt;ThemeDetectionMixin&lt;/code&gt;, style with host selectors, reflect state to host attributes.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;For server-side custom components:&lt;/strong&gt; Detect active theme from root CSS markers, add root class (&lt;code&gt;aura&lt;/code&gt; or &lt;code&gt;lumo&lt;/code&gt;), scope CSS under that class.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;With third-party JS libraries:&lt;/strong&gt; Keep vendor CSS as baseline, then override under &lt;code&gt;.aura ...&lt;/code&gt; and &lt;code&gt;.lumo ...&lt;/code&gt; with Vaadin tokens.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Finally, for extended Vaadin components (for example checkbox as toggle):&lt;/strong&gt; Use theme variant &lt;code&gt;+ ::part(...)&lt;/code&gt;, and a fallback chain (&lt;code&gt;lumo -&amp;gt; aura -&amp;gt; literal&lt;/code&gt;).&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;h2&gt;&lt;span&gt;Foundation styling pattern for all implementations&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Start with internal component tokens and consume only those in shared rules.&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```css&lt;br&gt;.my-component,&lt;/span&gt;&lt;br&gt;&lt;span&gt;:host {&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-text-color: var(--vaadin-text-color);&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-border-radius: var(--vaadin-radius-m);&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-focus-ring-color: var(--vaadin-focus-ring-color);&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-padding-s: var(--vaadin-padding-s);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;.my-component .field,&lt;/span&gt;&lt;br&gt;&lt;span&gt;:host .field {&lt;/span&gt;&lt;br&gt;&lt;span&gt; color: var(--my-text-color);&lt;/span&gt;&lt;br&gt;&lt;span&gt; border-radius: var(--my-border-radius);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;.lumo.my-component,&lt;/span&gt;&lt;br&gt;&lt;span&gt;:host([data-application-theme="lumo"]) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-border-radius: var(--lumo-border-radius-m);&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-padding-s: var(--lumo-space-s);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;.aura.my-component,&lt;/span&gt;&lt;br&gt;&lt;span&gt;:host([data-application-theme="aura"]) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-border-radius: var(--vaadin-radius-m);&lt;/span&gt;&lt;br&gt;&lt;span&gt; --my-padding-s: var(--vaadin-padding-s);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;This removes the need to duplicate full CSS blocks per theme.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Example 1: Color Picker (Lit web component)&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Let’s start with the most straightforward case: a Lit-based component.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Color Picker uses &lt;code&gt;ThemeDetectionMixin(ThemableMixin(LitElement))&lt;/code&gt;.&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;Theme is exposed as &lt;code&gt;data-application-theme&lt;/code&gt; on host.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;CSS uses host selectors for Aura and Lumo.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;State is reflected to the host (&lt;code&gt;invalid&lt;/code&gt;, &lt;code&gt;readonly&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;required&lt;/code&gt;, compact flags).&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;Minimal pattern:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```typescript&lt;br&gt;export class MyComponent extends ThemeDetectionMixin(ThemableMixin(LitElement)) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; static get is() {&lt;/span&gt;&lt;br&gt;&lt;span&gt; return "my-component";&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;pre&gt;&lt;span&gt;´´´css&lt;br&gt;:host([data-application-theme="aura"][invalid]) .field {&lt;/span&gt;&lt;br&gt;&lt;span&gt; border-color: var(--vaadin-error-color);&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;:host([data-application-theme="lumo"][invalid][disabled]) .field {&lt;/span&gt;&lt;br&gt;&lt;span&gt; opacity: 0.7;&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;If your component contains nested Vaadin components, make sure to forward the theme so everything stays in sync:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```typescript&lt;br&gt;&amp;lt;vaadin-custom-field theme="${ifDefined(this.theme)}"&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt; &amp;lt;vaadin-combo-box theme="${ifDefined(this.theme)}"&amp;gt;&amp;lt;/vaadin-combo-box&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&amp;lt;/vaadin-custom-field&amp;gt;&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;h2&gt;&lt;span&gt;Example 2: Bean Table (server-side custom DOM)&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;For server-side components, the approach is slightly different. Bean Table cannot use &lt;code&gt;ThemeDetectionMixin&lt;/code&gt; because it is not a Lit web component. Instead, it detects the theme on attach and adds class names to the component root.&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```java&lt;br&gt;@CssImport("./my-component.css")&lt;/span&gt;&lt;br&gt;&lt;span&gt;public class MyComponent extends Composite&amp;lt;Div&amp;gt; {&lt;/span&gt;&lt;br&gt;&lt;span&gt; @Override&lt;/span&gt;&lt;br&gt;&lt;span&gt; protected void onAttach(AttachEvent event) {&lt;/span&gt;&lt;br&gt;&lt;span&gt; super.onAttach(event);&lt;/span&gt;&lt;br&gt;&lt;span&gt; getElement().executeJs(&lt;/span&gt;&lt;br&gt;&lt;span&gt; "const s=getComputedStyle(document.documentElement);" +&lt;/span&gt;&lt;br&gt;&lt;span&gt; "const t=s.getPropertyValue('--vaadin-aura-theme').trim()==='1'?'aura':" +&lt;/span&gt;&lt;br&gt;&lt;span&gt; "(s.getPropertyValue('--vaadin-lumo-theme').trim()==='1'?'lumo':'');" +&lt;/span&gt;&lt;br&gt;&lt;span&gt; "if(t){this.classList.add(t);}"&lt;/span&gt;&lt;br&gt;&lt;span&gt; );&lt;/span&gt;&lt;br&gt;&lt;span&gt; }&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;CSS then stays cleanly scoped:&lt;/span&gt;&lt;/p&gt; 
&lt;pre&gt;&lt;span&gt;```css&lt;br&gt;.my-component {&lt;/span&gt;&lt;br&gt;&lt;span&gt; /* shared rules */&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;.lumo.my-component {&lt;/span&gt;&lt;br&gt;&lt;span&gt; /* lumo token mapping */&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;.aura.my-component {&lt;/span&gt;&lt;br&gt;&lt;span&gt; /* aura token mapping */&lt;/span&gt;&lt;br&gt;&lt;span&gt;}&lt;br&gt;```&lt;/span&gt;&lt;/pre&gt; 
&lt;p&gt;&lt;span&gt;One limitation to be aware of: this approach runs at attach time, so it doesn’t automatically react to live theme switching.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Example 3: Pivot Table (third-party legacy JS)&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;In this case, Pivot Table integrates an older jQuery-based library via a connector. A practical approach:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;Detect the theme in connector before rendering heavy UI.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Add root class (&lt;code&gt;aura&lt;/code&gt; or &lt;code&gt;lumo&lt;/code&gt;).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Keep the library’s native CSS as the baseline.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Apply Vaadin-specific overrides for each theme.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;For this kind of library, expect broader override surfaces than with modern Vaadin components.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Matching Vaadin overlay quality in third-party dialogs&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;When working with dialogs or popups from third-party libraries, simple color overrides aren’t enough.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;To achieve visual parity with Vaadin components, you often need to align with the same underlying design rules:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;Surface and text token mapping&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Radius and border treatment&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Elevation/shadow formulas&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Focus-visible ring behavior&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;In some cases, this means porting the same “CSS math” used by Vaadin overlays, so that add-on dialogs blend with Aura and Lumo.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Vaadin 25.1+ recommendations&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;A couple of practical tips can save you time when working across Aura and Lumo.&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Prefer Vaadin 25.1 or newer for Aura adaptation work. &lt;/strong&gt;Earlier versions of Aura evolved quickly, especially around design tokens and defaults. That made styling harder to stabilize, as small updates could introduce visual inconsistencies. From 25.1 onward, the theme is more predictable, which makes a token-driven approach much easier to maintain.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;&lt;strong&gt;Use unprefixed theme variants (25.1) where applicable. &lt;/strong&gt;They help custom components apply variants that work across both Lumo and Aura, so you can define them once without branching styles or logic per theme and keep your component APIs simpler and more consistent.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;h2&gt;&lt;span&gt;Current framework gaps&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;While the current approach works well, a few gaps still lead to extra duplication in custom components:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;Server-side API for active theme detection.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Shared JavaScript utility for theme detection usable outside Lit mixins.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Supported hook for dynamic theme switching in server-side and connector-based components.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;p&gt;&lt;span&gt;Until more standardized support is available, lightweight, reusable helper utilities remain the most practical approach.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Optional acceleration for dual-theme CSS&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;For larger components or design systems, writing everything by hand can get repetitive. In these cases, a utility-first layering approach can help. Map utility classes to Aura/Lumo token-backed variables for color, radius, spacing, and shadows to reduce repetitive handcrafted CSS.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;span&gt;Practical checklist&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;When implementing or reviewing a component, this is a good sanity check:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;span&gt;Pick architecture (Lit, server-side DOM, extended Vaadin component, or connector).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Define internal &lt;code&gt;--my-*&lt;/code&gt; tokens and consume only those in shared styles.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Add thin theme detection (&lt;code&gt;data-application-theme&lt;/code&gt; or root class tagging).&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Keep behavior and interaction logic theme-agnostic.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Add narrow Aura/Lumo overlays for visual differences.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Verify required, focus, invalid, readonly, disabled, and combined states.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Verify nested components and overlays for visual parity.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Re-test after Vaadin upgrades, especially around theme token changes.&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;h2&gt;&lt;span&gt;Final takeaway&lt;/span&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;You do not need separate component implementations for Aura and Lumo. A token-driven styling model plus a small theme-selection layer is enough to make components feel native across both and scales across:&lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;span&gt;Lit web components&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Server-side custom components&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Connector-based third-party widgets&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span&gt;Extended Vaadin components with part-based styling&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;Once the foundation is in place, supporting both themes becomes a small, predictable layer of work—not a second implementation to maintain.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Have you tried this approach? We’d love to hear what worked (and what didn’t) in your setup. Happy coding!&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fhow-to-style-vaadin-components-for-both-aura-and-lumo&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>theming</category>
      <category>custom components</category>
      <category>css</category>
      <category>category/development</category>
      <pubDate>Thu, 16 Apr 2026 11:39:03 GMT</pubDate>
      <author>tatu@vaadin.com (Tatu Lund)</author>
      <guid>https://blog.vaadin.com/how-to-style-vaadin-components-for-both-aura-and-lumo</guid>
      <dc:date>2026-04-16T11:39:03Z</dc:date>
    </item>
    <item>
      <title>Java Software Development in an AI-First World</title>
      <link>https://blog.vaadin.com/the-future-of-software-development-in-an-ai-first-world</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/the-future-of-software-development-in-an-ai-first-world" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/Blog_Img2.png" alt="Why Java is built for AI" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fthe-future-of-software-development-in-an-ai-first-world&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>AI</category>
      <category>Vaadin 25</category>
      <pubDate>Tue, 14 Apr 2026 06:31:24 GMT</pubDate>
      <author>fredu@vaadin.com (Fredrik Rönnlund)</author>
      <guid>https://blog.vaadin.com/the-future-of-software-development-in-an-ai-first-world</guid>
      <dc:date>2026-04-14T06:31:24Z</dc:date>
    </item>
    <item>
      <title>Vaadin 2026 Community Survey: What Java Developers Told Us</title>
      <link>https://blog.vaadin.com/vaadin-2026-community-survey-what-java-developers-told-us</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/vaadin-2026-community-survey-what-java-developers-told-us" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/Vaadin%20Community%20Survey%202026.png" alt="Vaadin 2026 Community Survey" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #000000;"&gt;We asked our customers and community how they use Vaadin,, what they value most, and what we need to do better. Here’s what we learned from the 2026 survey, even the parts that made us question what we thought we knew.&lt;br&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #000000;"&gt;We asked our customers and community how they use Vaadin,, what they value most, and what we need to do better. Here’s what we learned from the 2026 survey, even the parts that made us question what we thought we knew.&lt;br&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #000000;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse;"&gt;
  &lt;colgroup&gt;
   &lt;col width="208"&gt;
   &lt;col width="208"&gt;
   &lt;col width="208"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa;"&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; font-size: 30px;"&gt;&lt;span style="color: #3574e3;"&gt;&lt;strong&gt;691&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Respondents&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa;"&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; font-size: 30px;"&gt;&lt;span style="color: #3574e3;"&gt;&lt;strong&gt;98%&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Use for Work&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa;"&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px;"&gt;&lt;span style="font-size: 30px; color: #3574e3;"&gt;&lt;strong&gt;71%&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&amp;nbsp;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Are Developers&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Key Findings at a Nutshell&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;67% of people build internal applications, &lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt;and&lt;/span&gt;&lt;span style="color: #2c3e50;"&gt;&amp;nbsp;Vaadin’s core is enterprise data-heavy apps&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;63% of people chose Vaadin because they already had a Java team. &lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt;Existing skills drive adoption&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;40% faster time to market&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; and 30% lower project costs reported by developers&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;83% of commercial users&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; say that&amp;nbsp;UI components are their top value driver&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;35% adopted Vaadin for legacy migration&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; from Swing, JSF, GWT, and JavaFX&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;94% of commercial users&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; would be disappointed if Vaadin disappeared&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;Top requests:&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; more components, better documentation, React interoperability&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Who Responded to the 2026 Survey&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;691 developers took the time to tell us about their experiences with Vaadin in our 2026 community survey. 98% of people use Vaadin for work, not side projects or experiments, and 71% are developers (the rest are architects, team leads, and managers).&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;They come from a lot of different types of organizations:&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse;"&gt;
  &lt;colgroup&gt;
   &lt;col width="312"&gt;
   &lt;col width="312"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: middle; background-color: #1b2d3e; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span style="color: #ffffff;"&gt;Company Size&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: middle; background-color: #1b2d3e; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span style="color: #ffffff;"&gt;Industry Focus&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Startups (1–50)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Technology &amp;amp; Software&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Mid-market (50–1,000)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Financial Services&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Enterprise (1,000–10,000)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Manufacturing&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Large Enterprise (10,000+)&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Government &amp;amp; Public Sector&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;This isn’t a self-selected group of fans. There are a lot of different people building real enterprise Java web apps with real deadlines.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2; font-weight: bold;"&gt;Internal Apps Are the Most Popular&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;67% of survey respondents build internal applications with Vaadin. External-facing and SaaS applications account for roughly 30%.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;That makes sense with what we’ve always seen: Vaadin’s sweet spot is data-heavy business applications, like&amp;nbsp;the admin panels, dashboards, and operational tools that keep business running. These are the kinds of apps where developer productivity is more important than making sure the design is perfect for every pixel, where security needs are non-negotiable, and where the app needs to be maintained for years, not months.&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;If you’re building internal tools for a Java team, you’re not alone.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Why Java Teams Choose Vaadin for Enterprise Web Apps&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;We wanted to know what made them choose Vaadin. Anyone who has worked with enterprise Java will not be surprised by the top answer:&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;63% said they chose Vaadin because they already had a Java team.&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;That’s the main point of the value proposition. If your team knows Java, Spring Boot, and Maven, &lt;/span&gt;&lt;a href="https://vaadin.com/flow"&gt;&lt;u&gt;&lt;span style="color: #0563c1;"&gt;building web UIs in the same language&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span style="color: #2c3e50;"&gt; gets rid of the frontend bottleneck. You don't need a separate JavaScript team, no context-switching, and and you don't&amp;nbsp;need two build pipelines.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="line-height: 14.4px;"&gt;But having some experience with Java was only part of the picture. The second most important factor was &lt;span style="font-weight: bold;"&gt;developer productivity and faster development&lt;/span&gt; speed&lt;/span&gt;. Teams consistently reported that consolidating frontend and backend into a single Java stack cuts down coordination overhead and speeds up delivery cycles. Survey respondents said they got &lt;span style="font-weight: bold;"&gt;their product to market&amp;nbsp;40% faster than before&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;The third big reason was the &lt;strong&gt;need for a modern, web-based UI&lt;/strong&gt;&lt;span style="line-height: 14.4px;"&gt; without leaving the Java ecosystem. A lot of teams, especially those running desktop applications built on Swing or JavaFX, don't have to decide whether to move to the web; they just have to figure out how to do it without rewriting everything in a new language. Vaadin lets Java teams build modern, responsive web apps using the skills and infrastructure they already have.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.2;"&gt;&lt;span style="line-height: 14.4px;"&gt;It's interesting that &lt;/span&gt;&lt;a href="https://vaadin.com/components"&gt;&lt;u&gt;&lt;span style="line-height: 14.4px; color: #0563c1;"&gt;components&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span style="line-height: 14.4px;"&gt; weren’t the main reason teams chose Vaadin — but they are a major part of the value once teams start building. More on that below.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2; font-weight: bold;"&gt;&lt;span style="color: #1b2d3e;"&gt;40% Faster&lt;/span&gt;&lt;span style="color: #1b2d3e;"&gt; Time to Market, 30% Lower Costs&lt;/span&gt;&lt;/h2&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse; width: 100.047%; height: 78.195px;"&gt;
  &lt;colgroup&gt;
   &lt;col style="width: 41.3245%;" width="312"&gt;
   &lt;col style="width: 41.3245%;" width="312"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 78.195px;"&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; height: 78.195px;"&gt; &lt;p style="line-height: 1; text-align: center; margin-top: 0px; font-size: 30px;"&gt;&lt;span style="color: #3574e3;"&gt;&lt;strong&gt;40%&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Faster Time to Market&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; height: 78.195px;"&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; font-size: 30px;"&gt;&lt;span style="color: #3574e3;"&gt;&lt;strong&gt;30%&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Lower Project Costs&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p style="line-height: 1.5;"&gt;When developers switched to Vaadin, they said they could &lt;span style="font-weight: bold;"&gt;deliver&lt;/span&gt; &lt;span style="font-weight: bold;"&gt;their products to market 40% faster&lt;/span&gt; and &lt;span style="font-weight: bold;"&gt;save 30% on the total cost of the project&lt;/span&gt;.&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;Those are averages that people gave themselves, so use them as guides instead of strict benchmarks. But the pattern is consistent: teams that build their stack around Java ship faster and spend less time coordinating between the frontend and backend. You don't have to worry about designing a REST API layer, keeping a frontend build pipeline up to date, or handing off work between teams when your UI logic and business logic run on the same JVM.&lt;span style="color: #2c3e50;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;What Commercial Users Care About&amp;nbsp;Most&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;We asked people who have &lt;/span&gt;&lt;a href="https://vaadin.com/pricing"&gt;&lt;u&gt;&lt;span style="color: #0563c1;"&gt;commercial plans&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span style="color: #2c3e50;"&gt;, which features they think are the most useful. The results were obvious:&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse;"&gt;
  &lt;colgroup&gt;
   &lt;col width="312"&gt;
   &lt;col width="312"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: middle; background-color: #1b2d3e; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span style="color: #ffffff;"&gt;Feature&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: middle; background-color: #1b2d3e; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span style="color: #ffffff;"&gt;Very/Somewhat Valuable&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;UI Components&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;83%&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Hotfixes&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;57%&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Expert Chat&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;43%&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;Extended Maintenance&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa; border: 0.666667px solid #e0e4e8; text-align: center;"&gt; &lt;p style="line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #2c3e50;"&gt;42%&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p style="line-height: 1.5;"&gt;Components are by far the most important thing that adds value. Access to hotfixes and direct expert support are the last two things that make up the top tier. This is the kind of safety net that businesses need when an application is critical to their business.&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;At the bottom of the list are Acceleration Kits (11%), legacy modernization features (12%), and &lt;/span&gt;&lt;a href="https://vaadin.com/copilot"&gt;&lt;u&gt;&lt;span style="color: #0563c1;"&gt;Vaadin Copilot&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span style="color: #2c3e50;"&gt; (14%). That means we need to work on making some of our newer features more useful to developers.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Open Source Is the Starting Point&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;61% of respondents use the open-source version of Vaadin. That's by design; Vaadin's core framework is fully open source, and we want developers to be able to work without hitting the paywall.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;But here’s something we didn’t expect: in developer interviews conducted alongside this survey, many new users didn’t know that Vaadin is open source at all. We need to fix that gap in communication. If you’re evaluating &lt;a href="https://vaadin.com/docs"&gt;Java web frameworks&lt;/a&gt; and open-source licensing matters to you, Vaadin Core is Apache 2.0 licensed.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Swing to Web Migration: Legacy Modernization Is Growing&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;35% of the companies that took part in the survey chose Vaadin specifically when migrating from legacy frameworks like Swing, JavaFX, JSF, and GWT.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;We looked more closely at the group of people who said they keep Swing apps running:&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;78% said their legacy applications are very important to their business,&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; with 64% calling them “critical”&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;37% consider their legacy applications permanent parts of their infrastructure&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt;, not temporary fixes that need to be replaced.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;70% plan to run these apps for 5+ years &lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt;or more, or even forever&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;The majority are still actively developing&lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt; and adding features to their legacy codebases&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;This makes it clear that old Java apps aren't going away; they're just changing.&amp;nbsp; And when organizations decide to modernize, &lt;/span&gt;&lt;span style="color: #3574e3; font-weight: bold;"&gt;&lt;a href="https://vaadin.com/swing" style="color: #3574e3;"&gt;Vaadin is the best way to do it&lt;/a&gt;&lt;/span&gt;&lt;span style="color: #2c3e50;"&gt;&lt;/span&gt;&lt;span style="color: #2c3e50;"&gt;. 35% of those with Swing applications are already migrating to Vaadin, and another 20% are still deciding.&lt;/span&gt;&lt;br&gt;&lt;span style="color: #2c3e50;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Product-Market Fit: 94% Would Be Disappointed&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="font-weight: normal;"&gt;&lt;span style="color: #1b2d3e;"&gt;&lt;span style="color: #2c3e50;"&gt;We asked commercial users the classic question about product-market fit: “How disappointed would you be if you could no longer use Vaadin?”&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse;"&gt;
  &lt;colgroup&gt;
   &lt;col width="208"&gt;
   &lt;col width="208"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa;"&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; font-size: 30px;"&gt;&lt;span style="color: #3574e3;"&gt;&lt;strong&gt;94%&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Would Be Disappointed&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top; background-color: #f5f7fa;"&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; font-size: 30px;"&gt;&lt;span style="color: #3574e3;"&gt;&lt;strong&gt;6%&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt; &lt;p style="line-height: 1.2; text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span style="color: #6b7b8d;"&gt;Not Disappointed&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;&lt;span&gt;Only 6% claimed they would not be disappointed. 94% would be very or somewhat disappointed.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="line-height: 14.4px;"&gt;That’s a strong signal that for the teams using Vaadin commercially, it's not just a tool; it's the foundation of their infrastructure. Only 2% of commercial users don't have any active projects; the rest are both maintaining existing Vaadin apps and making new ones at the same time. It also means we have a duty to those teams to make sure that that releases happen on time, deliver long-term maintenance, and the roadmap is clear.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;When compared, free-tier users have a higher rate of non-use (8%) and less active development, which suggests that a commercial relationship leads to a stronger commitment to the platform.&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;Where We’re Falling Short&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;Honest feedback is more valuable than compliments. Here’s what respondents told us needs to improve:&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;Components need to go further. &lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt;The most common frustration was that Vaadin doesn’t have enough components, and existing ones need deeper functionality, like&amp;nbsp;advanced grid features like column span and freeze options, more customization out of the box.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;Documentation needs to be updated with new releases. &lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50; font-weight: normal;"&gt;Developers, especially those who are new to Vaadin, have a hard time finding up-to-date examples for newer versions. There is a specific gap in CSS styling documentation, even in older versions.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;The most common reason why more organizations &lt;span style="font-weight: bold;"&gt;don't use it is that it doesn't work with other frontend technologies&lt;/span&gt;. The most common request was for React integration. Vaadin has a TypeScript and React path with the same Vaadin component library for teams that need flexibility on the front end, but not many people know about this option.&lt;br&gt;&lt;span style="color: #2c3e50;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;strong&gt;&lt;span style="color: #2c3e50;"&gt;Developer availability &lt;/span&gt;&lt;/strong&gt;&lt;span style="color: #2c3e50;"&gt;is a real concern. Vaadin isn’t as widely known as some alternatives, and organizations report difficulty finding experienced developers.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;We’re taking all of this seriously. Better components, better documents, and better interoperability are the things that make the framework more useful for more teams.&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.2;"&gt;&lt;strong&gt;&lt;span style="color: #1b2d3e;"&gt;What the 2026 Survey Means for the Vaadin Community&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;This survey backs up&amp;nbsp;what we hear from&amp;nbsp;developers every day: &lt;span style="font-weight: bold;"&gt;Vaadin works best for Java teams building internal, data-heavy enterprise web apps, and those teams are very committed.&lt;/span&gt; The need for legacy modernization is growing, not shrinking. And the open-source core still is the starting point for most developers.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;It also tells us where to put our attention: more &lt;/span&gt;&lt;a href="https://vaadin.com/components"&gt;&lt;u&gt;&lt;span style="color: #0563c1;"&gt;UI components&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span style="color: #2c3e50;"&gt;, better documentation for each release, and clearer messaging about what Vaadin is and how it fits into modern Java development.&lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;If this is your first time using Vaadin, &lt;/span&gt;&lt;a href="https://start.vaadin.com"&gt;&lt;u&gt;&lt;span style="color: #0563c1;"&gt;start a project&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span style="color: #2c3e50;"&gt; and see how it works your team’s workflow. If you’re already building with Vaadin, thank you for being part of this survey — your feedback directly shapes what we build next.&lt;/span&gt;&lt;span style="color: #2c3e50;"&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2 style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;&lt;strong&gt;Want to add something?&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;We want to hear from you, even if you didn't take this year's survey. You can share your&amp;nbsp;thoughts in the &lt;a href="https://vaadin.com/forum/"&gt;Vaadin Forum&lt;/a&gt;, start a discussion on &lt;a href="https://github.com/vaadin"&gt;GitHub&lt;/a&gt;, or &lt;a href="https://pages.vaadin.com/contact"&gt;reach out to us&lt;/a&gt;. Let us know what's working, what's frustrating, and what you wish Vaadin did differently. &lt;/span&gt;&lt;/p&gt; 
&lt;p style="line-height: 1.5;"&gt;&lt;span style="color: #2c3e50;"&gt;Every piece of feedback helps us prioritize what is most important to the people who are actually building with the framework.&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fvaadin-2026-community-survey-what-java-developers-told-us&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>category/community</category>
      <pubDate>Thu, 09 Apr 2026 10:22:27 GMT</pubDate>
      <guid>https://blog.vaadin.com/vaadin-2026-community-survey-what-java-developers-told-us</guid>
      <dc:date>2026-04-09T10:22:27Z</dc:date>
      <dc:creator>Roman Kałkowski</dc:creator>
    </item>
    <item>
      <title>Vaadin 24 Free Maintenance Ends June 16 — Choose Your Path Forward</title>
      <link>https://blog.vaadin.com/vaadin-24-free-maintenance-ends-june-16-choose-your-path-forward</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://vaadin.com/blog/vaadin-24-free-maintenance-ends-june-16-choose-your-path-forward" title="" class="hs-featured-image-link"&gt; &lt;img src="https://blog.vaadin.com/hubfs/Extended%20maintenance%2024-1.png" alt="Vaadin 24 Extended Maintenance" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;a href="https://vaadin.com/blog/vaadin-25-1-release"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin 25.1 is now available&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; and makes for a strong, forward-looking upgrade. It is the first big release in the 25-series that makes it an attractive next step for teams building on the Vaadin platform.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;With the June 16, 2026 maintenance deadline for Vaadin 24 approaching, this is a good moment to explore your options and plan ahead. Upgrading to Vaadin 25.1 lets you take advantage of the latest enhancements and stay on a fully supported path. &lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This post covers what the maintenance transition means in practice, what makes Vaadin 25.1 a compelling upgrade target, and what to do if upgrading right now is not realistic for your team.&lt;/span&gt;&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;a href="https://vaadin.com/blog/vaadin-25-1-release"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin 25.1 is now available&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; and makes for a strong, forward-looking upgrade. It is the first big release in the 25-series that makes it an attractive next step for teams building on the Vaadin platform.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;With the June 16, 2026 maintenance deadline for Vaadin 24 approaching, this is a good moment to explore your options and plan ahead. Upgrading to Vaadin 25.1 lets you take advantage of the latest enhancements and stay on a fully supported path. &lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;This post covers what the maintenance transition means in practice, what makes Vaadin 25.1 a compelling upgrade target, and what to do if upgrading right now is not realistic for your team.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;What "Maintenance Mode" Means for Your V24 Application&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Vaadin maintenance helps you deliver long-lasting, reliable applications by keeping your software up to date with critical fixes and improvements. From security patches and browser compatibility updates to framework and integration enhancements, staying on a maintained version ensures your app remains secure, stable, and dependable over time.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;During the free maintenance window (until June 16, 2026), security patches and critical bug fixes are available for the latest Vaadin 24 minor version. If your application is pinned to an older minor version (for example, 24.3 or 24.5), that version is already past its free support window.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;After June 16, free maintenance ends. Applications on Vaadin 24 will continue to function normally, but security patches, resolutions for browser compatibility issues, and bug fixes will no longer be provided.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;If your team needs more time beyond June, our recommendation is to plan your upgrade now or get Extended Maintenance (available through the Enterprise plan) covers Vaadin 24 — including individual minor versions — with security patches and browser compatibility updates for up to 15 years. &lt;/span&gt;&lt;a href="https://vaadin.com/maintenance"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;More on that in the maintenance policy overview&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;. &lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;What's New in Vaadin 25.1&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Here are the key areas worth knowing about. &lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Simpler Theming and Styling&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Theming in Vaadin 25 is less framework configuration and more plain CSS. Themes are now just stylesheets, which means you can dynamically switch between them at runtime. This is useful for light/dark mode, per-user preferences, or multi-tenant branding.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Vaadin 25 also introduces &lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/styling/themes/aura/color"&gt;&lt;strong&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Aura&lt;/span&gt;&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt;&lt;span&gt;, a new modern theme built on stronger component base styles. The previous theme engine, Lumo, however also remains available. The Material theme has been removed — applications using it need to migrate to Lumo, Aura, or a custom approach built on the new base styles.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Leaner and Faster&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;A production-bundled Vaadin 25 application has roughly 30% fewer transitive dependencies by default.This is a 22 MB reduction compared to V24. Dev-mode server startup is over 50% faster, and deployment is about 27% quicker. The framework removed several external dependencies and leveraged APIs available in newer Java versions instead.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Simplified Build System&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;Production builds no longer depend on a dedicated Maven profile. Both Maven and Gradle now produce a production artifact by default, and development builds are used only when running inside an IDE. CI pipelines and buildpacks behave like a standard Java build.&lt;/span&gt;&lt;/p&gt; 
&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Updated Technology Baseline&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt; 
&lt;p&gt;&lt;span&gt;This is where the upgrade effort primarily sits for most teams:&lt;/span&gt;&lt;/p&gt; 
&lt;div&gt; 
 &lt;table style="border-style: none; border-collapse: collapse;"&gt;
  &lt;colgroup&gt;
   &lt;col width="104"&gt;
   &lt;col width="180"&gt;
   &lt;col width="168"&gt;
  &lt;/colgroup&gt; 
  &lt;tbody&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Requirement&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Vaadin 24&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Vaadin 25&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Java&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;17+&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;21+&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Spring Boot&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;3.x&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;4&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Spring Framework&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;6&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;7&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Jakarta EE&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;10&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;11&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Servlet&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;6.0&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;6.1&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Node.js&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;20+&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;24+&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style="height: 34.3333px;"&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="margin-top: 0px; margin-bottom: 0px;"&gt;&lt;strong&gt;&lt;span&gt;Maven &lt;/span&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;3.5+&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
    &lt;td style="vertical-align: top;"&gt; &lt;p style="text-align: center; margin-top: 0px; margin-bottom: 0px;"&gt;&lt;span&gt;3.8+&lt;/span&gt;&lt;/p&gt; &lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt; 
&lt;p&gt;&lt;a href="https://vaadin.com/docs/next/upgrading/version-comparison"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;View full Vaadin’s version comparison table&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Vaadin 25 also upgrades from Jackson 2 to Jackson 3, which may affect applications with custom serialization logic. And Hilla is now an opt-in dependency — if your project uses Hilla or React-based views, add &lt;/span&gt;&lt;span style="color: #188038;"&gt;hilla-spring-boot-starter&lt;/span&gt;&lt;span&gt; alongside &lt;/span&gt;&lt;span style="color: #188038;"&gt;vaadin-spring-boot-starter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;If your project is already on Java 21 and can move to Spring Boot 4, the baseline shift is straightforward. If you are locked to Java 17 or Spring Boot 3, assess those dependencies first: they define the scope of your upgrade.&lt;/span&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Vaadin 24 vs. Vaadin 25: Choosing the Right Timing&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;For most teams, Vaadin 25.1 is the recommended upgrade target — it is stable, feature-rich, and the natural adoption point for anyone who was waiting for the platform to mature past the initial 25.0 release. But upgrading is not the only valid path forward.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;That said, the right timing depends on your situation:&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Upgrade now if your project is on Java 21 and Spring Boot 3.x &lt;/span&gt;&lt;/strong&gt;&lt;span&gt;(or ready to move to Spring Boot 4), you are not using the Material theme, and your third-party dependencies are compatible with the new baselines. We recommend first addressing any deprecation warnings on the latest Vaadin 24, then updating the Vaadin version in pom.xml. Any remaining deprecation warnings after upgrading are not urgent — they will most likely not require immediate attention before Vaadin 26. &lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;strong&gt;&lt;span&gt;Consider Extended Maintenance if you cannot realistically upgrade before June 16, 2026 &lt;/span&gt;&lt;/strong&gt;&lt;span&gt;— Extended Maintenance is not a consolation prize. It is a legitimate path that provides up to 15 years of security patches, browser compatibility updates, and critical bug fixes for your specific V24 version. It keeps your V24 application secure and supported while you plan your upgrade on your own timeline. It covers both V24 major and individual minor versions (24.0 through 24.9), so even teams pinned to an older minor release are fully supported. Extended Maintenance is included in the Enterprise plan and available as a standalone service. Learn more at &lt;/span&gt;&lt;a href="http://vaadin.com/maintenance"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;vaadin.com/maintenance.&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/p&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Before You Start: A Practical Checklist&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;Here is a quick checklist to help scope the effort before you commit to a sprint:&lt;/span&gt;&lt;/p&gt; 
&lt;ol&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Read the upgrade guide.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The&lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/upgrading"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;official V24 → V25 upgrade instructions&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; walk through the specific steps and breaking changes.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Start a test branch.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The fastest way to estimate effort is to bump the version and see what breaks. Many teams find the upgrade smoother than expected.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Check your Java version.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Vaadin 25 requires Java 21. If you are on Java 17, this is likely your first upgrade step.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Check your Spring Boot version.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Vaadin 25 requires Spring Boot 4 / Spring Framework 7. Review your Spring dependencies and any libraries that may be pinned to Spring Boot 3.x.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Review your security configuration.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Spring Security 7 introduces breaking changes, and the deprecated &lt;/span&gt;&lt;span style="color: #188038;"&gt;VaadinWebSecurity&lt;/span&gt;&lt;span&gt; patterns from V24 have been removed. If your application has custom security configuration, review this early. The new patterns are supported in Spring Security 6 and the latest Vaadin 24 versions, so it is recommended to make this change as a separate step even before the main upgrade.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Audit your theme.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; If you are using the Material theme, plan for a styling migration. If you are on Lumo, the transition is smoother — but review any custom theme configuration for the new stylesheet-based approach.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Review third-party add-ons.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Check that any Vaadin add-ons or third-party components you use have published V25-compatible versions. The&lt;/span&gt;&lt;a href="https://vaadin.com/directory"&gt;&lt;span style="white-space-collapse: preserve;"&gt; &lt;/span&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin Directory&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; is a good place to verify.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Check your Node.js version.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The frontend build now requires Node.js 24+. Note that if a compatible version is not found globally, &lt;/span&gt;&lt;a href="https://vaadin.com/docs/latest/flow/configuration/development-mode/node-js"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin will automatically download and install it&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; for you into your ~/.vaadin/ directory.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Update your test setup.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Spring Boot 4 defaults to JUnit 6 (JUnit Platform). If you have JUnit 4 tests using &lt;/span&gt;&lt;span style="color: #188038;"&gt;UIUnit4Test&lt;/span&gt;&lt;span&gt; or &lt;/span&gt;&lt;span style="color: #188038;"&gt;TestBenchTestCase&lt;/span&gt;&lt;span&gt;, they will not be detected without adding the JUnit Vintage Engine dependency. Also note that &lt;/span&gt;&lt;span style="color: #188038;"&gt;ComponentTester.click()&lt;/span&gt;&lt;span&gt; in UI Unit has a new signature — tests using &lt;/span&gt;&lt;span style="color: #188038;"&gt;AnchorTester&lt;/span&gt;&lt;span&gt; or &lt;/span&gt;&lt;span style="color: #188038;"&gt;RouterLinkTester&lt;/span&gt;&lt;span&gt; need to change to the &lt;/span&gt;&lt;span style="color: #188038;"&gt;navigate()&lt;/span&gt;&lt;span&gt; method.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Check Jackson usage.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; Vaadin handles the Jackson 2 to Jackson 3 migration automatically in most cases. If your application has custom serializers, deserializers, or Jackson annotations, &lt;/span&gt;&lt;a href="https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;review the Jackson 3 migration guide&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Migrate Elemental JSON usage.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; If you have custom components that use the Elemental JSON library for browser communication, these need to be changed to use beans, lists, maps, and other standard Java collections instead.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;strong&gt;&lt;span&gt;Re-add dev tools explicitly.&lt;/span&gt;&lt;/strong&gt;&lt;span&gt; The &lt;/span&gt;&lt;span style="color: #188038;"&gt;vaadin-dev&lt;/span&gt;&lt;span&gt; module is no longer included as a transitive dependency. Always include &lt;/span&gt;&lt;span style="color: #188038;"&gt;vaadin-dev&lt;/span&gt;&lt;span&gt; as an optional dependency so that it is used in development but not included in production builds.&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;/span&gt;&lt;span style="white-space-collapse: preserve;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
&lt;/ol&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Resources&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/vaadin-25-0-release"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin 25.0 release post&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — detailed walkthrough of all changes&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/docs/latest/upgrading"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Upgrade guide&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — step-by-step upgrade instructions&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/docs/next/upgrading/version-comparison"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Versions of Vaadin comparison table&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — side-by-side technical comparison across Vaadin versions&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/maintenance"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Extended maintenance policy&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — long-term security and stability for V24&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/faster-and-slimmer-vaadin-25"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Faster and Slimmer Vaadin 25&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — performance benchmarks and dependency analysis&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://svenruppert.com/2026/02/16/an-unexpectedly-hassle-free-upgrade/"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;An Unexpectedly Hassle-Free Upgrade&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; — a community developer's real-world V25 upgrade experience&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/blog/vaadin-25-1-release"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Vaadin 25.1 release post — free Copilot, production-ready Signals, and more&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;h2&gt;&lt;strong&gt;&lt;span&gt;Need Help Planning Your Upgrade?&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt; 
&lt;p&gt;&lt;span&gt;If your team wants a second opinion on your path forward — whether you are upgrading to Vaadin 25.1 or evaluating Extended Maintenance — we are happy to help. &lt;/span&gt;&lt;a href="https://vaadin.com/maintenance#contact-us-form"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Book a 30-minute consultation&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; to review your setup and get a realistic assessment of both options.&lt;/span&gt;&lt;/p&gt; 
&lt;p&gt;&lt;span&gt;Vaadin 24 has served the community well, and Vaadin 25.1 builds on that foundation with a cleaner, faster, more standards-aligned platform. You have two equally valid paths: &lt;/span&gt;&lt;/p&gt; 
&lt;ul&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/docs/latest/upgrading"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Upgrade to Vaadin 25.1 now,&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt; while free maintenance is still active and the transition is straightforward. &lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;a href="https://vaadin.com/maintenance"&gt;&lt;u&gt;&lt;span style="color: #1155cc;"&gt;Secure Extended Maintenance &lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;span&gt;and upgrade when your team is ready. &lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt; 
&lt;p&gt;&lt;span&gt;The June 16 deadline is approaching, but whichever path you choose, the goal is the same: no surprises, no scrambling, just a clear path forward.&lt;/span&gt;&lt;/p&gt;  
&lt;img src="https://track.hubspot.com/__ptq.gif?a=1840687&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fblog.vaadin.com%2Fvaadin-24-free-maintenance-ends-june-16-choose-your-path-forward&amp;amp;bu=https%253A%252F%252Fblog.vaadin.com&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <category>extended maintenance</category>
      <pubDate>Tue, 07 Apr 2026 13:37:45 GMT</pubDate>
      <guid>https://blog.vaadin.com/vaadin-24-free-maintenance-ends-june-16-choose-your-path-forward</guid>
      <dc:date>2026-04-07T13:37:45Z</dc:date>
      <dc:creator>Roman Kałkowski</dc:creator>
    </item>
  </channel>
</rss>
