Category Archives: Serverside

Practical i18n in Vaadin: Resource Bundles, Locale Handling and UI Language Switching

Modern web applications are rarely used only by users with the same language. Even internal tools often reach international teams or are used in different countries. A multilingual user interface is therefore not a luxury feature, but an important part of the user experience.

The open-source project URL-Shortener also benefits from a clear internationalisation strategy. The application is intended for developers, administrators, and other users who want to manage, analyse, or distribute links. To ensure that the interface remains understandable regardless of the user’s origin, the application supports multiple languages.

Screenshot of a URL shortener interface displaying a list of shortened URLs, their codes, original links, creation dates, and status indicators.

The project can be found on GitHub at the URL: https://3g3.eu/url

This article shows how the URL shortener UI has been enhanced with simple, robust internationalisation. The implementation is based entirely on Vaadin Flow’s capabilities and deliberately uses only a few additional helper classes to keep the architecture clear.

The solution has several key objectives. On the one hand, the users’ language is to be automatically recognised based on the locale transmitted by the browser. In addition, it must be possible to manage multiple translations via resource bundles without creating additional administrative overhead in the application code. At the same time, the user should be able to change the language at any time during a running session. Another important goal is to keep the implementation lean and deliberate and to dispense with additional frameworks so that internationalisation can be implemented directly within the existing application without complex infrastructure.

Currently, the application supports three languages:

  • English
  • German
  • Finnish

The choice of these languages is deliberately pragmatic. English serves as the standard language, while German and Finnish depict real-life usage scenarios from the project’s environment.

The focus of this article is not only on the technical implementation but also on how internationalisation can be integrated into a Java web application in a way that keeps the source code clearly structured and maintainable. To do this, we examine both the organisation of translation resources and the handling of locales within the application.

In the following course, it is explained step by step how Vaadin recognises and loads translation resources, how translation keys can be structured sensibly, and how the user’s language is determined based on the browser locale. In addition, it shows how simple language switching can be integrated directly into the user interface, allowing users to switch the desired language during a running session.

This creates an internationalisation solution that is deliberately kept simple but can be easily expanded if other languages are added later.

Internationalisation in Vaadin

Vaadin Flow already provides basic support for internationalisation. Applications can provide translations via so-called resource bundles, which are automatically recognised and loaded by the runtime environment. In this way, user interfaces can be operated in several languages without much additional effort.

The central mechanism relies on translation files stored in a special directory within the project. By default, Vaadin searches for translation resources in the vaadin-i18n folder within the classpath. All files stored there are automatically recognised and are available to the application at runtime.

The translation files follow the familiar structure of Java Resource Bundles. A base file contains the standard translations, while additional files provide language-specific variants. The language is defined by a language suffix in the file name.

A typical setup looks like this, for example:

vaadin-i18n/

  translations.properties

  translations_de.properties

  translations_fi.properties

The file without a language suffix serves as the default resource. In many projects, it contains the English texts. As soon as Vaadin determines an active locale, the framework automatically attempts to load the appropriate translation file. If a language-specific variant exists, it is used. Otherwise, Vaadin falls back on the default resource.

This behaviour corresponds to the classic fallback mechanism of Java Resource Bundles. This means that the application remains functional even if individual translations are not yet complete.

For the user interface, this means that text is no longer defined directly in the source code but is referenced via unique translation keys. These keys are then resolved at runtime via the active locale.

Vaadin provides the getTranslation() method for this purpose, which can be used directly in UI components. This method is used to pass a translation key to the framework, after which Vaadin determines the appropriate text from the loaded resources.

Thus, the resource bundle system forms the basis for all other internationalisation mechanisms within the application. On this basis, additional concepts such as language switching, session-based locale management or structured translation keys can then be developed.

Structuring Translation Resources

After looking at Vaadin’s basic internationalisation support in the previous chapter, an important question quickly arises in practice: How should translation resources be organised within a project to remain maintainable in the long term?

In the URL shortener project, all translations are stored in so-called resource bundles. These are located in the directory src/main/resources/vaadin-i18n/. Vaadin automatically detects this folder and loads the translation files it contains at runtime.

The structure of the files follows the conventions of Java Resource Bundles. A base file contains the standard translations, while additional files provide language-specific variants.

src/main/resources/vaadin-i18n/

  translations.properties

  translations_de.properties

  translations_fi.properties

The translations.properties file serves as the default resource. This project contains the English texts. For other languages, additional files with a language suffix are created. The language is defined by the ISO language code in the file name.

For example, the German version contains the file translations_de.properties, while the Finnish version contains translations_fi.properties.

Within these files, the actual translations are defined via key-value pairs. Each key corresponds to a specific text in the user interface.

main.appTitle=URL Shortener

main.logout=Logout

nav.overview=Overview

nav.create=Create

nav.youtube=Youtube

nav.about=About

The keys themselves follow a simple naming convention. They are often built according to the pattern bereich.element. This allows texts that belong together to be logically grouped.

This structure makes it easier to keep track of a large number of translations. At the same time, it prevents name conflicts between different areas of the application.

Another advantage of this structure is that translations remain completely separate from the application code. The source code only references the keys, while the actual texts are defined in the resource bundles.

This allows translations to be expanded or corrected later without changes to the application code.

This clear separation between code and translation resources provides an important basis for further internationalisation of the application. The next step will therefore look at how these translation keys can be used comfortably within the user interface.

Simplified access to translations with I18nSupport

Once the structure of the translation resources has been determined, another question arises in practical development: How can these translations be used as comfortably as possible within the user interface?

Vaadin basically provides the getTranslation() method, which can be used to resolve translation keys at runtime. This method is available, for example, within UI components and allows you to use a key to determine the appropriate text from the loaded resource bundles.

In practice, however, direct use of this method often results in recurring boilerplate code. Especially in larger user interfaces, translations have to be resolved in many different places. This quickly results in redundant calls and less readable code.

To make this access easier, the URL shortener project uses a small helper interface called I18nSupport. This interface encapsulates access to the translation function and provides a compact method for querying translations within the UI components.

A simplified example of this interface looks like this:

public interface I18nSupport {
  default String tr(String key, String fallback) {
    return UI.getCurrent().getTranslation(key);
  }
}

This auxiliary method allows translations to be used much more compactly within the application. Instead of working directly with the Vaadin API, a single method call suffices.

An example from the application’s MainLayout illustrates this:

H1 appTitle = new H1(tr(“main.appTitle”, “URL Shortener”));

This keeps the source code clear and the actual text completely separate from the user interface implementation.

Another advantage of this solution is that access to translations can be adjusted centrally. If the implementation changes later – for example, due to additional fallback mechanisms or logging – this can be done within the helper interface without having to adapt all UI components.

I18nSupport thus forms a small but effective abstraction layer on top of the Vaadin i18n API. It ensures that translations can be used consistently throughout the application with as little boilerplate code as possible.

Determining the user locale

Once translation resources have been structured and convenient access has been provided via I18nSupport, the next step is to ask a central question: What language should the application use when it is first launched?

In web applications, this decision is usually made based on the browser’s language preferences. Modern browsers send a so-called Accept-Language header with every HTTP request that contains a prioritised list of preferred languages. Based on this information, the application can decide which locale to use initially.

Vaadin Flow already takes this information into account during user interface initialisation. When building a new UI, the browser’s locale is automatically detected and set as the current locale. This allows an application to select the appropriate language when the interface is first rendered.

In practice, however, it rarely makes sense to support every language reported by the browser fully. Many applications only offer a limited number of translations. Therefore, the browser-reported locale must first be compared with the application’s actually supported languages.

In the URL shortener project, this matching is implemented via a small helper class called LocaleSelection. This class defines the application’s supported languages and provides methods to map a requested locale to an available variant.

A simplified example looks like this:

public static Locale match(Locale requested) {
  if (requested == null) {
    return EN;
  }
  String language = requested.getLanguage();
  return SUPPORTED.stream()
      .filter(l -> l.getLanguage().equals(language))
      .findFirst()
      .orElse(EN);
}

The method first checks which language the browser requested. It then checks whether this language is among the application’s supported languages. If this is the case, the corresponding locale is adopted. Otherwise, the application’s default language is used.

This strategy ensures that the application remains consistent even if a browser reports a language for which there are no complete translations. At the same time, the implementation remains clear and easily expandable.

The next step is how to keep the chosen language stable during a session. Therefore, in the following chapter, we will look at how a user’s locale is stored and managed within the Vaadin session.

Session-based voice management

After describing in the previous chapter how the initial language of an application can be determined based on the browser locale, another important question arises in practice: How does this decision remain stable throughout the application’s use?

In a typical web application, a user session consists of multiple HTTP requests. During this time, the user expects the interface to be consistently displayed in the same language. If the language were to be determined exclusively by the browser locale for each request, this could lead to unexpected changes – especially if a user manually switches languages within the application.

For this reason, the URL shortener project stores the selected language within the Vaadin session. The session represents a server-side context that is retained across multiple requests and is therefore well-suited to storing user-specific settings, such as the current locale.

Vaadin itself already offers basic support for this. Each VaadinSession has an associated locale that can be set and queried by the application. If this locale is changed, it directly affects the resolution of translation keys in the user interface.

In the project, the session locale is also managed using the LocaleSelection helper class. In addition to the previously shown method for selecting a supported language, this class also provides methods to store or read the locale in the session.

A simplified example for storing the locale looks like this:

public static void setToSession(VaadinSession session, Locale locale) {
  session.setAttribute(SESSION_KEY, locale);
}

When changing the language, the selected locale is saved in the session and set directly on the current UI. This ensures that the new language becomes active immediately and is retained for all subsequent interactions within the same session.

This approach has several advantages. For one, a user’s language remains consistent throughout the session. On the other hand, the language can be changed at any time by a user action without the need for additional persistence mechanisms such as cookies or database entries.

In addition, the implementation remains deliberately simple. The application uses only the existing Vaadin mechanisms and adds only a small helper class for central management of the supported locales.

The next chapter shows how this session-based locale is eventually linked to the user interface and how users can switch languages directly within the application.

Language switching in the user interface

After describing in the previous chapter how to store the selected language in the Vaadin session, the practical question now arises: how can users change this language in the first place? Internationalisation is only useful if users can actively switch the interface language.

In the URL shortener project, language switching has been integrated directly into the MainLayout. This layout forms the central structure of the user interface and includes, among other things, the navigation bar and various status and control elements. This makes it ideal for placing a compact language switch.

Instead of a classic drop-down menu, the application uses a small group of buttons, each representing a language. The buttons are displayed as flags, making the desired language recognisable at a glance.

The user interface shows three options:

  • German
  • English
  • Finnish
User interface showing language options with German, English, and Finnish flags, the name 'EclipseStore,' and a logout button.

Each of these buttons changes the current locale when clicked. Technically, the first step is to check which language has been chosen. This language is then set in both the current UI and the Vaadin session.

The switch’s specific design is deliberately kept small and does not require any additional components or dependencies. Instead of a ComboBox, a horizontal button group is rendered, which acts like a “segmented control”: three round buttons in a slightly separated bar. The bar itself is merely a horizontal layout, with styling handled via Lumo CSS variables, so the look blends harmoniously with the rest of the theme.

When the switch is created, the currently effective locale is first determined. It is crucial that not only the browser locale is considered, but that a value already stored in the session takes precedence. This is exactly what LocaleSelection.resolveAndStore(…) is used for. The result is the “current” locale, which is used to mark the active button.

private Component createLanguageSwitch() {
  Locale current = LocaleSelection.resolveAndStore(
      VaadinSession.getCurrent(),
      UI.getCurrent().getLocale()
  );
  Button de = flagButton(LocaleSelection.DE, current);
  Button en = flagButton(LocaleSelection.EN, current);
  Button fi = flagButton(LocaleSelection.FI, current);
  HorizontalLayout bar = new HorizontalLayout(de, en, fi);
  bar.setSpacing(false);
  bar.getStyle()
      .set("gap", "6px")
      .set("padding", "2px")
      .set("border-radius", "999px")
      .set("background", "var(--lumo-contrast-5pct)");
  return bar;
}

The actual logic lies in creating a single button. Each button only gets a chip with an emoji flag as content. This seems trivial, but it’s practical: it requires no assets, no additional files, and no build steps. Styling and interaction take place directly on the button.

Three points are important here: First, the buttons are shaped into a compact, round form so that they are perceived as a group that belongs together. Second, the active state is calculated based on the language (getLanguage()), because regional variants such as de-DE or de-AT should not affect the UI. Third, a click triggers the actual locale switch.

private Button flagButton(Locale locale, Locale current) {
  String flag = flagEmoji(locale);
  Button b = new Button(new Span(flag));
  b.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
  b.getStyle()
      .set("width", "38px")
      .set("height", "32px")
      .set("min-width", "38px")
      .set("border-radius", "999px")
      .set("padding", "0")
      .set("line-height", "1")
      .set("font-size", "18px");
  boolean active = locale.getLanguage().equalsIgnoreCase(current.getLanguage());
  applyActiveStyle(b, active);
  b.addClickListener(e -> switchLocale(locale));
  Tooltip (optional)
  b.getElement().setProperty("title", locale.getLanguage().toUpperCase());
  return b;
}

For the active state, no additional theme variant is deliberately introduced; instead, a minimal style is applied: a slight background tint and an outline ring in the primary colour. Thus, the component remains visually restrained but unambiguous.

private void applyActiveStyle(Button b, boolean active) {
  if (active) {
    b.getStyle()
        .set("background", "var(--lumo-primary-color-10pct)")
        .set("outline", "2px solid var(--lumo-primary-color-50pct)");
  } else {
    b.getStyle()
        .remove("background")
        .remove("outline");
  }
}

The locale change itself is deliberately implemented “robustly”. First, the selected locale is matched to a supported language. It is then set in both the session and the UI. The final reload() ensures that all views, dialogues, and components resolve their texts from the resource bundles again. This avoids the additional complexity that would otherwise be caused by LocaleChangeObserver implementations across many components.

private void switchLocale(Locale selected) {
  UI ui = UI.getCurrent();
  VaadinSession session = VaadinSession.getCurrent();
  Locale effective = LocaleSelection.match(selected);
  LocaleSelection.setToSession(session, effective);
  session.setLocale(effective);
  ui.setLocale(effective);
  ui.getPage().reload();
}

Finally, the display of the flags is encapsulated via a small mapping method. This keeps the display in a central place and can be easily adapted later – for example, if US is to be used instead of EN or if other languages are added.

<IMG DE>  – this is a replacement of the emoji —

private string flagEmoji(Locale locale) {
  return switch (locale.getLanguage()) {
    case "de" -> "<IMG DE>";
    case "fi" -> "<IMG FI>";
    default -> "<IMG EN>"; EN
  };
}

The process consists of several steps. First, the desired language is compared with the application’s supported languages. Then the locale is set in both the session and the current UI. As a result, the new language is immediately available.

The final page reload ensures that all UI components are re-rendered and that all translations are consistently resolved with the new locale. This approach is deliberately kept simple and avoids additional complexity within the component logic.

By integrating language switching into the MainLayout, this feature is available in every view of the application. Users can switch languages at any time without leaving the current page.

This means that the internationalisation of the user interface is fully integrated into the application. Translation resources, locale discovery, session management, and language switching are now intertwined, enabling a multilingual interface with comparatively low implementation effort.

Conclusion and outlook

With internationalisation, the URL shortener has reached a point where good user-friendliness and clean architecture converge. The application supports multiple languages without the source code becoming confusing or requiring additional dependencies. Instead, the existing Vaadin Flow mechanisms are relied on, and only added where they make sense for the project.

The essential building blocks mesh neatly. Translations are stored as resource bundles in the vaadin-i18n directory so that Vaadin recognises them automatically. Clearly structured keys are used to extract texts from the interface and manage them centrally. I18nSupport reduces access to translations to a compact, anywhere-to-use method, keeping UI classes readable.

The locale determination follows a pragmatic principle. By default, the browser’s locale is used, but limited to the languages that are actually supported. This keeps the system predictable and avoids incomplete translation states. At the same time, the selected language is stored on a per-session basis, so that users retain a consistent interface during a session and their selection does not have to be “renegotiated” with each request.

Finally, the whole thing becomes particularly visible in the user interface. The language switch in the MainLayout is deliberately minimalist, but on the UX side, it is much more pleasant than a nested menu. Three flag buttons are enough to quickly change the language, and the active state is immediately recognisable. The technical change to the locale is implemented robustly across the session and UI. The final reload is not a workaround, but a conscious design decision: It ensures that all views, dialogues, and components are consistently rendered in the new language without each component having to implement additional observer logic.

This creates a solid foundation that can be expanded if necessary. An obvious next step would be to add more languages or refine translations without changing the code. For more complex language cases – such as pluralisation, number and date formatting, or language-dependent sentence modules – ICU-based formatting could be added later. It would also be possible to save the language selection not only on a session basis, but also to link it to a user profile permanently.

For the current state of the URL shortener, however, the solution is deliberately kept to the essentials: It is small enough to remain maintainable, but complete enough to deliver real benefits in everyday life. This is precisely the practical value of good internationalisation – it is hardly noticeable in the code but immediately noticeable to users.

Separation of Concerns in Vaadin: Eliminating Inline Styles

Vaadin Flow enables the development of complete web applications exclusively in Java. Components, layouts, navigation, and even complex UI structures can be modelled on the server side without working directly with HTML or JavaScript. This approach is one of the main reasons why Vaadin is especially popular in Java-centric projects.

However, as a project grows in size, a typical problem arises: styling is often done directly in Java code. Vaadin allows this conveniently via the getStyle().set(…) method. This allows you to adjust spacing, colours, or layout properties quickly. In small prototypes, this procedure initially seems harmless and even practical.

In larger applications, however, this practice quickly leads to a code state that is difficult to maintain. Styling information is spread over numerous views and components. Changes to the design have to be made throughout the Java code. Reusability of styles becomes virtually impossible, and the distinction between UI logic and presentation layer becomes increasingly blurred.

A typical example in many Vaadin projects looks something like this:

component.getStyle()
    .set("padding", "var(--lumo-space-m)")
    .set("border-radius", "var(--lumo-border-radius-l)");

Such constructs can often be found in almost every view. Over time, a mixture of layout logic, component structure and CSS definitions emerges directly in Java code.

In the open-source project URL-Shortener, exactly this problem became apparent. Several views contained inline styling, which was originally intended for quick UI adjustments. However, as functionality grew, it became clear that this pattern is not sustainable in the long term.

The goal of the following refactoring was therefore clearly defined:

  • Remove inline styling from views
  • Introduction of clear CSS classes
  • Clean separation between structure (Java) and presentation (CSS)
  • Better maintainability and reusability of the UI

The article describes step by step how an existing Vaadin UI can be converted from inline styling to a structured CSS architecture. The examples come directly from the real application and show typical situations that occur in many Vaadin projects.

This is not about making Vaadin completely CSS-driven. Vaadin remains a server-side UI framework. Rather, it is about establishing a clear division of responsibilities: Java defines the structure of the user interface, while CSS handles its visual design.

This seemingly small change has a significant impact on the maintainability of an application – especially if a Vaadin application is developed over the years.

The problem with inline styling in Vaadin Flow

At first glance, the getStyle().set(…) method in Vaadin Flow seems extremely practical. With just a few lines, spacing can be adjusted, layout problems corrected, or colours changed – directly in the Java code of the respective view. Especially in early project phases or during prototype development, this approach seems quick and uncomplicated.

In real applications, however, a different development often emerges. As a project grows in size, inline styles spread across multiple views. What were initially only small adjustments gradually became a large number of local layout and styling corrections that are implemented directly in the Java code.

For example, a typical snippet from a Vaadin application might look like this:

component.getStyle().set("padding", "var(--lumo-space-m)");
layout.getStyle().set("gap", "var(--lumo-space-l)");
button.getStyle().set("margin-left", "auto");

Each of these lines is harmless in itself. Taken together, however, they lead to several structural problems.

Firstly, the styling is spread over the entire application. Instead of a central place where the visual appearance is defined, design decisions are spread across many different Java classes. If, for example, the spacing of a layout or the display of certain components is to change later, developers must first find the appropriate places in the code.

Secondly, reusability is made more difficult. Many layout patterns – such as map layouts, typical spacing, or alignments – appear multiple times in an application. However, if these are defined as inline styles, they exist again in each view. Changes then have to be made in several places.

Third, inline styling blurs the architectural separation between structure and representation. Java classes actually define the structure of the user interface, orchestrate components, and respond to user interactions. When CSS properties are integrated directly into these classes, presentation logic mixes with the UI’s structural definition.

This problem is particularly evident in larger Vaadin applications developed over a longer period. Small visual adjustments accumulate, and individual getStyle().set(…) calls, an implicit styling strategy gradually emerges – spread across numerous classes.

In the open-source project URL-Shortener, exactly this pattern emerged. In several views, including MainLayout, AboutView and CreateView, layout and styling adjustments were made directly in Java code. Although the application remained functionally correct, the UI’s visual behaviour became increasingly difficult to understand.

The refactoring was therefore not just aimed at removing individual style calls. Instead, a clear rule should be introduced:

Java defines the structure of the user interface – CSS defines its visual design.

At first, this separation seems like a small organisational change. In practice, however, it significantly improves the application’s maintainability. Styles can be defined centrally, reused, and used consistently across multiple views.

The following sections show step by step how this separation can be implemented in an existing Vaadin Flow application.

Separation of structure and presentation: Java vs CSS

Now that the problems of inline styling in Vaadin Flow have become clear, the central question arises: How can a clean separation be created between the structure of the user interface and its visual representation?

In classic web applications, this division is clearly defined. HTML describes the structure of the page, CSS controls the visual appearance, and JavaScript handles interactions and logic. In Vaadin Flow, this architecture shifts slightly because the UI structure is defined entirely in Java. Nevertheless, the basic idea remains: structure and presentation should be kept separate.

Structure belongs in Java.

Java defines the structure of the user interface in a Vaadin application. These include, in particular:

the composition of components

  • Layout Structures
  • Navigation and routing
  • Event Handling
  • Data Binding

A typical example is the structure of a layout:

VerticalLayout container = new VerticalLayout();
container.add(new H2("Create new short links"), form, actions);

Here, the code describes only the structure of the interface: which components are present and how they are arranged.

Appearance belongs in CSS.

The visual representation of the components, on the other hand, should be defined via CSS. These include, but are not limited to:

  • Spacing and padding
  • Colours and backgrounds
  • Shadows and Borders
  • Typography
  • responsive layout adjustments

For example, instead of setting padding directly in Java code

component.getStyle().set(“padding”, “var(–lumo-space-m)”);

A CSS class is used:

component.addClassName(“card”);

The actual visual definition is then in the stylesheet:

.card {
  padding: var(--lumo-space-m);
  border-radius: var(--lumo-border-radius-l);
}

This division ensures that layout decisions can be managed centrally rather than spread across several views.

Advantages of this separation

The consistent separation between Java structure and CSS presentation has several advantages.

First, the Java code will be much more readable. Views focus on the composition of the UI components without being overloaded by styling details.

Second, styles can be centrally managed and reused. Once a CSS class has been defined, it can be used across different views without layout rules repeating in the code.

Third, the application’s maintainability is improved. Changes to the design do not require adjustments in numerous Java classes, but can be made centrally in the stylesheet.

Especially in larger Vaadin projects with several developers, it quickly becomes apparent how important this clear division of responsibility is.

In the next section, we will use concrete examples from the application to show how existing inline styles can be replaced by CSS classes step by step.

Replace inline styles with CSS classes

Once the basic separation between structure (Java) and presentation (CSS) has been defined, a practical question arises in existing Vaadin applications: How can an existing codebase be gradually converted from inline styling to CSS classes?

In practice, the changeover usually takes place incrementally. Instead of refactoring the entire application at once, individual views or components are cleaned up first. Direct style definitions in the Java code are removed and replaced by CSS classes.

Case in point

A common pattern in Vaadin views is to control layout behaviour directly via inline styles. An example from the application is moving a component to the right end of a layout.

For example, before refactoring, the code looked like this:

storeIndicator.getStyle().set(“margin-left”, “auto”);

The code serves its purpose: the component is moved to the right. The problem, however, is that this layout rule is now firmly anchored in Java code.

Step 1: Introduce a CSS class

Instead of setting the Style property directly, a suitable CSS class is first defined. This is typically located in the application’s global stylesheet file.

.mainlayout-right {
  margin-left: auto;
}

The advantage of this solution is that the layout rule is now centrally defined and can be reused by several components.

Step 2: Using the class in Java code

The next step is to remove the inline style in the Java code and replace it with the CSS class.

storeIndicator.addClassName(“mainlayout-right”);

This means that the Java code remains solely responsible for the UI’s structure. The component’s visual alignment is controlled entirely by CSS.

Another example from a view

Larger layout definitions can also be cleaned up in this way. In many Vaadin views, for example, constructs such as:

container.getStyle()
        .set("border-radius", "var(--lumo-border-radius-l)")
        .set("gap", "var(--lumo-space-l)");

These style definitions can be converted into a CSS class:

.card-container {
  border-radius: var(--lumo-border-radius-l);
  gap: var(--lumo-space-l);
}

In Java code, the definition is then reduced to:

container.addClassName(“card-container”);

Benefits of this approach

This refactoring results in several improvements.

First, the Java code will be much more compact and easier to read. Layout details disappear from the View classes.

Second, styles can be managed centrally. Layout changes only need to be made at one point in the stylesheet.

Third, it creates a consistent visual language within the application by leveraging reusable CSS classes.

The migration of inline styles to CSS classes is therefore a central step in keeping Vaadin applications maintainable in the long term.

The next chapter shows where CSS files are stored in a Vaadin Flow application and how they are correctly integrated.

Structure of CSS Files in a Vaadin Flow Application

Now that inline styles have been removed from the Java code and replaced with CSS classes, the next important question arises: Where should these CSS definitions be stored in a Vaadin flow application?

Vaadin Flow is internally based on Web Components and uses the application’s frontend directory to provide static resources such as stylesheets, JavaScript, or images. For CSS, this results in a clear structure that has proven itself in practice.

The frontend directory

In a typical Vaadin application, there is a directory called frontend. This contains all resources that are loaded directly by the browser.

For example, a commonly used structure looks like this:

frontend/

  styles/

    main.css

    layout.css

    components.css

Within this folder, the styles can be logically organised by area of responsibility.

  • main.css contains basic styles of the application
  • layout.css defines layout rules
  • components.css includes reusable component styles

This splitting prevents all styles from ending up in a single file and facilitates long-term maintenance.

Integration of styles in Vaadin

For the CSS files to take effect in the application, they must be loaded by Vaadin. This is typically done via annotations such as @CssImport.

An example in a central layout class might look like this:

@CssImport("./styles/main.css")
public class MainLayout extends AppLayout {
    ...
}

Once this annotation is in place, the styles defined in it become available throughout the application.

Alternatively, you can use a global theme, in which styles are loaded automatically. In this case, the CSS files are located in the directory, for example:

frontend/themes/<theme-name>/

This approach is particularly recommended in larger applications, as the theme serves as a central point for all design decisions.

Separation of layout and component styles

Another best practice is to distinguish between layout styles and component styles.

Layout styles describe, for example:

  • Container spacing
  • Grid or Flex Layouts
  • responsive behavior

Component styles, on the other hand, define the visual appearance of reusable UI elements such as cards, badges, or toolbars.

This separation prevents layout rules and visual component logic from being mixed.

Consistent naming of CSS classes

To ensure that CSS classes remain understandable in the long term, a consistent naming convention should be used.

One possible pattern is based on the respective view or component:

.about-card

.about-hero

.mainlayout-right

.searchbar-container

This makes it immediately recognisable which area of the application a certain style belongs to.

A clearly structured CSS organisation is the basis for ensuring that the previously introduced separation between Java and CSS is permanent.

While Java continues to define the structure and behaviour of the user interface, the application’s visual appearance is centrally controlled via stylesheets. This reduces redundancies, facilitates design changes, and ensures a consistent user interface across all views.

The next chapter shows how concrete refactorings in the application were implemented and which patterns proved particularly helpful.

Refactoring Real Views: Examples from the URL Shortener

The principles described so far – the separation of structure and presentation, as well as the replacement of inline styles with CSS classes – can best be understood through concrete examples. In the open-source project URL-Shortener, several views were gradually refactored to implement this separation.

This chapter shows how existing Vaadin code has been adapted and which patterns have proven to be particularly practical.

Example 1: MainLayout

In the MainLayout, an inline style was originally used to move an element to the right within a layout.

Before refactoring, the code looked like this:

storeIndicator.getStyle().set(“margin-left”, “auto”);

This solution works technically flawlessly, but leads to layout rules being defined directly in Java code.

After the refactoring, a CSS class was introduced instead.

Java:

storeIndicator.addClassName(“layout-spacer-right”);

CSS:

.layout-spacer-right {
  margin-left: auto;
}

The layout rule is now exclusively in the stylesheet, while the Java code only contains the structural information that this component has a specific layout role.

Example 2: Map layout in the AboutView

The AboutView uses multiple containers that are visually represented as maps. Originally, these layout rules were defined directly via style calls.

Typical inline code looked something like this:

card.getStyle()
    .set("background", "var(--lumo-base-color)")
    .set("border-radius", "var(--lumo-border-radius-l)")
    .set("box-shadow", "var(--lumo-box-shadow-s)");

These styles were then converted into a reusable CSS class.

CSS:

.card {
  background: var(--lumo-base-color);
  border-radius: var(--lumo-border-radius-l);
  box-shadow: var(--lumo-box-shadow-s);
  padding: var(--lumo-space-l);
}

Java:

card.addClassName(“card”);

The advantage is that all cards in the application now have a consistent appearance, and changes can be made centrally.

Example 3: Structured layout classes

Another pattern is to name layout roles via CSS classes explicitly. Instead of generic styles, semantic classes are introduced to describe an element’s purpose.

Examples include:

.searchbar-container

.bulk-actions-bar

.overview-grid

Such classes make it easier to understand the application’s CSS layout.

Refactoring as an incremental process

It is important to note that this change need not occur in a single step. In practice, an incremental approach has proven to be effective.

  • New components are developed directly with CSS classes
  • Existing inline styles will be refactored when the opportunity arises
  • Recurring style patterns are gradually centralised

In this way, an existing Vaadin application can be converted into a cleanly structured CSS architecture without major risks.

The examples from the URL shortener show that even small refactorings can have a significant impact on an application’s maintainability. Removing inline styles makes the Java code clearer, while the visual design can be maintained centrally in the stylesheet.

The next chapter identifies the general best practices that can be derived from this refactoring and how to apply them in future Vaadin projects.

Best Practices for CSS in Vaadin Flow Applications

After the refactoring of the existing views is complete, the question arises: which general rules can be derived from this? In practice, it has been shown that a few simple principles are already sufficient to keep Vaadin Flow applications maintainable and consistent in the long term.

This chapter summarises the most important best practices that have proven themselves during the conversion of the URL Shortener project.

Java describes structure – CSS describes representation

The most important rule is also the simplest: Java defines the structure of the user interface, CSS defines its visual design.

In Vaadin, the entire UI composition is modelled in Java. Layout containers, components and their hierarchy therefore clearly belong in the Java code.

Examples of structural definitions in Java include:

  • Layout containers (VerticalLayout, HorizontalLayout, FormLayout)
  • Component Structure
  • Event Handling
  • Data Binding

Visual aspects, on the other hand, should be regulated via CSS. These include, in particular:

  • Spacing
  • Colors
  • Shadows
  • Typography
  • visual highlights

This clear separation prevents UI logic and presentation details from being mixed in the same code.

CSS Classes Instead of Inline Styles

Inline styles should only be used in exceptional cases. Instead, it is recommended to define consistent CSS classes and bind them to components via addClassName().

Example:

component.addClassName(“card”);

The visual definition is then done in the stylesheet:

.card {
  padding: var(--lumo-space-m);
  border-radius: var(--lumo-border-radius-l);
}

This approach ensures that layout rules can be maintained centrally.

Leveraging Lumo Design Variables

Vaadin uses Lumo, a consistent design system that provides numerous CSS variables.

Typical examples are:

  • –lumo-space-m
  • –lumo-border-radius-l
  • –lumo-box-shadow-s
  • –lumo-primary-color

These variables should be used preferably, as they automatically harmonise with the chosen theme and ensure consistent spacing and colours.

An example:

.card {
  padding: var(--lumo-space-l);
  border-radius: var(--lumo-border-radius-l);
}

Semantic Class Names

CSS classes should not only describe how something looks, but what role an element plays in the layout.

Less helpful would be, for example, names like:

  • .box1
  • .red-border
  • .container-large

Semantic names that are based on views or components are better:

  • .about-card
  • .searchbar-container
  • .bulk-actions-bar
  • .overview-grid

This means that even in larger style sheets, it remains clear to which area of the application a style belongs.

Use responsive layouts consciously

Vaadin already provides many mechanisms for responsive layouts, including FormLayout, FlexLayout, and SplitLayout.

For example, FormLayouts can be configured directly via Java:

form.setResponsiveSteps(
    new FormLayout.ResponsiveStep("0", 1),
    new FormLayout.ResponsiveStep("900px", 2)
);

These structural layout decisions belong in the Java code by design, as they are part of the UI composition. CSS then only complements the visual behaviour.

Inline Styles as a Deliberately Used Exception

Despite all the recommendations, there are situations in which inline styles can be useful.

Typical examples are:

  • dynamically calculated layout values
  • short-term UI adjustments
  • experimental prototypes

However, it is important that these cases remain the exception and do not become the default style of an application.

The combination of a clear division of responsibility, reusable CSS classes and consistent design variables results in a much more maintainable Vaadin application.

Especially in projects developed over a longer period, this structure quickly pays off. Changes to the visual design can be implemented centrally, while the Java code continues to describe only the user interface’s structure and behaviour.

Conclusion of the refactoring

The switch from inline styling to a clearly structured CSS architecture may seem like a small cosmetic change at first glance. In practice, however, it quickly becomes apparent that this refactoring has a profound impact on the maintainability and comprehensibility of a Vaadin Flow application.

In the original state of the application, many layout and display details were anchored directly in the Java code of the views. Method calls such as getStyle().set(…) provided quick UI adjustments in the short term but, in the long term, led to a mixing of structure and presentation. The result was a codebase in which design decisions were spread across numerous classes.

With the introduction of CSS classes and the consistent outsourcing of visual properties to style sheets, this mixing has been broken up again. Java now only describes the structure of the user interface: components, layout containers, navigation and interactions. The visual design, on the other hand, is defined centrally via CSS.

This separation brings several immediate benefits.

First, the Java code’s readability improves significantly. Views focus on the user interface’s composition without being overwhelmed by layout details. Developers can more quickly understand which structure a view has and which components interact with each other.

Second, the application’s design becomes more consistent. Reusable CSS classes ensure that similar components in different views are also visually identical. Changes to the appearance can be made centrally in the stylesheet without adapting numerous Java classes.

Thirdly, this structure facilitates team collaboration. Developers can focus on the Java side of the application, while design adjustments can be made independently in CSS. Especially in larger projects, this reduces conflicts and simplifies further UI development.

The refactoring of the URL shortener project has shown that even relatively small changes to the style structure can have a big impact on long-term maintainability. Especially in applications developed over the years, a clear separation between structure and presentation quickly pays off.

Vaadin Flow remains a server-side UI framework that puts Java at the centre of UI development. Nevertheless, such an architecture also benefits significantly from the classic principles of web development: structure, presentation, and behaviour should be clearly separated.

If you apply this basic rule consistently, you will get a Vaadin application that is not only functionally stable but also remains understandable and maintainable in the long term.

An unexpectedly hassle-free upgrade

The starting point for this article was not a strategic architecture workshop or a long-term planned migration path, but a comparatively unspectacular step: updating the version numbers in the existing project. As part of further developing my URL shortener project, a regular dependency update was due anyway. Vaadin 25 (was already available at that time, so it made sense to proceed.

The source code for the project can be found and is

available under https://3g3.eu/url

The expectations were realistic, perhaps even slightly sceptical. A new major release, new minimum requirements, a modernised stack – all of these are usually good reasons to expect at least minor adjustments or initial friction. Accordingly, there was little hope that simply checking the version numbers would be sufficient.

However, that was exactly the case. After adapting the Vaadin version and the current Java and platform dependencies, the project could be built, started, and operated without further changes. The application behaved as before: views were rendered correctly, dialogues worked, styles were intact, and even more complex UI flows showed no abnormalities.

This result was remarkable in that it was not based on a super trivial demo project. The URL shortener is a mature application with a clear module structure, server-side UI logic, security mechanisms and a small number of UI components. The fact that a major upgrade was possible under these conditions without immediate follow-up work is not routine.

Screenshot of a URL Shortener dashboard overview showing a list of shortcodes, corresponding URLs, creation dates, active status, expiry information, and action options like Import and Export.

It was precisely this experience that triggered a closer look at Vaadin 25. If a framework upgrade of this magnitude works right away, the question inevitably arises: why? What decisions in the substructure make this possible? Where was stability deliberately focused? And where are the changes lurking that only become relevant on closer inspection?

Why Vaadin 25 is more than a version leap

Vaadin 25 does not mark a classic evolutionary step with a collection of new widgets or API extensions, but a deliberate realignment of the framework. While previous major releases often featured additional features or incremental improvements, Vaadin 25 focuses on consolidation: fewer special logic elements, less legacy, and closer alignment with established standards in modern Java and web development.

This realignment is a direct response to the reality of many productive enterprise applications. Today, long-running web UIs must not only be functional but also maintainable, secure, and easy to integrate for years. This is exactly where Vaadin 25 comes in. The framework views itself less as an isolated ecosystem with its own rules and more as an integral part of a contemporary Java stack.

A central signal for this is the clear commitment to modern platforms. With Java 21 as the minimum requirement and the focus on current Jakarta and Spring versions, Vaadin is deliberately positioning itself where the rest of the enterprise Java stack is moving. Older compatibility promises are abandoned in favour of creating a clean, consistent technological underpinning.

Vaadin 25 is also a turning point conceptually. Many mechanisms that were once considered Vaadin-typical are now being questioned or simplified. Styling follows more classic CSS principles, build processes are approaching the standard Java workflow, and client-side code is becoming leaner and more transparent. The result is a framework that requires less explanation and integrates more seamlessly into existing development and operational processes.

Vaadin 25 is therefore not a release that primarily stands out for visible UI innovations. Its added value lies in its long-term perspective: more stable foundations, reduced complexity, and better integration with modern Java architectures. For developers and architects, this means one thing above all: In the future, decisions for or against Vaadin can be made more clearly along technical criteria – and less along framework specifics.

The new minimum standard: Java 21, Jakarta EE and modern platforms

With Vaadin 25, the framework draws a clear line and defines a new minimum standard for technology. This decision was made deliberately and is aimed at projects to be operated in the long term, using current Java platforms. Backward compatibility at all costs is abandoned in favour of clarity, consistency, and maintainability.

The focus is on Java 21 as a binding basis. Vaadin thus relies on the current LTS version, which brings both linguistic and runtime improvements. For Vaadin applications, this means not only access to modern language constructs but also a common denominator across UI code, backend logic, and tests. The days when UI code remained at an older language level for compatibility are over.

Closely linked to this is the alignment with current Jakarta standards. Vaadin 25 is based on Jakarta EE 11 and Servlet 6.1, and finally says goodbye to historical javax dependencies. At first glance, this change may seem like a purely technical detail. Still, in practice, it has noticeable effects: libraries, containers, and security mechanisms now share a consistent namespace and can be integrated more cleanly. Especially in safety-relevant applications, this reduces friction losses and misconfigurations.

This line will also be consistently continued in the Spring environment. Vaadin 25 is geared toward Spring Boot 4 and Spring Framework 7 and does not support earlier versions. This eliminates a significant amount of compatibility code required in previous versions. At the same time, Vaadin applications are moving closer to the mainstream Spring stack, which simplifies operation, monitoring and security integration.

The new minimum standard also affects the entire build and toolchain area. Modern Maven and Gradle plugins, current Node versions for the frontend, and a clearer separation between development and production artefacts make Vaadin projects feel more like classic Java applications. Special profiles and project-specific workarounds are becoming less important.

The perspective is important: Vaadin 25 requires more discipline at the start but reduces complexity over the long term. Projects planning to migrate to Java 21 or that have already migrated to Java 21 and current platforms will benefit directly. For older applications, on the other hand, the upgrade is a deliberate strategic decision – with initial effort but a much more stable foundation for the coming years.

Leaner stack, faster builds and less ballast.

One of the most noticeable effects of Vaadin 25 is not immediately visible in the UI; it is part of the project’s underlying architecture. The framework has been specifically streamlined in this version: transitive dependencies have been reduced, historical compatibility layers have been removed, and build processes have been simplified. The result is a stack that is much closer to a classic Java server application.

In previous Vaadin versions, it was not uncommon for projects to carry a significant amount of indirect dependencies. Many of these were necessary to support different platform versions, old browser strategies or alternative operating modes. With Vaadin 25, much of this ballast is eliminated. The dependencies are more clearly structured, more transparently comprehensible and overall more manageable.

This reduction directly affects the build process. Both Maven and Gradle builds benefit from shorter resolution times and less special configuration. In particular, the gap between development and production build has narrowed. Vaadin applications behave much more strongly than ordinary Java artefacts, making it easier for new developers to get started and simplifying CI pipelines.

Startup times in development mode also benefit from this approach. Fewer initialisation steps, clearer separation between server and client components, and modernised front-end dependencies result in a faster feedback loop. Especially in larger projects, where frequent restarts are part of the daily routine, this productivity gain should not be underestimated.

Another aspect is resource consumption during operations. A leaner stack does not automatically mean minimal memory usage, but it does reduce unnecessary load. Fewer libraries mean fewer classes, fewer potential conflicts, and a smaller attack surface. For production environments – especially in safety-critical or highly regulated contexts – this is a clear advantage.

The key thing is the demarcation: Vaadin 25 is not a minimal framework. Rather, it is about conscious reduction. Everything that is no longer necessary will be removed or simplified. This attitude runs through the entire stack and is a direct continuation of the repositioning described in the previous chapters.

The leaner stack thus forms an essential basis for further innovations in Vaadin 25. Simplified styling, more stable UI mechanisms and clearer migration paths are only possible because the framework frees itself from legacy burdens that have grown over the years.

Styling & Theming Rethought: CSS Instead of Special Logic

The styling model is one of the areas in which Vaadin 25 stands out most clearly from previous versions. While Vaadin has long brought its own concepts and abstractions around themes, variants, and style hooks, version 25 follows a clear course: CSS is understood as an equal, direct design mechanism – without unnecessary framework-specific paths.

This decision is less cosmetic than architectural. In many projects, the previous styling model required in-depth knowledge of Vaadin to make UI adjustments. At the same time, existing web know-how could only be used to a limited extent. Vaadin 25 specifically reduces this friction by aligning styling more closely with established web standards, making it easier to understand and maintain.

A visible result of this approach is the new default theme Aura. It does not replace Lumo, but sets a different focus. Aura looks calmer, more modern, and less playful without pushing itself to the forefront. For productive applications, this means a consistent look and feel without extensive customisation. Lumo remains available and is particularly suitable for projects that already build heavily on it or deliberately pursue a different visual profile.

In addition to the visual basis, flexibility is crucial. Vaadin 25 makes it possible to change themes dynamically – depending on user roles, clients or environments, for example. Light and dark variants can be realised without far-reaching modifications. This capability is not new, but it can be implemented more cleanly and comprehensibly with the simplified styling model.

The material theme was deliberately removed. This decision underscores the new focus: Instead of maintaining several design languages in parallel, Vaadin focuses on a few well-integrated core themes. For projects that require a highly individualised interface, this is not a disadvantage. On the contrary, a stronger CSS focus allows your design systems to be implemented more consistently and more framework-agnostic.

The effect on team collaboration is also important. Designers and front-end developers can work much more directly with Vaadin 25 without having to familiarise themselves with Vaadin-specific styling concepts. At the same time, Java developers retain full control over the UI’s structure and behaviour.

Before and after: styling in practice

To make the difference tangible, it is worth taking a direct look at typical styling approaches before and after Vaadin 25. The examples are deliberately simplified but clearly demonstrate the paradigm shift.

Example 1: Button styling

Before (classic Vaadin approach with theme variants and Java hooks):

Button save = new Button("Save");
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
save.getStyle().set("background-color", "var(--lumo-success-color)");
save.getStyle().set("color", "white");

The styling here is partly anchored in the Java code. Colours and visual details are controlled via Vaadin-specific variables and APIs, which blur the separation between structure and representation.

After (Vaadin 25, CSS-first):

Button save = new Button("Save");
save.addClassName("btn-save");
.btn-save {
  background-color: var(--vaadin-color-success);
  colour: white;
}

Responsibility is clearly separated: Java describes the structure and semantics, while CSS handles the visual appearance. The styling is easy to find, testable and customizable independently of the Java code.

Example 2: Layout Spacing and Consistency

Before (inline styles in code):

VerticalLayout layout = new VerticalLayout();
layout.getStyle().set("padding", "var(--lumo-space-m)");
layout.getStyle().set("gap", "var(--lumo-space-s)");

Such constructs are functional, but quickly lead to scattered styling knowledge in the code.

After (CSS-based layout classes):

VerticalLayout layout = new VerticalLayout();
layout.addClassName("content-layout");
.content-layout {
  padding: 1rem;
  Gap: 0.5rem;
}

Spacing and layout rules are now centrally defined and can be adapted consistently throughout the project.

Example 3: Dark/Light theme without special logic

Before (Vaadin-specific theme switch):

UI.getCurrent().getElement().setAttribute("theme", "dark");

After (Vaadin 25, CSS-oriented):

UI.getCurrent().getElement().getClassList().add("theme-dark");
.theme-dark {
  --vaadin-color-background: #1e1e1e;
  --vaadin-color-text: #f5f5f5;
}

Changing the theme is now a clearly defined CSS mechanism that can be easily integrated with existing design systems or user preferences.

These examples illustrate the core of the change: Vaadin 25 consistently shifts styling decisions to their proper place. The code becomes clearer, the styling more flexible, and collaboration among backend, frontend, and design roles much more relaxed.

Component and rendering improvements in everyday life

While many changes in Vaadin 25 are structural, the improvements to components and rendering are concrete in day-to-day UI work. In particular, dialogues, overlays, menus, and other floating UI elements benefit from a revised architecture designed for consistency, predictability, and stability.

In previous Vaadin versions, problems with overlaps, loss of focus or unexpected Z-index behaviour were not uncommon – especially in more complex applications with nested dialogues or dynamically reloaded components. Such effects could usually be remedied, but required additional logic, workarounds or a deep understanding of internal Vaadin mechanisms.

Vaadin 25 relies on a unified overlay model here. Dialogues, context menus, tooltips and similar components now follow a common rendering strategy. As a result, they are more consistently positioned, respond more stably to size changes, and behave more predictably when interacting with one another. For developers, this means fewer surprises and less special treatment in the code.

A particularly relevant aspect is focus management. Keyboard navigation, focus restoration after closing a dialogue, and simultaneous interaction with multiple overlays are much more robust in Vaadin 25. This not only improves the user experience but also enhances accessibility and testability.

The interaction with modern layouts also benefits from the new rendering logic. Overlays adapt better to viewport changes, respond cleanly to scroll containers, and retain their position even during dynamic UI updates. Especially in data-driven applications with many interactions, this reduces visual artefacts and inconsistencies.

It is important to note that these improvements hardly require any new APIs. Existing code often benefits from the upgrade itself. At the same time, previously necessary protective measures or auxiliary constructs become superfluous, simplifying the code and making it easier to read.

Before and After: Rendering and Overlay Behaviour in Practice

The differences between previous Vaadin versions and Vaadin 25 are particularly evident when examining typical UI scenarios in real-world applications.

Example 1: Nested dialogues

Before (classic approach with potential overlay issues):

Dialog editDialog = new Dialog();
editDialog.add(new TextField("Name"));
Dialog confirmDialog = new Dialog();
confirmDialog.add(new Span("Save changes?"));
Button save = new Button("Save", e -> confirmDialog.open());
editDialog.add(save);
editDialog.open();

In more complex layouts, the second dialogue may not be positioned correctly on top of the first, or focus and keyboard navigation may be lost.

After (Vaadin 25, consistent overlay model):

Dialog editDialog = new Dialog();
editDialog.setModal(true);
Dialog confirmDialog = new Dialog();
confirmDialog.setModal(true);
Button save = new Button("Save", e -> confirmDialog.open());
editDialog.add(save);
editDialog.open();

Unified overlay rendering reliably stacks dialogues, passes focus correctly, and restores it after closing—without the need for additional auxiliary constructs.

Example 2: Focus return after dialogue closure

Before (manual focus correction necessary):

Button openDialog = new Button("Edit");
Dialog dialog = new Dialog();
dialog.addDialogCloseActionListener(e -> openDialog.focus());
openDialog.addClickListener(e -> dialog.open());

Such patterns were necessary to ensure clean keyboard navigation.

After (Vaadin 25, automatic focus management):

Button openDialog = new Button("Edit");
Dialog dialog = new Dialog();
openDialog.addClickListener(e -> dialog.open());

Vaadin 25 reliably handles focus return. The code becomes shorter, more understandable and less error-prone.

Example 3: Overlay over Grid and Scroll Container

Before (unexpected positioning for scroll containers):

Grid<Item> grid = new Grid<>(Item.class);
ContextMenu menu = new ContextMenu(grid);
menu.addItem("Details", e -> showDetails());

Depending on the layout and scrolling behaviour, the context menu could appear offset or cut off.

After (Vaadin 25, more stable positioning):

Grid<Item> grid = new Grid<>(Item.class);
ContextMenu menu = new ContextMenu(grid);
menu.setOpenOnClick(true);
menu.addItem("Details", e -> showDetails());

The new rendering model handles scroll containers and viewport changes more consistently, so overlays appear where the user expects them.

These examples show that Vaadin 25 solves many UI problems not through new APIs, but through a more robust internal architecture. For existing applications, this often means less code, fewer workarounds, and a UI that behaves stably even in borderline cases.

Element API, SVG and MathML: Technical UIs directly from Java

With Vaadin 25, an area is gaining importance that has so far played only a minor role in many projects: direct work with the Element API. Native support for SVG and MathML significantly expands the scope for design – especially for technical, data-driven user interfaces where classic UI components reach their limits.

Until now, such requirements have often been the point at which additional JavaScript libraries or external front-end frameworks have been introduced. Diagrams, status graphics or mathematical representations could be integrated, but led to media breaks in the code and increased integration effort. Vaadin 25 noticeably reduces this need by exposing these technologies as full-fledged elements in the server-side UI model.

The Element API enables the creation and manipulation of structured DOM elements directly from Java. With the addition of SVG, graphical primitives such as lines, circles or paths can now also be modelled on the server side. For example, it can be used to create simple diagrams, status indicators or visual markers without leaving the Vaadin component model.

A similar effect is observed with MathML. Technical applications that have to represent formulas or calculation results benefit from the fact that mathematical expressions can be described semantically correctly. The display is not only visually more precise, but also more accessible – for example, for screen readers or automated tests.

The decisive point is not so much the technical feasibility as the architectural consistency. By integrating SVG and MathML into the Element API, the Java backend retains control over UI logic, rendering, and data flow. There is no additional client-side state that needs to be synchronised or secured. Especially in safety-critical or highly regulated environments, this advantage should not be underestimated.

Of course, this approach does not replace specialised charting libraries or complex visualisation tools. Vaadin 25 deliberately positions the Element API as a tool for targeted, controlled visualisations. When it comes to clear states, technical information or explanatory graphics, this approach is often more robust and maintainable than an external dependency.

Chapter 6 thus shows another facet of Vaadin 25’s realignment. The framework does not expand its capabilities through additional abstractions; instead, it provides direct access to established web standards. For developers, this means: more expressiveness, less integration effort and a UI that can be cleanly modelled beyond classic form and table views.

Before and After: Technical Visualisation with and without Element API

The advantages of the extended Element API are best understood through concrete examples. The following scenarios are typical of technical applications and illustrate the difference between traditional integration approaches and the Vaadin 25 solution.

Example 1: Status indicator as SVG

Before (external JavaScript library or client-side snippet):

Div status = new Div();
status.getElement().setProperty("innerHTML",
  "<svg width='20' height='20'><circle cx='10' cy='10' r='8' fill='green'/></svg>");

The SVG code is embedded here as a string. Structure, semantics, and type checking are lost, and changes are error-prone.

After (Vaadin 25, SVG via Element API):

Element svg = new Element("svg");
svg.setAttribute("width", "20");
svg.setAttribute("height", "20");
Element circle = new Element("circle");
circle.setAttribute("cx", "10");
circle.setAttribute("cy", "10");
circle.setAttribute("r", "8");
circle.setAttribute("fill", "green");
svg.appendChild(circle);
getElement().appendChild(svg);

The SVG is now a fully modelled DOM element. Changes can be made in a targeted manner, and the structure remains comprehensible and testable.

Example 2: Simple Bar Chart

Before (passing data to client-side code):

JsonObject data = Json.createObject();
data.put("value", 75);
ui.getPage().executeJs("renderChart($0)", data);

This creates additional client-side state that must be synchronised and secured.

After (Vaadin 25, server-side generated SVG):

int value = 75;
Element bar = new Element("rect");
bar.setAttribute("x", "0");
bar.setAttribute("y", "0");
bar.setAttribute("width", String.valueOf(value));
bar.setAttribute("height", "20");
bar.setAttribute("fill", "#4caf50");
Element svg = new Element("svg");
svg.setAttribute("width", "100");
svg.setAttribute("height", "20");
svg.appendChild(bar);
getElement().appendChild(svg);

The visualisation follows the server state directly. There is no duplicate logic and no hidden client-side code.

Example 3: Representation of a Formula with MathML

Before (text or rendered graphic):

Span formula = new Span("E = mc^2");

The formula’s semantic meaning is lost, as are its accessibility and machine-evaluability.

After (Vaadin 25, MathML):

Element math = new Element("math");
Element mrow = new Element("mrow");
mrow.appendChild(new Element("mi").setText("E"));
mrow.appendChild(new Element("mo").setText("="));
mrow.appendChild(new Element("mi").setText("m"));
mrow.appendChild(new Element("mo").setText("·"));
Element msup = new Element("msup");
msup.appendChild(new Element("mi").setText("c"));
msup.appendChild(new Element("mn").setText("2"));
mrow.appendChild(msup);
math.appendChild(mrow);
getElement().appendChild(math);

The formula is now semantically correct, accessible, and clearly structured – generated directly from Java.

These examples make it clear how Vaadin 25 can be used to implement technical visualisations without additional front-end dependencies. The Element API thus becomes a targeted tool for controlled, server-side presentation – exactly where classic components are no longer sufficient.

The Importance of UI in Import Processes

Why an import needs a UI at all

Import functions are often treated as purely technical details in applications. Data is read in, processed and then made available – ideally without further interaction. In practice, however, an import is rarely an invisible process. It marks a transition between existing system states, between old and new data, between trust and control. This is exactly where the need for a user interface arises.

In the URL shortener, the import is not designed to run in the background; it is an explicit, visible process. The UI does not take on the role of a technical decision-maker, but that of a transparent mediator. It creates a clearly demarcated space in which an import can occur without immediately modifying existing data. This decoupling alone justifies its own import interface.

The current state of the source code is available on

GitHub: https://github.com/svenruppert/url-shortener or https://3g3.eu/url

Overview of a URL shortener tool displaying a table with shortcodes, URLs, creation dates, active status, expiration, and action options.

Importing is always associated with uncertainty. Even if the data format is known, it remains unclear how incoming datasets interact with existing datasets. Are there any overlaps? Are conflicts arising? Are individual entries incomplete or invalid? These questions cannot be answered by a technical process alone, but require visibility. The UI makes this intermediate state tangible without prematurely evaluating it.

Instead of treating the import as an atomic step, it is understood as a state in the URL shortener. The user interface displays this state, records it, and prevents it from being overlooked and inadvertently entered into a production database. This makes the import process controlled, even if the UI itself does not control the content.

The clear distribution of roles is important here. The user interface does not interpret data, validate content, or make decisions about its technical correctness. Their task is exclusively to make the current technical status of the import visible. This deliberate restraint is crucial, as it prevents UI and import logic from being conflated.

So the import doesn’t need a UI because it’s complex, but because it’s a transition. The surface acts as a boundary between file and system, between potential changes and existing persistence. It gives the import a place, a time and a clear context – and that’s exactly where its raison d’être lies.

The entry point in the application

The import does not start automatically in the URL shortener; it only starts via a deliberately designed entry point in the application. It is where users manage existing short URLs and view the current database. This clearly positions the import as an exceptional act rather than part of the everyday processing flow.

Screenshot of a URL shortener interface showing the import section, including options to upload a ZIP file, preview of URLs, and displayed conflicts.
User interface for importing a ZIP file with a preview section showing a staging ID, number of new items, conflicts, and invalid entries. The interface includes a table for displaying short codes, URLs, and activation statuses.

Entry is done via an explicit action that opens a modal dialogue. This action only creates a new instance of the import dialogue and gives it the necessary dependencies. Further logic is deliberately not provided at this point. By opening the dialogue, the user leaves the application’s normal working context and enters a clearly demarcated area where the import is fully encapsulated.

The modal character of the dialogue fulfils an important function here. It indicates that the import is not a background process running in parallel, but a coherent process with its own state. As long as the dialogue is open, the rest of the UI state remains unchanged. There is no implicit update and no automatic data transfer.

From the user guidance perspective, this creates a clear-cut. The import is not understood as an extension of the table view, but as a temporary workspace. This separation prevents import actions from being mixed with regular editing steps. At the same time, it creates a mental orientation: everything that happens within this dialogue belongs to the import, and nothing beyond it.

From a technical standpoint, the UI assumes no further responsibility at this point. The entry point does not initiate validation or server communication; instead, it delegates the entire import process to the dialogue itself. Only when the user consciously uploads a file does the import process begin.

Button importButton = new Button("Import");
importButton.addClickListener(event -> {
ImportDialog dialog =
new ImportDialog(urlShortenerClient, () -> {
refreshGrid();
});
dialog.open();
});

An excerpt from the corresponding view illustrates this principle. The button opens the import dialogue and optionally passes a callback, allowing a return to the normal UI context after a successful import.

The code makes it clear that the entry point merely instantiates and opens the dialogue. There is no pre-check or interaction with the server or import APIs. The callback is used only for downstream view updates and is not part of the import process itself.

Thus, the entry point defines not only where the import starts, but also how it is perceived. It is a clear turning point in the context of use and forms the basis for all subsequent steps in the import dialogue.

The import dialog as a closed workspace

When the import dialogue opens, the application deliberately switches to a well-defined working mode. The dialogue is not intended as a mere overlay, but as a standalone UI space with its own logic, state, and clearly defined boundaries. Everything that happens within this dialogue belongs exclusively to the import and has no effect on the existing database until the process is explicitly completed.

This partitioning is a central design element. While the dialogue is open, the rest of the application remains frozen in its previous state. Tables, filters or active selections are neither updated nor influenced. This creates a clear separation between ongoing work and the import process, providing both technical and mental orientation.

The import dialogue is implemented as a standalone Vaadin component. It is fully initialised when opened and has no implicit return channel to the calling view. This decoupling allows the dialog to manage its internal state independently. Neither when opened nor during the interaction is it assumed that import data already exists. The initial state is always neutral and empty.

Structurally, the dialogue is divided into distinct sections, each representing a specific phase of import. However, these areas are not implemented as step-by-step wizards; instead, they coexist within a common framework. This keeps the dialogue fully visible at all times and avoids implicit transitions that could obscure the actual state.

It is particularly important that the dialogue does not make a technical interpretation of the import data. It simply provides UI surfaces where results are displayed as soon as the server delivers them. Whether these spaces remain empty or are filled depends solely on the current import state, not on assumptions or expectations of the UI.

The control elements of the dialogue also follow this principle. Actions such as validation or application of the import are not permanently available, but are tied to the internal state of the dialogue. As long as there is no corresponding import data, the dialogue remains inactive. The UI does not enforce an order or progression, but only reacts to clearly defined state changes.

The import dialogue thus acts as a controlled container for a potentially critical operation. It fully encapsulates the import, makes its current status visible, and prevents incomplete or unchecked data from taking effect unnoticed. This clear boundary underpins all subsequent steps in the import process and explains why the dialogue was deliberately designed as a closed workspace.

Implementation of the ImportDialog in Vaadin

The import dialogue in the URL shortener clearly shows how little “framework magic” it takes to build a complete, state-driven UI with Vaadin Flow. The entire functionality is built from a few easy-to-understand building blocks: a dialogue container, standard components such as upload, grid, tabs, and button, and clear state variables that carry the dialogue throughout the import process. The result is an interface that treats the import as a separate workspace while keeping the source code manageable.

public final class ImportDialog
extends dialog
implements HasLogger {
private final URLShortenerClient client;
private final Upload upload = new Upload();
private byte[] zipBytes;
private final Button btnValidate = new Button("Validate");
private final Button btnApply = new Button("Apply Import");
private final Button btnClose = new Button("Close");
private final Div applyHint = new Div();
private string stagingId;

Getting started is already remarkably simple. Dialog is a normal Java class that extends Dialogue. This means the UI is not a declarative construct or a template; it can be fully traced in Java code. At the same time, the dialogue deliberately remains stateful: it holds both the uploaded ZIP bytes and the stagingId generated by server-side validation.

With this structure, it’s already clear how Vaadin Flow works here: Components are objects, and the dialogue has them like ordinary fields. The UI is thus automatically addressable without requiring “UI IDs” or bindings. At the same time, the import state is not kept externally; it belongs precisely to the dialogue in which it is made visible.

The constructor then shows the core of the Vaadin mechanics. With just a few lines, the dialogue is configured as a modal, resizable container. There is no separate configuration file and no DSL, but pure Java calls. This allows the import dialogue’s visual framework to be understood directly.

setHeaderTitle("Import");
setModal(true);
setResizable(true);
setDraggable(true);
setWidth("1100px");
setHeight("750px");

Immediately afterwards, it becomes apparent how Vaadin Flow typically models interactions. The buttons are initially deactivated and are unlocked only via events. This initialisation is a central part of the state model. The dialogue always starts neutrally and does not force progress unless there is a ZIP file.

btnValidate.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
btnValidate.setEnabled(false);
btnApply.addThemeVariants(ButtonVariant.LUMO_SUCCESS);
btnApply.setEnabled(false);
btnClose.addClickListener(_ -> close());
btnValidate.addClickListener(_ -> validate());
btnApply.addClickListener(_ -> applyImport());

The interaction of UI and file upload can also be read directly. The upload will be limited to ZIP files and will have a maximum size. The key point here is the in-memory handler: once the file is successfully uploaded, the bytes are placed in zipBytes and the next step is unlocked by enabling btnValidate. The transition from “upload available” to “validation possible” is thus a single, very understandable state change.

upload.setAcceptedFileTypes(".zip", APPLICATION_ZIP);
upload.setMaxFiles(1);
upload.setMaxFileSize(IMPORT_MAX_ZIP_BYTES);
UploadHandler inMemoryUploadHandler = UploadHandler
.inMemory((metadata, bytes) -> {
String fileName = metadata.fileName();
long contentLength = metadata.contentLength();
logger().info("uploaded file: fileName: {} , contentLength {}", fileName, contentLength);
zipBytes = bytes;
logger().info("setting zipBytes..");
btnValidate.setEnabled(true);
});
upload.setUploadHandler(inMemoryUploadHandler);

The actual UI structure is then created in buildContent(). Here, too, the strength of Vaadin Flow becomes apparent: Layouts are components, and the dialogue is simply built by assembling components. Headings, upload area, preview summary, tabs, and a central content container are merged into a single VerticalLayout. The code reads like a description of the interface.

var root = new VerticalLayout(
new H3("Upload ZIP"),
upload,
new H3("Preview"),
summary,
applyRow,
tabs,
tabContent
);
root.setSizeFull();
root.setPadding(false);
renderTab(tabContent);
return root;

For displaying results, the dialogue uses two grid instances and switches between them via tabs. The decisive factor is that the tab interaction does not trigger any backend logic; it only updates the visible projection. In Vaadin Flow, switching is a single listener that empties the container and uses the appropriate combination of paging bar and grid.

tabs.addSelectedChangeListener(e -> renderTab(tabContent));
private void renderTab(VerticalLayout container) {
container.removeAll();
container.setSizeFull();
if (tabs.getSelectedTab() == tabInvalid) {
container.add(pagingInvalid, gridInvalid);
container.expand(gridInvalid);
} else {
container.add(pagingConflicts, gridConflicts);
container.expand(gridConflicts);
}
}

At this point, it becomes clear why the implementation remains so compact despite several states. Vaadin Flow provides the UI building blocks, and the dialogue connects them via a few, clearly defined state variables and listeners. The actual import process remains server-side. The UI only needs to know when a state is reached and how to display it.

This makes the ImportDialog a good example of how Vaadin Flow reduces complexity: not by hiding logic, but by using a direct programming model in which UI components, events, and state converge into a single, easy-to-read Java codebase. Especially for a minimalist import function, a complete, robust interface can be created quickly without requiring additional framework layers.

File upload: Transport instead of interpretation

In the import dialogue, the actual import process does not begin with a technical decision, but with a purely technical step: uploading a ZIP file. This moment marks the transition from an empty, neutral dialogue state to a potentially processable import without already making any content evaluation.

The upload component is firmly anchored in the dialogue and visible from the beginning. It deliberately serves as the first point of interaction within the closed work area. However, their task is clearly limited. The upload is used exclusively to receive a file and make its content temporarily available in the UI context. Neither the structure nor the semantics of the data contained play a role at this point.

This attitude is directly reflected in the source code. The upload is configured to accept exactly one ZIP file, and the maximum file size must not be exceeded. These restrictions do not serve the purpose of technical validation; they serve only the technical validation of the UI process.

upload.setAcceptedFileTypes(".zip", APPLICATION_ZIP);
upload.setMaxFiles(1);
upload.setMaxFileSize(IMPORT_MAX_ZIP_BYTES);

Once a file is selected and uploaded, an in-memory upload handler processes it. The dialogue saves the complete contents of the ZIP file as a byte array in an internal field. At this point, there is no check to verify whether the file contains import data or is structured correctly. In the UI, the file is initially a binary block.

UploadHandler inMemoryUploadHandler = UploadHandler
.inMemory((metadata, bytes) -> {
zipBytes = bytes;
btnValidate.setEnabled(true);
});
upload.setUploadHandler(inMemoryUploadHandler);

These few lines mark a crucial UI state change. Upon successful upload completion, the validation button is enabled. That’s all that happens at this point. The upload alone does not trigger any server communication and does not create an import state. It merely signals that there is now enough information to trigger the next user step.

Errors during uploads are also dealt with exclusively from a technical perspective. If a file is rejected, for example, due to an incorrect data type or a file size that is too large, the UI responds with a corresponding notification. However, there is still no evaluation of the file’s content.

upload.addFileRejectedListener(event -> {
String errorMessage = event.getErrorMessage();
Notification notification = Notification.show(errorMessage, 5000,
Notification.Position.MIDDLE);
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
});

The upload section of the dialogue thus clarifies a central design principle: The user interface does not interpret imported data. It merely ensures that a file is received in a technically correct manner and transfers this state into a clearly recognisable next possibility for action.

Only by clicking the “Validate” button does the dialogue leave this purely technical state of preparation. The file upload thus serves as the necessary, but deliberately content-free, basis for all subsequent steps of the import process.

Validation as UI state change

By clicking on the “Validate” button, the import dialogue leaves its purely preparatory state for the first time. Up to this point, only technical requirements were created: a file was uploaded and stored in memory without its content being interpreted or further processed. The validate() method now marks the clear transition from a passive UI state to a state-changing step that makes the import visible for the first time.

From a user interface perspective, validation is not a technical review process, but a coordinated flow of multiple UI updates, all triggered by a single user action. The dialogue deliberately assumes no responsibility for the content. It asks the server to verify the uploaded ZIP content and processes only the technical response it receives.

The introduction to the method is correspondingly defensive. First, the dialogue state checks whether a ZIP file exists. If this is missing, the UI displays a brief notification and cancels the process. At this point, no exception is propagated, and no internal state is changed. The validation is thus clearly tied to a previous, explicit user action.

if (zipBytes == null || zipBytes.length == 0) {
Notification.show("No ZIP uploaded.", 2500, Notification.Position.TOP_CENTER);
return;
}

Only when this prerequisite is fulfilled is the actual validation step triggered. The dialogue passes the complete contents of the ZIP file to the client and calls the dedicated server endpoint for import preview. For the UI, this call is a black box. It does not know the internal validation rules or the criteria by which entries are classified as new, conflicting, or invalid.

String previewJson = client.importValidateRaw(zipBytes);

The server’s response is received in its entirety as a JSON string. Instead of converting this into a fixed data model, the UI extracts the individual values relevant to representing the import state. This includes the generated stagingId, as well as the counts of new, conflicting, and invalid entries. These values are immediately visible in the interface and constitute the first concrete import state displayed by the dialogue.

this.stagingId = extractJsonString(previewJson, "stagingId");
int newItems = extractJsonInt(previewJson, "newItems", 0);
int conflicts = extractJsonInt(previewJson, "conflicts", 0);
int invalid = extractJsonInt(previewJson, "invalid", 0);

With these values set, the character of the dialogue changes noticeably. The previously empty preview area is populated, and the import receives an identity for the first time: the stagingId. Nevertheless, the UI remains strictly descriptive. It does not evaluate whether the figures are plausible or in a certain relationship to each other. It only shows the current technical status.

At the same time, the dialogue reads the result lists directly from the server response. Iterators are used to read JSON arrays for conflicts and invalid entries, and to convert them into UI-internal row objects. These are then assigned to the corresponding grids. Here, too, there is no interpretation. If an array is empty or non-existent, the tables remain empty – a state that the UI treats as completely valid.

for (String obj : new ItemsArrayIterator(r, "conflictItems")) {
Map<String, String> m = JsonUtils.parseJson(obj);
conflictRows.add(ConflictRow.from(m));
}

After the grids are filled, paging information is updated, and the associated UI components are synchronised. This step is also performed exclusively based on the previously determined result lists, without any additional feedback to the server.

With the completion of the validate() method,  the import dialogue is in a new, stable state. The uploaded data is validated on the server side; the results are visible, and the dialogue can now decide which further actions to offer the user. Note that validation itself does not trigger an import. It only changes the UI state and creates transparency about what a subsequent import would do.

The validate() method is thus the central linchpin of the entire import dialogue. It combines the upload and presentation of results without making technical decisions itself. In this role, it becomes the core of the UI-controlled import process.

Presentation of results without interpretation

After validation, the import dialogue is in a state where concrete results are available for the first time. However, these results are not interpreted, weighted, or further processed; they are only presented. The last chapter describes exactly this moment: the transformation of raw information delivered on the server side into visible UI structures – without the user interface drawing its own conclusions.

Central to this chapter is the realisation that the dialogue lacks a technical view of conflicts or invalid entries. He knows neither their significance nor their effects. Instead, it performs a purely mechanical task: it takes lists of results and renders them into prepared UI components.

This projection is done via two separate tables, each associated with a specific result type. Conflicts and invalid entries are deliberately not presented together; they are kept in separate contexts. The dialogue offers two tabs for this purpose, which the user can switch between. However, this change only changes the view of the data, not its content or state.

The technical basis of this representation consists of two grid components, which are already fully configured when the dialogue is set up. Columns, widths, and display logic are fixed before it is even known whether data will ever be displayed. The grids thus exist independently of the existence of results and represent a stable projection surface.

gridConflicts.addColumn(ConflictRow::shortCode).setHeader("shortCode").setAutoWidth(true).setFlexGrow(0);
gridConflicts.addColumn(ConflictRow::d iff).setHeader("diff").setAutoWidth(true).setFlexGrow(0);
gridConflicts.addColumn(ConflictRow::existingUrl).setHeader("existingUrl").setAutoWidth(true);
gridConflicts.addColumn(ConflictRow::incomingUrl).setHeader("incomingUrl").setAutoWidth(true);

These grids are filled not through a traditional data model, but via direct iteration over JSON fragments in the server response. The UI reads each object from its corresponding array and converts it into a simple line representation. These row objects contain exactly the fields that should be displayed, no more and no less.

for (String obj : new ItemsArrayIterator(r, "conflictItems")) {
Map<String, String> m = JsonUtils.parseJson(obj);
conflictRows.add(ConflictRow.from(m));
}

It is noteworthy that the dialogue makes no assumptions about the number of expected entries or about which fields must be present. If an array is empty or cannot be read, the associated table remains empty. This state is not treated by the UI as an error; it is considered a valid result of validation.

The tab control also follows this principle of neutrality. The currently selected tab only determines which table is visible. When switching, no data is reloaded, and no states are changed. The UI only shows a different section of the same import state.

tabs.addSelectedChangeListener(e -> renderTab(tabContent));

The presentation of results is supplemented by a simple paging component that operates only on results already loaded. It provides a better overview of large datasets but is fully local and does not execute any additional server queries. Here, too, no content filter is applied; paging is purely representation-oriented.

The interplay of grids, tabs, and paging results in a deliberately restrained user interface. It shows what’s there and doesn’t show anything that hasn’t been clearly delivered. Neither is there an attempt to compensate for missing data, nor are implicit assumptions made about the “actual” state of the import.

This makes it clear that the result displayed in the import dialogue is not an analysis tool, but a mirror. It reflects the state delivered by the server exactly and defers any further evaluation to subsequent steps in the import process.

Action Control and Apply Logic

After uploading, validating, and displaying the results, one central question remains in the import dialogue: Can the import actually be used? The answer to this question is not implicit or automatic. Instead, it is completely controlled by the UI state.

The basis of this control is not an additional server call, but the already known import state. All information relevant to the decision is already available to the UI. The updateApplyState() method serves as a central hub that interprets the state and translates it into concrete UI activations or deactivations.

The starting point is a strictly defensive default state. Regardless of what happened before, the Apply button will be disabled first. The UI never assumes that an import is automatically applicable. Only when all conditions are explicitly fulfilled is this state lifted.

btnApply.setEnabled(false);
btnApply.setText("Apply Import");

The first hard test point is the presence of a stagingId. Without this identifier, there is no valid import context from a UI perspective. Even if the data is already displayed, the import remains inapplicable until a server-side confirmed staging state is reached. The UI does not treat this case as an error, but as an incomplete state.

Then, the two result dimensions that have already been made visible in the previous chapters are considered: invalid entries and conflicts. Invalid entries generally block the import. As soon as at least one invalid record exists, the Apply function remains deactivated, and the dialogue explicitly communicates this state via a hint text. The UI does not force a correction; it simply makes it clear that an import is not possible under these conditions.

Conflicts, on the other hand, are treated differently. From a UI perspective, they do not represent an absolute exclusion but rather a deliberate decision-making process. The dialogue includes a checkbox for this purpose, allowing the user to specify whether conflicting entries should be skipped during import. Only with this explicit consent is the import released despite existing conflicts.

This differentiation is directly evident in the interplay of the checkbox, information text and apply button. Activating or deactivating the checkbox immediately triggers a reassessment of the UI state without loading or recalculating any additional data. The UI responds only to the known state.

if (conflicts > 0 && !chkSkipConflicts.getValue()) {
applyHint.setText("Apply disabled: " + conflicts + " conflict(s). Tick "Skip conflicts on apply" to proceed.");
return;
}

When the import is finally approved, not only does the button’s activation change, but its label does as well. This allows the UI to clearly signal the conditions under which the import will be executed. This visual notice is part of the action management and serves to ensure transparency, not to enforce professional rules.

By clicking “Apply Import”, the dialogue leaves its purely display and decision modes. Only at this point is another server call triggered, which actually applies the previously validated import. Up to this point, the UI has only managed states, displayed them and demanded decisions.

The action control thus forms the deliberate conclusion of the import dialogue. It bundles all previously built-up information and converts it into an explicit user decision. It is precisely this restraint – not to apply anything automatically, to imply anything – that makes dialogue a controlled and comprehensible tool within the application.

JSON export in Vaadin Flow

Export functions are often seen as a purely technical side task: one button, one download, done. In a Vaadin-based application, however, it quickly becomes apparent that exporting is much more than writing data to a file. It is a direct extension of the UI state, an infrastructural contract between frontend and backend, and a decisive factor for maintainability and predictability.

This article shows how a JSON-based export was deliberately designed as a UI-driven workflow in the URL shortener project. The focus is not on file formats or complex backend abstractions, but on the clean embedding of the export in a Vaadin Flow interface: filter coupling, download mechanics, paging boundaries and clear responsibilities between UI, client and server.

The current source code can be found on GitHub under https://github.com/svenruppert/url-shortener or https://3g3.eu/url

Overview page of a URL shortener tool displaying a list of shortened URLs, their user IDs, creation dates, status indicators, and options for managing the links.

Export from a UI point of view: more than a download button

In classic web applications, export is often thought of as an isolated API endpoint. From the perspective of a Vaadin UI, this consideration falls short. For the user, export is not a technical process but a consequence of the current UI state: the filters applied, the sorting, and the paging limits.

An export that ignores this coupling immediately leads to cognitive breaks. The display in the grid shows a certain amount of data, but the export provides something else – be it more, less or simply different data. This is exactly where it is determined whether an application is perceived as consistent.

The claim in the project was therefore clear: The export is not a special function, but a mirror of the UI state.

This decision shapes all further design steps – from the filter generation to the download mechanics to the structure of the export data itself.

Initial situation: functional export, but non-UI

Before the revision, an export was already technically possible. Data could be read on the server and returned to the client as JSON, ensuring the basic functionality was fulfilled. However, from a user interface perspective, this implementation introduced several structural issues that only became apparent upon closer inspection.

The response structures were inconsistent and required the client to interpret them in context. The meaning and interaction of HTTP status codes and response bodies were not explicitly defined; they were derived from implicit assumptions in the client code. At the same time, there was no clear connection between the export and the currently visible filters in the interface. From the UI’s perspective, it was only possible to trace the data that was actually exported indirectly. In addition, there was special logic for empty results or error cases that could not be derived consistently from the response itself but were distributed across several places in the client.

From Vaadin’s perspective, the export was not an integrated UI workflow but rather an isolated technical endpoint. The UI had to provide knowledge of special cases, status codes, and response formats that were not explicitly covered by a contractually defined framework. This state of affairs was also reflected in the test landscape: tests were often based on concrete string representations or complete JSON output rather than on clearly defined functional structures. Changes to filters, sorting or response formats therefore had to be followed up in several places and carried an increased risk of unintended side effects.

In short, the export worked technically but did not meet the requirements for a UI-enabled, traceable, and maintainable component in a Vaadin application.

Design Goal: Export as a Deterministic UI Workflow

The central goal of the redesign was not “more features”, but predictability. For the Vaadin UI, this means:

The export uses the same filters as the grid and thus reflects the current UI state. Paging limits are deliberately set and comprehensible for developers and users alike, so that the scope and character of the export remain clearly recognisable. Success, empty results and error cases are clearly distinguishable and can be handled in the UI without special logic. At the same time, the download behaves browser-compliantly and UI-stably without affecting the current UI state.

From the UI’s perspective, an export must not have its own state. He must not “think” anything, expand anything, or change anything implicitly. It is a snapshot of what the user sees – nothing more, nothing less.

Uniform responses as a prerequisite for clean UI logic

In a Vaadin application, API responses have an immediate effect on the UI code, as each response is typically translated directly into UI states, component logic, and user feedback. In contrast to purely client-side frontends, the UI logic is tightly coupled to server-side processing: Each response directly updates component state, enables or disables controls, and renders feedback to the user.

In this context, inconsistent response formats inevitably lead to complex if-else cascades in the UI code. Special treatments for seemingly trivial cases, such as “empty” exports or different error states, must be explicitly requested. The UI code starts by interpreting technical details of the API – such as certain HTTP status codes or the presence of individual JSON fields – instead of relying on clearly defined business signals. This not only increases the code complexity but also complicates the interface behaviour during extensions, making it harder to understand and more error-prone.

In the URL shortener project, this problem was solved by introducing an explicit and stable response structure. Regardless of whether an export record is empty or contains an error, the response always follows the same structure. HTTP status codes are still used to signal the rough outcome of a request, but they do not serve as the sole signifier. The actual technical information – such as the context, scope, and content of the export – is transmitted in full and consistently in JSON format.

A simplified, real export from the system illustrates this approach:

{
  "formatVersion": "1",
  "mode": "filtered",
  "exportedAt": "2026-02-05T11:28:54.582886239Z",
  "total": 9,
  "items": [ /* subject records */ ]
}

This creates a clear and stable contract for the Vaadin UI. The UI code can rely on the fact that metadata such as mode, exportedAt, or total is always present and interpreted consistently. The interface no longer has to guess whether an export was successful or whether there are special cases. Instead, the process can be designed in a linear, deterministic way: metadata is evaluated, the scope is checked, and the user data is processed or reported back to the user.

This structure has far-reaching consequences for UI logic. Loading indicators, confirmation dialogs or error messages can be derived directly from the structured response, without additional special logic or context-dependent checks. This keeps the interface clear, predictable, and closely linked to the technical significance of the answer, rather than tied to technical special cases or implicit assumptions.

Filter logic as a common language between the grid and export

A crucial Vaadin-specific point is the reuse of the filter logic. There is no separate export filter in the project. Instead, the export is generated exclusively from the current UI state.

The SearchBar acts as the only source of truth:

public UrlMappingListRequest buildFilter(int page, int size) {
  UrlMappingListRequest req = new UrlMappingListRequest();
  req.setPage(page);
  req.setSize(size);
  req.setActiveState(activeState);
  req.setCodePart(codeField.getValue());
  req.setUrlPart(urlField.getValue());
  req.setFrom(from);
  req.setTo(to);
  req.setSort(sort);
  req.setDir(dir);
  return req;
}

This Request object is used for both grid display and export. This guarantees:

Display and export thus produce identical results, since both are based on the same filter definitions. Changes to filters or collations automatically and consistently affect display and export without requiring additional code. At the same time, there are no hidden or implicit export parameters, so the export behaviour can be fully explained by the UI state.

From a maintenance perspective, this is a significant advantage: if you understand the UI, you understand the export.

Download mechanics in Vaadin: Button ≠ Download

A common mistake in Vaadin applications is trying to start a file download directly from a button click. Technically, this is problematic: a button click primarily triggers server-side logic, whereas a download is a resource from the browser’s perspective.

In Vaadin, a button click is primarily a server-side UI event. The browser does not send a “classic” download request; instead, Vaadin processes the click via its UI/RPC communication (server round-trip, event listener, component update). From the browser’s perspective, this is not a normal navigation or resource retrieval. And that’s exactly why “button clicks → browser downloads file” is not reliable, because the browser typically only starts a download cleanly when it retrieves a resource (link/navigation) or submits a form – i.e. something that is perceived in the browser as a “real request for a file”.

The anchor (<a>) element solves this problem because it is a standard download target for the browser: it has an href attribute that points to a resource, and the download attribute signals to the browser: “This is a file”. In Vaadin, you bind this href to a StreamResource. This creates a separate HTTP request when clicking the anchor, which is not part of the Vaadin UI event flow but rather an independent resource retrieval. Only at this moment is the StreamResource “pulled”, and the export content is generated on demand.

In practice, this has three major advantages:

  1. Browser compliance and reliability: The download is started via a mechanism that the browser natively supports. This reduces edge cases in which a download triggered by a UI event is blocked or behaves inconsistently (e.g., pop-up/download policies, timing, UI updates).
  2. Decoupling from the UI lifecycle: The download occurs in a separate request. Even if Vaadin processes UI requests in parallel, if the user clicks on or rerenders the interface, the download can continue to run stably. This is especially important if export generation takes longer or is streamed.
  3. Clean accountability: The button is purely UI/UX (icon, tooltip, permissions, enable/disable, visual feedback). The anchor is purely “transport” (browser download). The StreamResource is purely a “data supplier” (the export is generated only when needed). This separation makes the code more maintainable and reduces the side effects.
Button btnExport = new Button(VaadinIcon.DOWNLOAD.create());
btnExport.setTooltipText("Export current result set as ZIP");
btnExport.addClickListener(e ->
    exportAnchor.getElement().callJsFunction("click")
);

The actual download behaviour is in the anchor connected to a StreamResource:

StreamResource exportResource =
    new StreamResource("export.zip", () -> {
      UrlMappingListRequest filter =
          searchBar.buildFilter(1, chunkSize);
      return urlShortenerClient.exportAllAsZipDownload(filter);
    });
exportAnchor.setHref(exportResource);
exportAnchor.getElement().setAttribute("download", true);

This pattern clearly separates the responsibilities: the UI interaction is limited to the button, which serves exclusively as a trigger for export. The browser download is triggered via the anchor element and is therefore treated as a regular resource request. Finally, the data is made available via the StreamResource, which only generates the export content when it is actually downloaded.

The export is only generated when the browser actually retrieves the resource – not when the user clicks on it.

StreamResource: Export on demand instead of in advance

The use of StreamResource is not a detail, but a deliberate architectural decision. The export is generated on demand while the browser reads the stream.

This has several advantages. On the UI side, the memory footprint remains low because the export does not need to be fully pre-generated and buffered. At the same time, the UI thread is not blocked because the data transfer occurs outside the regular UI lifecycle. The download can continue regardless of the current UI state, even if the user navigates or performs further actions during this time. If errors occur during stream generation, they can be propagated cleanly via a separate HTTP request without causing the UI state to become inconsistent.

The export is thus technically decoupled from the UI lifecycle, although it is logically triggered by the UI.

Paging boundaries as a protective mechanism

Another explicitly UI-related aspect of the export implementation is the deliberate limit on export quantity. The export uses the same chunkSize as the grid in the interface and is additionally limited by a fixed upper limit. This decision ensures that the export always remains within a clearly defined framework and can be derived directly from the current UI state.

From an architectural perspective, this limitation prevents the export from processing large amounts of data in an uncontrolled manner when a user triggers an export. Especially in Vaadin applications, where UI interactions are typically synchronous with server-side logic, this protective measure is crucial. It reduces the risk of heavy memory loads, long runtimes, or blocking operations that could negatively impact other users or the entire server.

At the same time, the paging boundary conveys a clear technical semantics to the outside world. The export is deliberately defined as an image of the currently visible result set. It mirrors exactly what the user sees in the grid, including filtering, sorting, and paging configurations. This does not imply a claim to completeness, as is typically associated with a backup.

This clarity is particularly relevant for user expectations. The export does not provide a complete system print or a historically complete data set, but a specifically selected excerpt. The limitation makes this character explicit and prevents misinterpretations, such as assuming that an export can fully restore the system.

From a maintenance and operations perspective, the paging boundary also serves as a natural safety line. It forces us to consciously design export scenarios and, if necessary, to provide separate mechanisms for backups or mass data withdrawals. As a result, the export remains a controllable UI tool and does not insidiously become an infrastructural backdoor for unlimited data queries.

In summary, limiting export volume is not a technical constraint but a deliberate design decision. It combines UI state, user expectations and system stability into a consistent overall picture and underlines once again that the export in the URL shortener is understood as a UI-driven result set – and expressly not as a substitute for a backup.

Real JSON export from the running system

The architectural decisions described above are particularly well understood from a real export of the running system. The following JSON export was generated directly from the Vaadin interface and represents a specific UI state at a defined point in time.

Even at the top level, the export contains all the necessary contextual information to enable independent classification. The formatVersion field explicitly defines the export format version, providing a stable foundation for future extensions. Changes to the internal data model do not automatically propagate to the export contract, provided the version limit is respected.

The field mode is deliberately chosen to speak. The filtered value makes it unmistakably clear that this is not a complete data deduction, but a result set restricted by UI filters. This information is crucial because it prevents the export from being mistakenly interpreted as a backup. The export does not capture the entire system state; it only includes the section the user has seen in the grid.

With exportedAt, the exact time of snapshot creation is recorded. The export thus clearly refers to a defined system state. Later changes to individual data records are deliberately not included and can be clearly delineated on the basis of this time stamp. This context is supplemented by the total field, which indicates the number of exported data records and enables a quick plausibility check without analysing the actual user data.

The actual technical data is located exclusively in the items array. Each entry describes a single URL-mapping dataset, including subject-relevant properties such as shortCode, originalUrl, and active, as well as temporal attributes createdAt and, optionally, expiresAt. It is notable that these objects contain no UI or export-specific metadata. They are deliberately reduced to the technical core and could also come from other contexts in the same form.

It is precisely this clear separation between top-level metadata and functional user data in the items array that makes the export an explainable artefact in itself. Even without knowledge of the internal code or the Vaadin interface, it is possible to determine when the export was created, under what conditions, its scope, and where the actual technical data begins.

The real export thus confirms the design goals described above. It is reproducible, rich in context and clearly recognisable as a UI-driven result set. Instead of merely transporting data, it also conveys its meaning and context of creation – a property that is crucial for maintainability, analysis and long-term further processing.

Effects on maintainability and comprehensibility

The tight coupling between the export and the UI state ensures behaviour that is predictable for developers and users alike. The export follows the same rules as the grid display and contains no hidden special paths or implicit deviations. As a result, the export automatically evolves with the UI: any adjustment to filters, sorting, or paging mechanisms has a consistent effect on both paths without requiring additional synchronisation code.

From a developer’s perspective, this architecture significantly reduces cognitive load. There is no separate mental model space for exporting, as its behaviour can be completely derived from the known UI state. If you understand the grid and its filter logic, you automatically understand the export. This not only simplifies onboarding new developers but also reduces the risk of unintentional inconsistencies during refactorings or functional enhancements.

Testability also benefits directly from this clarity. Since the export has no state and relies on stable request and response structures, it can be tested in isolation. Tests can be run with specific filter combinations and validate the resulting exports without simulating the entire UI or complex interaction sequences. At the same time, UI tests remain lean because they can focus on correctly generating the filter state.

In the long term, this structure improves the maintainability of the overall system. Changes to the UI do not introduce hidden side effects in the export, and conversely, further development of the export does not require parallel adjustments elsewhere. The risk of divergent logic paths between display and export is not only reduced but systematically eliminated.

In summary, the close integration of UI state and export logic ensures that export is not a special case in the system. It becomes a transparent, explainable, long-term, and maintainable component of the application that fits seamlessly into the existing Vaadin architecture.

Conclusion

The export in the URL shortener is not an isolated API endpoint, but an integral part of the Vaadin UI architecture. It follows the same rules as the grid, uses the same filters and respects the same boundaries.

Vaadin Flow applications in particular show that a cleanly integrated export is less a question of the file format – and much more a question of clear responsibilities, explicit contracts and a consistently conceived UI workflow.

Advent Calendar 2025 – Extracting Components – Part 2

What has happened so far

In the first part, the focus was deliberately on the user interface’s structural realignment. The previously grown, increasingly monolithic OverviewView was analysed and specifically streamlined by outsourcing key functional areas to independent UI components. With the introduction of the BulkActionsBar and the SearchBar, clearly defined building blocks were created, each assuming a specific responsibility and freeing the view from operational details.

This refactoring was not a cosmetic step but a conscious investment in the application’s long-term maintainability. By separating presentation, interaction, and logic, a modular foundation was created that is not only easier to test but also significantly simplifies future extensions. The OverviewView transformed from a function-overloaded central element into an orchestrating instance that brings components together instead of controlling their internal processes.

The second part now builds upon this foundation. While Part 1 highlighted the motivation, goals, and initial extractions, the focus now shifts consistently to the new structure of the OverviewView itself: how its role has changed, how event flows have been simplified and how the interaction of the extracted components leads to a clearer, more stable architecture.

The source code for this article can be found on GitHub at: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-11

Here is a screenshot of the current development status from the user’s perspective.

Overview page of a URL shortener application, featuring options to search, filter, and manage short links with corresponding URLs and actions.
Screenshot of the URL Shortener application interface, showing the 'Create new short links' section with input fields for target URL, expiration date, and aliases. The section includes buttons to save or reset the form and validations for the aliases, displaying their status as valid or invalid.
Screenshot of a URL shortener application overview, displaying a functional user interface with options for search filters, pagination controls, and a data table showing shortcodes, original URLs, creation dates, active states, and actions.

The focus is on dividing the previously monolithic OverviewView into clearly defined, reusable components such as the BulkActionsBar and the SearchBar. This separation improves maintainability, increases clarity and makes it easier to test individual functional areas. At the same time, this will pave the way for further UI optimisations and interaction patterns to be implemented gradually in the coming days.

New structure of the OverviewView

After removing the subcomponents, the new OverviewView appears in a much more streamlined, clearer form. While previously a large number of elements, event handlers and logic fragments were spread across the entire class, the View is now limited to a few, clearly defined tasks: the initialisation of the core components, the setup of the grid and the orchestration of the interactions between the SearchBar, BulkActionsBar and the backend.

This new structure follows a simple but powerful principle: the OverviewView focuses on bringing the components together rather than on how they work internally. It defines which building blocks are displayed, how they work together, and when they need to be updated. The internal structure of the individual elements – whether it be the handling of filter changes, the management of bulk actions, or the technical logic of the DataProvider – lies entirely within the respective components.

This clear layout makes the OverviewView look much tidier. The previously lush sections, with interspersed event listeners, complex UI layouts, and manual error handling, have largely disappeared, either merged into outsourced components or removed. They are replaced by short, easy-to-understand methods that control the flow of the view and connect its various building blocks.

This reduction not only makes the OverviewView easier to maintain but also easier to expand. New functions can be integrated at appropriate points without risking damage to existing logic. At the same time, an easily testable structure is created, as the dependencies are clearly defined and the responsibilities are clearly separated.

A central element of the new structure is the initialisation of the view, which is now clearly structured and largely free of embedded logic. This new distribution of roles can already be clearly seen in the constructor:

Here it becomes clear: The OverviewView only instantiates its components, inserts them into a layout and delegates all details to specialised classes. Neither filter logic nor bulk operations are included here – the view only orchestrates.

Another example of the streamlined structure is the way selection is handled in the grid. In the past, there was extensive logic for buttons, states and actions here; today, the view only controls the visibility and status of the BulkActionsBar:

So the view only handles showing or hiding the BulkActionsBar. The actual logic for executing the actions lies entirely in the component itself.

The refresh mechanic is now clearly defined as well. Instead of repeating refresh calls in many places in the code, the safeRefresh() method has been created, which centrally defines how updates are performed:

This design not only makes updating cleaner, but also prevents duplicate refreshes and unwanted infinite loops.

The new clarity is also reflected in the configuration of the grid, which remains complex in terms of content, but is clearly delineated and outsourced to its own methods:

This structure makes it clear which areas of the view have which tasks. The multi-outsourced logic ensures that each method fulfils a clearly defined function.

Simplified event pipeline after refactoring

Refactoring has significantly reduced this complexity. The event pipeline now follows a linear, predictable flow: users interact with the UI components, which trigger clearly defined actions; the OverviewView responds with manageable control signals and finally updates the grid via a uniform refresh mechanism. Critical is the introduction of safeRefresh() and withRefreshGuard(), which prevent unnecessary or recursive updates.

The result is an architecture in which events are no longer propagated across the view, but run along a handful of defined interfaces in a structured manner. This is already clearly visible in the central listener setup of the OverviewView:

This is where the new clarity becomes apparent: External events such as MappingCreatedOrChanged trigger a targeted refresh; the grid selection only affects the BulkActionsBar; the paging buttons only control page and refresh. The complex logic from earlier days is clearly divided.

The keyboard shortcuts also integrate seamlessly into this simplified pipeline and use the encapsulated bulk logic:

On the SearchBar side, event processing is also greatly relieved and follows a clear pattern. Changes to filters or settings always lead to the OverviewView and its uniform refresh mechanism:

The use of withRefreshGuard(true) in combination with safeRefresh() ensures that certain internal state changes do not immediately trigger a cascade refresh, but are triggered in a controlled, conscious manner.

Improvements in the MultiAliasEditorStrict

The MultiAliasEditorStrict is a central UI element in the URL shortener, allowing you to edit and manage multiple alias variants of a shortlink. During the refactoring, this editor was also revised to better integrate with the new component architecture and, at the same time, improve the user experience. Many of the original challenges resulted from tight integration with the OverviewView and from a poorly structured internal logic that had grown over the course of development.

One of the most essential innovations concerns the consistency of his behaviour. The MultiAliasEditorStrict is implemented as a standalone, clearly focused UI element that is solely responsible for capturing and validating aliases:

It is already clear here that the editor has a well-defined task: it knows the preview baseUrl prefix, manages its own UI elements, and provides an isAliasFree function to check for alias conflicts on the server side.

The structurinterface’s interface is entirely contained within the build() method. This is where text input, toolbar and grid are assembled:

This makes the user interface clear: First, aliases are entered in the bulk field, then transferred to the grid via “Take over” and finally checked via “Validate all”.

The MultiAliasEditorStrict has also been improved in terms of structure and internal architecture. Instead of distributing logic fragments across validation, synchronisation, and UI updates in an unstructured way, these areas were clearly separated and moved to dedicated methods. This is particularly evident in the grid structure and in the validation logic.

The grid itself displays the alias lines, a preview and the status in a compact form:

Here, it is clearly visible how all relevant information – alias, preview URL, status, and deletion action – converges in a separate, self-contained table. The status display plays a central role in providing the user with feedback on format errors, conflicts, successful validation or entries that have already been saved.

The real intelligence of the editor lies in the parseBulk() and validateRow() logic. parseBulk() takes over the task of processing the free text input, detecting duplicates and creating new lines in the grid:

The validation of individual rows is encapsulated in validateRow() and follows a clear step-by-step model of format checking, duplicate checking, and optional server querying:

Together with the status enum and the inner row class, this results in a self-contained, easily comprehensible state machine for each alias:

Overall, the revision of the MultiAliasEditorStrict shows how even smaller UI components can benefit significantly from a clean structure, clear responsibilities and consistent behaviour. The component is now more stable, more comprehensible and easier to integrate – ideal conditions for future extensions or adjustments.

Cheers Sven

Advent Calendar 2025 – Extracting Components – Part 1

Today marks a crucial step in the evolution of the URL shortener’s user interface. After the focus in the past few days was mainly on functional enhancements – from filter and search functions to bulk operations – this day is dedicated to a structural fine-tuning: the refactoring of central UI components. This refactoring not only serves to clean up the code but also creates a clear, modular basis for future extensions as well as a significantly improved developer experience.

The source code for this article can be found on GitHub at: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-11

Here is a screenshot of the current development state from the user’s perspective.

Screenshot of a URL shortener interface showing the overview with a search bar, pagination controls, and a table of shortened URLs with their details such as shortcode, creation date, and active status.
A screenshot of a URL shortener interface showing the 'Create new short links' section. The left sidebar displays navigation options like Overview, Create, Youtube, and About. The main area includes fields for entering a target URL, expiry date, and time, along with action buttons for saving or resetting the form. Additionally, there's a section for entering aliases with a table displaying existing aliases, their previews, and statuses.
Screenshot of the URL shortener overview interface, displaying fields for searching, filtering, and a table showing shortened URLs with related information such as creation date and active status.

The focus is on dividing the previously monolithic OverviewView into clearly defined, reusable components such as the BulkActionsBar and the SearchBar. This separation improves maintainability, increases clarity and makes it easier to test individual functional areas. At the same time, this will pave the way for further UI optimisations and interaction patterns to be implemented gradually in the coming days.

Motivation for refactoring

As an application’s functionality increases, so does its source code complexity. Especially in a UI that is continuously expanded and adapted to new requirements, this quickly leads to monolithic structures in which presentation, logic and state management are closely interwoven. This is precisely the situation that had developed in the OverviewView over the past few days of development: a central view that had to take on more and more tasks and thus became increasingly difficult to understand, extend, and test.

Refactoring today, therefore, starts at a fundamental point. The goal is not to introduce new features, but to create a clear, modular foundation that makes future expansions much easier. Individual UI building blocks are removed from the overloaded view and formed into independent components, with responsibility-specific tasks, clear communication channels, and significantly improved reusability. This not only reduces the complexity within the view itself, but also makes the entire architecture more robust to change.

At the same time, this refactoring strengthens the developer experience: The structure of the code becomes more comprehensible, components can be improved in isolation, and future changes – such as to the search logic or bulk operations – can be made in the right place in a targeted manner. The result is a UI design that is more stable, more flexible and easier to maintain in the long term.

Why is this step a turning point today

Today marks a critical moment in the development of the URL shortener’s user interface, as the focus shifts for the first time from new features to the code’s structural quality. While the previous steps were primarily aimed at delivering visible improvements for the user, it is now a matter of ensuring the foundation on which these functions remain solid and scalable over the long term. Such refactoring creates clarity in areas that had previously grown instead of planned – and that’s precisely what makes this day a turning point.

By separating the central UI building blocks, an architecture emerges that no longer consists of a single, heavyweight view but of clearly defined components that perform precisely defined tasks. This structural realignment opens up new possibilities for expansion, halts progress on technical debt reduction, and ensures that upcoming features no longer have to be integrated into a confusing block. Instead, each feature can be implemented in the right place without destabilising existing areas.

This step thus makes a decisive contribution to long-term development: reduced friction in the code, fewer side effects, and greater clarity. So today it’s not about visible innovations, but about the quality of the foundation – and that’s exactly what makes working on upcoming expansions much more efficient, stable and enjoyable.

Overview of the most important changes

Today, the focus is on replacing the previously central OverviewView, which had increasingly assumed responsibility over the course of development and had become correspondingly confusing. By extracting independent components, this complexity is decomposed into clearly defined building blocks.

An essential part of this restructuring is the introduction of the new BulkActionsBar, which bundles all mass operations, making both the code and the user interface more straightforward. The new SearchBar also creates a dedicated area that includes search and filter functions, making it easier to expand. Both components not only simplify the view but also provide a foundation for consistent design and recurring interaction patterns throughout the user interface.

In addition, the internal logic of the overview has been streamlined: state management, event handling, and grid interactions have been rearranged to reduce unwanted dependencies and enable targeted future changes. Minor improvements, such as the unified formatting of specific UI components, also contribute to the overall impression of a clean, structured codebase.

Why are UI components removed from views?

In mature user interfaces, a close interlocking of layout, interaction logic and state management often arises. As a result, central views become increasingly extensive over time and lose their original character. A clear entry point of the application becomes a complex block whose internal logic is difficult to understand. This is exactly where the separation of independent components comes in: It serves to clearly separate responsibilities and shift complexity to where it belongs – into small, clearly defined building blocks.

By extracting UI components, the view is reduced to its core task: orchestrating the interaction of several well-defined elements. Each component takes on a clearly defined role – be it managing search filters, triggering bulk operations, or displaying individual UI sections. This modular approach improves readability, fosters a more natural understanding of the architecture, and significantly reduces side effects during refactoring.

Another advantage is reusability. Components that are only loosely coupled to the overall view can be used flexibly elsewhere, expanded, or improved in isolation. This not only creates a robust structure but also enables more sustainable development practices, allowing new functions to be implemented without major interventions in existing areas. The separation of UI components is, therefore, an essential step in keeping a growing project stable and clear in the long term.

The new BulkActionsBar

Bulk operations are a function that plays an increasingly important role in the daily use of a URL shortener. As the number of entries grows, the need to edit several items at the same time increases – for example, to deactivate expired links, delete outdated campaigns or manage a larger number of new entries together. Such operations are not only a convenience feature but also an essential part of efficient workflows.

In many projects, bulk operations are first integrated directly into the main view. This works well at first, but in the long run, it causes view overload. Buttons, health checks, and more complex interaction logics begin to blend between Grid and View. The result is a structure that is difficult to maintain and in which extensions always carry the risk of unwanted side effects.

The decision to outsource bulk operations to a separate component creates a clear separation: the BulkActionsBar takes over the entire UI-related part of mass actions and thus forms a dedicated functional area. It bundles all relevant actions in one place, ensures a consistent user experience, and reduces complexity in the higher-level view. This clear delineation makes it much easier to add new actions, extend existing ones, or adapt the presentation to future design requirements.

In this way, outsourcing bulk operations makes a decisive contribution to a more stable and flexible architecture – and ensures that the user interface remains clear and intuitive even as requirements grow.

Structure of the BulkActionsBar component

The BulkActionsBar is an independent UI component that bundles all mass actions and presents them in a clearly structured way. Their structure follows the principle that an element should cover exactly one area of responsibility: In this case, the initiation of collection actions without itself containing logic for data processing. This keeps it easy to understand and flexible to use.

The central component of the BulkActionsBar is a clearly defined collection of interaction elements – typically buttons or icons – each of which triggers a specific action. These elements are compactly arranged in a horizontal layout, making them highly visible and intuitively accessible in the user interface. This is complemented by a mechanism that controls the visibility and activability of actions based on whether and how many entries are selected in the grid. In this way, the component not only remains clear but also guides the user through clear interaction instructions.

Another essential aspect of the structure is the abstraction of event processing. The component itself does not perform any operations; instead, it signals to the higher-level view via events or callback functions which action to trigger. This creates loose coupling between the UI and the logic, which both facilitates testing and allows adjustments to individual areas without unintentionally affecting other components.

A practical example of this is the basic structure of the class that defines the BulkActionsBar:

Here, it becomes clear how the component encapsulates all elements relevant to mass actions: It knows the URLShortenerClient, the underlying Grid<ShortUrlMapping>, and the higher-level OverviewView, yet remains clearly delimited as an independent component. All buttons and the selection information area are defined within the class and are initialised in the constructor.

The actual visual structure of the BulkActionsBar is encapsulated in its own method:

The method shows how style, layout and interaction elements are brought together in a central place. The BulkActionsBar thus defines its own visual and functional cluster within the interface, while the URLShortenerClient and the OverviewView still handle the actual execution of the actions.

Overall, this setup enables a clean separation of display and behaviour, provides a robust foundation for future expansion, and integrates seamlessly with the application’s modular architecture.

Using the BulkActionsBar in the OverviewView

The BulkActionsBar is only valuable for interaction with the OverviewView, because there it becomes clear how much outsourcing mass actions simplifies the user interface structure. While these actions were previously implemented directly in the view itself, mixing a large number of event handlers, UI elements, and logic, the BulkActionsBar now handles this entire functional area. The OverviewView only needs to define when the bar becomes visible, which entries are selected, and how to respond to the triggered actions.

The integration follows a clear pattern: As soon as the user selects one or more rows in the grid, the view displays the BulkActionsBar and passes it the current selection state. The component itself does not perform any operations, but signals which action has been triggered via clearly defined methods and events. This creates loose coupling, which increases clarity and significantly improves visibility.

Another advantage is evident when handling status changes. The OverviewView no longer has to activate or deactivate buttons individually or control complex UI dependencies. Instead, a single central feedback to the BulkActionsBar is sufficient; it updates itself. This interaction not only makes the code leaner, but also prevents errors that could easily occur with multiple scattered logics.

A central entry point for the integration is to initialise the BulkActionsBar directly as a field of the OverviewView:

This provides the view with a fully configured instance that knows the URLShortenerClient, the grid, and the view itself. The component is then integrated into the constructor:

Significant is the interaction with the grid’s selection listener. Here, the view decides whether the BulkActionsBar is visible and what information it should display:

This section clarifies the precise distribution of roles: the view recognises the current selection state, updates the visibility of the BulkActionsBar, and forwards relevant status information. The component itself remains completely free of logic for selection management.

The triggering of the actual actions also remains bundled in the view. An example of this is deleting using keyboard shortcuts:

This once again shows the interplay of clearly defined responsibilities: the BulkActionsBar encapsulates the action UI, and the OverviewView determines the context in which it is executed.

Overall, the use of the BulkActionsBar in the OverviewView clearly demonstrates how a clear separation of responsibilities improves the architecture of a Vaadin application. The View refocuses on its core task – presenting and updating the data – while the component encapsulates and consistently delivers all the interaction around mass actions. The BulkActionsBar in the OverviewView demonstrates how a clear separation of responsibilities improves the architecture of a Vaadin application. The View refocuses on its core task – presenting and updating the data – while the component encapsulates and consistently delivers all the interaction around mass actions.

Code Comparison: Before vs. After

The difference between the previous implementation of bulk operations and the new, component-based structure is immediately apparent. Whereas previously all elements, buttons and dialogues were anchored directly in the OverviewView, distributed over numerous event handlers and UI areas, the code is now much more modular. The BulkActionsBar is a standalone component that encapsulates the entire UI for bulk actions. As a result, the OverviewView not only had to accommodate less logic but also gained overall clarity.

The difference is evident in the reduction of responsibilities within the view. Before the refactoring, the OverviewView handled all aspects: providing the buttons, displaying the bar, handling selection, opening and executing dialogues, and providing user feedback. This resulted in an extensive, tightly coupled block of code that was difficult to maintain and error-prone.

After the refactoring, the structure has improved significantly. All UI-related logic of bulk operations is now exclusively in the BulkActionsBar. The OverviewView is limited to recognising the selection and forwarding the necessary information. The component itself still triggers the actions, but the view is no longer involved in the UI’s visual or structural layout. This change improves readability, as each functional area is now located where it belongs. In addition, the risk of unintended side effects is significantly reduced, as changes to the BulkActionsBar no longer require direct intervention in the OverviewView. To clarify the difference, it is worth reviewing typical places that were previously located directly in the OverviewView and are now outsourced. A classic example is the handling of the bulk deletion operation. In the past, the OverviewView itself handled dialogue, loops, error handling, and refresh. Today, the entire logic can be found clearly closed in the BulkActionsBar:

New structure – outsourced bulk delete logic:

Here you can see that the entire interaction – from the UI to the error handling to the update – now takes place within the component and no longer burdens the OverviewView.

New structure – View only signals the selection:

This shows the new role distribution: the OverviewView only controls visibility and status display. In the past, the entire operation logic would have been implemented here as well. Another example is opening the bulk set expiry dialogues. This logic is now also completely in the component and no longer distributed across the View:

The entire dialogue logic is then located in:

Result:
The clear separation of responsibilities results in:

  • significantly fewer lines of code in the OverviewView,
  • a more structured, modular architecture,
  • less coupling,
  • improved maintainability and expandability.

Overall, this results in a more maintenance-friendly, extensible and clear structured architecture.

Search requirements

A practical search function is a central part of any administrative interface, especially when the number of entries is continuously growing. In the context of URL shorteners, this means that users can quickly and specifically access specific short links – regardless of whether they are looking for a particular shortcode, a URL fragment, the active status or time criteria. The previous implementation offered only simple filter options and was tightly coupled to the OverviewView, making both extensibility and maintenance difficult.

However, the requirements for a modern search component go far beyond a simple text field. It must support multiple filter criteria, respond flexibly to new fields, and adapt dynamically to the backend data structure. At the same time, it should offer the user an intuitive, consistent and clutter-free interface. This includes clear input fields, understandable labels, sensible default values, and the ability to change or reset search parameters quickly.

Technical reliability is just as crucial as user-friendliness: the search function must not burden the backend unnecessarily, support high-performance queries, and use server-side filters efficiently. A clean separation between UI, filter logic and data retrieval not only enables a better overview, but also later extensions – such as additional filter fields, sorting options or advanced functions such as combining several criteria.

The introduction of the new SearchBar addresses these requirements. It serves as the comprehensive filter and control centre for the overview and ensures that the view itself is decoupled from the filter logic. In doing so, it lays the foundation for a scalable and user-friendly search and filtering experience.

Structure and internal logic of the SearchBar

The SearchBar is designed as an independent UI component that aggregates all filter and search parameters from the OverviewView and presents them in a clearly structured format. This component aims to centralise previously scattered filter logic and significantly improve view clarity. Whereas previously individual input fields and sorting parameters were defined directly in the OverviewView, the SearchBar now assumes full responsibility for their management.

Its structure follows a modular concept: Each input – be it a text filter, a sorting criterion, the number of displayed elements or filtering by properties such as active status or expiration date – is clearly demarcated within the component and is processed independently. This not only ensures better logical separation but also enables flexible expansion with additional filters without requiring adjustments to the view itself.

The internal logic of the SearchBar works closely with the backend. Based on the user-selected parameters, the component creates structured filter objects that can be passed to the server consistently. Instead of collecting individual parameters loosely or combining them in the view, they are merged in a clearly defined process: validated, normalised, and then transferred to a backend request.

The central method buildFilter, which creates a UrlMappingListRequest object from the UI inputs, shows what this process looks like in concrete terms:

The code shows how all relevant filter fields – active status, shortcode and URL parts, time windows, and sorting and paging information – are consolidated in a single place. The SearchBar thus serves as a translator between UI inputs and the domain-specific filter structure in the backend. This creates a robust interface that both reduces errors and facilitates future expansions. At the same time, the SearchBar ensures a consistent user experience. Changes to a field automatically update the results list, while sensible default values and a consistent display ensure a familiar user experience. This combination of structural clarity, technical precision, and ease of use makes the SearchBar a central building block of the project’s modern UI architecture.

Improvements compared to the previous solution

From a technical standpoint, the new solution offers greater robustness. The processing is now handled entirely in the SearchBar, using structured data objects that are transmitted directly to the backend. This minimises errors caused by inconsistent or incomplete filter parameters. At the same time, the clear structure makes it easy to add new filters without jeopardising existing functionality.

A key example of the improvements is how changes to the SearchBar filter are handled. Whereas previously the OverviewView had to check and react to each value, the SearchBar now handles this task independently. This is already evident in the ValueChange listeners of the individual input fields:

These listeners make it clear that the SearchBar takes complete control of the filters’ behaviour and automatically updates the view. Previously, this logic was distributed in the OverviewView. Global search has also been significantly improved. It used to be a standalone input field with no real integration, but now it dynamically controls the specific search fields and ensures consistent filters:

As a result, global search has become a real entry point for filter logic, rather than being an additional field with no straightforward integration.

Another big step forward is the introduction of the reset mechanism, which resets all filters in a targeted manner while ensuring that the UI returns to a consistent state:

Finally, switching between simple and advanced search also shows the new structural quality of the SearchBar:

Overall, the new SearchBar is far superior in both functionality and architecture. Their listeners, control logic, and consistent handling of filter elements form the basis for a modern, scalable, and maintenance-friendly search architecture. It is not only functionally better, but also forms the basis for a modern, scalable and maintenance-friendly search architecture.

Interaction of the SearchBar with grid and backend

The SearchBar only shows its full strength when interacting with the grid and the backend that powers it. While it serves as a central input component for all search and filter parameters on the interface, it technically forms the link between user interaction and server-side data supply. It is precisely in this role that it becomes clear how crucial the decoupling of the filter logic from the actual view is for performance, maintainability and extensibility.

In the first step, the SearchBar accepts all user input – from global search texts to specific shortcode or URL parts to date ranges, sort fields and the active status. These values are no longer processed ad hoc in the OverviewView, but collected, validated and harmonised in a structured form within the SearchBar. This keeps filter states consistent and traceable, even when multiple input fields are changed simultaneously.

The second step is the interaction with the grid. As soon as a relevant filter value changes, the SearchBar notifies the OverviewView of the updated state. This, in turn, triggers a grid update without requiring the individual filter criteria to be known or processed. The grid then calls the backend via its DataProvider and receives a filtered subset of the data based on the SearchBar’s requirements. This creates a clearly separated yet closely interlinked system of input, data retrieval and presentation.

On the backend, the advantage of a structured filter is particularly evident in how the OverviewView configures its DataProvider. The SearchBar always provides a consistent filter object that is passed directly to the backend calls. Central to this is the initialisation of the DataProvider in the OverviewView:

This makes it clear how closely the grid, SearchBar, and backend interact: the DataProvider calculates the effective query parameters from the current paging information and page size, then builds a UrlMappingListRequest from the SearchBar and passes it directly to the URLShortenerClient. The SearchBar is also used for the counting function, this time without paging parameters, to determine the total number of entries.

The advantage of this structure is that the OverviewView itself does not need to know the details of the filter fields. It delegates the entire filter configuration to the SearchBar and focuses solely on controlling the grid and paging. Changes to the filters – such as additional criteria or changed default values – can be made entirely in the SearchBar without having to adjust the DataProvider or the View.

Overall, the interaction among SearchBar, Grid, and the backend shows a cleanly orchestrated data-flow model: Users change a filter, the SearchBar generates a unique search query, the DataProvider requests the appropriate data via the URLShortenerClient, and the Grid presents the results. This consistent, clearly structured process makes the entire interface much more stable, understandable and responsive.

Cheers Sven

Advent Calendar 2025 – De-/Activate Mappings – Part 2

What has happened so far?

In the first part of this article, the new active/inactive model for shortlinks was introduced and anchored at the architectural level. Based on the technical rationale, it was shown that a pure expiration date is insufficient for modern use cases and that an explicit activity status is required.

Based on this, the technical foundations were laid: the core domain model was extended with an active flag, the DTOs were adapted accordingly, and the serialisation was designed to ensure backward compatibility. In addition, the administrative REST endpoints have been expanded to enable targeted setting, querying, and filtering of activity status. The redirect behaviour has also been clarified so that deactivated and expired shortlinks can be distinguished by clearly defined HTTP status codes.

The structural basis of the active/inactive model is thus fully established. In the second part, the focus is on practical use: how the Java client maps these new capabilities, how users can conveniently control activity status via the Vaadin interface, and how this results in consistent, efficient workflows in everyday use.

The source code for this project‘s status can be found on GitHub at the following URL: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-10

A user interface for a URL shortener application showing an overview of shortlinks with columns for shortcode, URL, creation date, active status, expiration date, and actions including links to activate or deactivate.
Overview of the URL Shortener application displaying shortlinks with columns for shortcode, URL, creation date, activity status, expiration, and actions.

Java Client Enhancements

After the server API had been extended to include functions for activating and deactivating shortlinks, the Java client also had to be adapted accordingly. After all, it is the primary interface for many users to work programmatically with the URL shortener – whether in the context of desktop tools, automations, CI/CD pipelines or embedded systems.

Chapter 5 details how new capabilities have been added to the client to support the active/inactive model fully. These include:

  • the targeted switching of the activity status,
  • the initial setting of the activity and expiration date when creating a shortlink,
  • as well as editing existing mappings, including the new activity field.

The extensions are based on a consistent design approach: simple parameters, clear method focus, strict validation, and traceable error handling. For users, this creates an API that is not only complete, but also intuitive to use – regardless of whether individual values are changed or complete mappings are rebuilt.

New API: toggleActive(shortCode, active)

To allow the user to control the activity status of a shortlink not only via the REST API, but also conveniently via the Java client, a new method has been added to the client API. This method is the functional equivalent of the toggle endpoint on the server side and allows shortlinks to be enabled or disabled directly from applications, scripts, or automations.

The new API method toggleActive(shortCode, active) does exactly this. It ensures that all relevant information is transmitted to the server in the correct structure and that the server’s response is converted into a suitable representation. With a clear focus on switching activity status, the user eliminates the need to build full update objects or send unnecessary data.

Another advantage of this method is its simplicity: the user only needs to specify the shortcode of the link to be changed as well as the desired new status. The client’s internal logic takes care of everything else – from creating the appropriate request payload to interpreting the server response. This makes it particularly intuitive to use and reduces potential sources of error.

In the next step, we will look at the concrete implementation of this method using the source code. The following implementation comes directly from the URLShortenerClient:

The method starts with basic validation and logging. The shortCode parameter must be set because it uniquely identifies the shortlink to change. If the value is null or empty, the client throws an IllegalArgumentException even before an HTTP call is made.

The next step is to create the destination URL for the API call. The server’s administrative base path (serverBaseAdmin) is combined with the toggle endpoint path segment. Thanks to this dynamic composition, the client remains flexible in different deployment environments.

The client then opens an HTTP connection and configures it for a PUT request. The method sets the expected header fields, including Content-Type (for JSON) and Accept, to define the expected response type. setDoOutput(true) indicates that a request body is included in the request.

For the actual payload, an instance of ToggleActiveRequest is created, which consists of a shortCode and the desired new active state. This structure is serialized using toJson and then written to the output stream of the connection.

After the request is sent, the method reads the HTTP status code via con.getResponseCode().  The implementation distinguishes between three main cases:

  1. Successful state change (200, 204, or 201): The method flushes the InputStream via drainQuietly and returns true. This signals to the user that the shortlink has been updated successfully.
  2. Shortlink not found (404): Again, the ErrorStream is emptied. However, the method returns false to make it clear to the user that the shortlink does not exist and therefore cannot be updated.
  3. All other error cases: In the event of unexpected or erroneous responses, the ErrorStream is read and packaged together with the HTTP status code in an IOException. This forces the calling code to handle unforeseen errors and prevents such states from being silently ignored.

Thus, toggleActive provides a clearly defined, robust API for switching activity status via the Java client. It follows the same design principles as the client’s other methods: clear validation, consistent error handling, meaningful logging, and lean, JSON-based communication. The implementation thus integrates seamlessly into the existing client architecture and provides a simple yet effective extension of functionality.

Advanced createCustomMapping(...)

In addition to the possibility to activate or deactivate existing shortlinks afterwards, the process for creating new shortlinks has also been expanded. To allow users to determine whether a shortlink is active when making it, the createCustomMapping(...) method is used in the Java client.

Before this adjustment, a shortlink could be created using only its fundamental properties – shortcode, original URL, and optional expiration date. The activity status was set implicitly or could only be controlled via later processing steps. With the new extension, the user can determine during creation whether a shortlink is active or inactive.

The method follows the same principles as the client’s other functions: it focuses on clear, simple, and reliable communication with the server. The user only provides the required input values. At the same time, the client takes care of the entire technical processing of the request – from creating the request object to JSON serialisation and interpreting the server response.

This expansion allows new use cases to be realised. For example, a shortlink can already be created, but only activated later – for example, synchronously with a release time, a marketing campaign or automatically in CI/CD pipelines. At the same time, the uniform data structure ensures that the activity status of a shortlink is treated consistently both when creating and editing.

In the next step, we examine the concrete implementation of this method using the relevant source code sections and analyse how the extended parameters are integrated into the creation process.

The extension appears in the URLShortenerClient in the form of two overloaded methods: a simple variant and an extended version with expiration and activity parameters:

The simple variant of createCustomMapping serves as a convenience method: it accepts only an alias and a URL and delegates to the extended version, setting expiration date and activity status to zero. This keeps the API lean for simple use cases, while allowing complete control over the shortlink via the overloaded method.

The extended method first assumes responsibility for validating the input data. UrlValidator.validate(url) checks whether the specified destination URL meets the expected criteria. If this is not the case, an IllegalArgumentException is  thrown with a comprehensible error message. If an alias is set, the alias policy is then checked. This ensures that custom shortcodes comply with the set rules and, for example, do not contain unwanted special characters or prohibited patterns.

If the URL and alias are valid, a ShortenRequest is created that includes expiredAtOrNull and activeOrNull, in addition to URL and alias. In this way, the user can control whether the shortlink has an expiration date and whether it should be initially active or inactive when creating it. The request is then serialised in JSON format and sent to the PATH_ADMIN_SHORTEN endpoint.

The HTTP configuration follows the familiar pattern: A POST request is created with a JSON body, the Content-Type is set accordingly, and the body is transmitted via the OutputStream. The server’s response is first checked against the HTTP status code. If successful (200 or 201), the client reads the response body, converts it to a ShortUrlMapping object, and returns it to the user.

Two special paths are provided for error cases: If the alias reservation fails because the alias is already assigned (409 Conflict), an IllegalArgumentException is thrown with a clear description. General validation errors (400 Bad Request) also throw a meaningful IllegalArgumentException. All other unexpected status codes result in an IOException that includes the exact status code and error message from the server.

Overall, the advanced createCustomMapping method fits seamlessly into the existing API design. It allows users to create shortlinks in one step with an alias, expiration date, and activity status, combining strict validation with precise, predictable error handling.

Adjustments to edit(...) – Processing with activity status

In addition to creating new shortlinks, users often need to customise existing listings. This may involve updating the target URL, adjusting an expiration date, or, in the context of the new functionality, activating or deactivating an existing shortlink.

To cover these requirements, the existing edit(...) method of the Java client has been extended. It now also supports the optional activity status parameter, so the user no longer needs to change this value via a downstream toggle API; they can control it directly during editing.

This approach facilitates workflows in which multiple properties of a shortlink are edited simultaneously. Instead of making various API calls in sequence, the entire change process can be combined into a single edit operation. This means that the API remains efficient, consistent and can be easily integrated into various application scenarios – from manual editing to the UI to automated processes in scripts or backend systems.

In the next step, we will discuss the implementation of the extended edit(...) method.

The following implementation comes from the URLShortenerClient and shows how the activity state was incorporated into the editing process:

The method accepts four parameters: the shortcode to change, the new destination URL, an optional expiration date, and an optional activity status. Right from the start, we check whether the necessary fields – especially shortCode and newUrl – are valid. An IllegalArgumentException immediately catches invalid inputs.

The request is then sent as a PUT to the corresponding edit endpoint. The payload consists of a ShortenRequest that contains all editable attributes of a shortlink – including the activity specification activeOrNull. This allows the user to update the URL, expiration date, and activity status in a single process.

The client then processes the HTTP response. Success cases (200, 201, 204) return true, while a 404 clearly indicates that the shortlink does not exist. Unexpected status codes cause an IOException, which allows the user to provide precise fault diagnoses.

This extension makes the edit(...) method fully capable of updating all relevant properties of a shortlink in a single step, including the activity state, which was previously only controllable via a separate toggle endpoint.

User interactions and UI logic in the Vaadin interface

After the previous chapters covered server-side REST handlers, client APIs, and persistence, this chapter focuses on the application’s UI layer. The OverviewView is the central administration tool for users to search, filter, edit, and manage shortlinks in bulk.

Chapter 6, therefore, sheds light on the most critical user interactions in the frontend, in particular:

  • The interaction of Vaadin-Grid, CallbackDataProvider and dynamic filter parameters
  • the integration of single actions (e.g. activating/deactivating a link by clicking on an icon)
  • Implement more complex bulk operations , including dialogues, error handling, and visual feedback
  • The connection between UI inputs and the REST endpoints via the URLShortenerClient

Switching the active state directly in the grid

To allow users to change the active or inactive status of a shortlink not only via the API, but also conveniently in the user interface, the grid of the administration view has been extended. The goal is to create the most direct, intuitive, and low-risk way possible to switch the status of a shortlink without opening separate dialogues or editing masks.

At the centre of each table row is a clearly recognisable visual button. With a single click, the user can toggle a shortlink’s status between active and inactive. The interface immediately signals the current state of the shortlink and the action that will be taken when clicked.

This enlargement has three main objectives:

  • Speed – frequent switching does not require navigation into additional masks.
  • Transparency – the user can see at all times which shortlinks are currently active.
  • Robustness – possible error situations are clearly communicated and do not affect the rest of the application.

The core of the implementation lies in configuring the grid and the new “Active” column. The relevant snippet from the OverviewView looks like this:

Instead of a plain text field, a component column is used here, displaying a separate icon for each row. The choice of icon depends directly on the current activity status of the respective shortlink:

  • If m.active() is true, a CHECK_CIRCLE icon is displayed.
  • If m.active()is false, a CLOSE_CIRCLE icon is used.

The colour scheme (success for active, error for inactive) allows the user to recognise the state at a glance. In addition, the title attribute on the icon tells you which action is triggered by a click (“Activate” or “Deactivate”).

The toggle mechanism is implemented in the icon’s click listener. When clicked, the desired new status is first calculated:

The Java client is then called, which delegates the state change to the server via the REST API:

If the call succeeds, the user receives a short, unobtrusive confirmation notification, and the grid is reloaded via safeRefresh(). This means that subsequent changes (e.g. filtering by active status) are also displayed correctly immediately.

Error handling follows the same pattern as in the client API: If an exception occurs – whether due to network problems, unexpected HTTP status codes or server errors – it is communicated in the UI via a clearly visible notification:

For the user, this means that the active status of a shortlink can be changed directly in the overview with a click. The UI combines clear visual cues (icon, colour, tooltip) with immediate feedback and robust error handling. This reduces the need for separate processing dialogues and makes typical administrative tasks around active/inactive much more efficient.

Filter by active and inactive status

In addition to the ability to switch a shortlink’s active status directly in the grid, the user interface has been enhanced with a precise filter system. This allows the user to search specifically for active, inactive, or all shortlinks – without manual searches or complex queries.

The goal is to provide the user with a tool that allows them to control the visibility of entries flexibly. This improves both the clarity of large datasets and the efficiency of routine administrative tasks, such as detecting expired or disabled shortlinks.

A unified selection element controls the new filter and affects both data retrieval and paging. If the user sets the filter to Active, Inactive, or Not set, the query parameters are updated to load and display only relevant records in the grid.

Central to this is the Select field for the active status, which is defined in the OverviewView:

This UI element is configured in the search bar and displayed alongside other search and paging elements. Initialisation is done in the buildSearchBar() block:

This gives the user a clearly labeled drop-down selection with three states:

  • Active – only active shortlinks
  • Inactive – only inactive shortlinks
  • Not set – no filtering by active status

The filter is set to NOT_SET by default , so all shortlinks are displayed first. The ItemLabelGenerator function determines the label displayed in the drop-down for each enum value.

For the filter to be functionally effective, the view reacts to changes in the selection field. A corresponding listener is registered in the addListeners() block:

As soon as the user changes the active state, the current page is reset to 1 and the data provider is reloaded. This makes the filter changes directly visible and prevents the user from being on an invalid page (for example, if there are fewer entries due to filtering).

The actual connection to the REST API is created in the buildFilter(...)method block that creates a UrlMappingListRequest from the UI  :

Here, the enum value of the activeState select is read and, if it is considered “set,” converted to a Boolean (true for active, false for inactive). The request builder takes over this value and later transmits it to the server by the client (URLShortenerClient) as a query parameter. If the state is NOT_SET, no active value is set, so there is no active-status restriction on the server side.

Together, these building blocks form a consistent filter concept:

  • The Select<ActiveState> provides a clear, three-level selection.
  • The ValueChangeListener ensures that filter changes take effect immediately.
  • buildFilter(...) translates the UI selection into a typed request to the backend API.

For users, this creates a seamless interaction: Switching from “Active” to “Inactive” immediately leads to only the corresponding shortlinks appearing in the grid – without this logic having to be duplicated in the frontend or filtered on the client side.

Mass operations based on active status.

In addition to single editing, the interface offers convenient bulk operations that let users enable or disable multiple shortlinks at once. This is especially helpful in scenarios where large amounts of data need to be managed, such as temporarily shutting down campaign links or reactivating an entire group of previously disabled shortlinks.

In this extension, bulk handling has been designed to integrate seamlessly with the existing grid and the integrated selection mechanisms. The user can select as many rows as they want in the grid and then trigger appropriate actions via clearly recognisable buttons in the bulk bar at the bottom of the screen.

The process always follows the same pattern:

  1. The user marks the desired short links.
  2. A confirmation dialogue ensures that the mass change is deliberately triggered.
  3. The action is performed for each selected entry, regardless of potential errors.
  4. The result is displayed to the user in a compact success/failure overview.

This approach enables robust yet user-friendly bulk editing. The interface remains responsive, and error handling ensures that a failure on a single shortlink does not affect the entire operation.

The following snippet is taken directly from the OverviewView and shows the central method that enables or disables a set of shortlinks in one pass:

This method is central to mass activation. It receives the shortlinks currently selected in the grid, as well as the target status (activate = true or false). For each entry, the corresponding request is sent to the server API via the Java client. The approach is deliberately designed to be fault-tolerant: a failure of one entry does not affect the rest of the processing.

The UI is not updated until the loop is complete. This keeps the operation clear and efficient, even with many shortlinks. The result is displayed to the user in a compact summary.

The user triggers this operation via a confirmation dialogue. This is implemented in confirmBulkSetActiveSelected(boolean activate ):

This dialogue ensures that mass changes are not accidentally triggered. This is especially relevant for deactivating actions, as a deactivated shortlink no longer redirects.

The dialogue is activated via the two buttons in the bulk bar:

The associated wrapper methods are:

This means that mass editing is fully integrated into the user interface: selection, confirmation, execution and result feedback follow a clear, consistent process model. For users, an intuitive workflow makes administrative tasks much easier.

Redirect behaviour for end users.

With the introduction of the new active/inactive mechanism and improved handling of expiration times (expiresAt), the URL shortener’s behaviour when calling a shortlink changes fundamentally. While users previously received only a redirect or a generic error, the system now provides more granular HTTP status codes that enable precise conclusions about a link’s status.

Expired Shortlinks (expiresAt) → 410 Gone

If a shortlink has an expiration date and has passed, the user must not be redirected to the original URL when accessed. Instead, the system must clearly signal that this shortlink exists, but is no longer valid. This is precisely what the HTTP status code 410 Gone is used for.

The status code 410 indicates “permanently removed” and makes it unmistakably clear that the shortlink was previously active but is now deliberately unavailable. Unlike 404 Not Found, which means that a resource does not exist, 410 conveys a clear semantic meaning: this shortlink has expired and will never be valid again.

For the user, this means the call results in clear error behaviour that remains comprehensible. For developers, on the other hand, this mechanism creates transparency, as they can clearly distinguish between three situations:

  • A shortlink does not exist → 404 Not Found
  • A shortlink exists, but is disabled → 404 Not Found
  • A shortlink exists and has expired → 410 Gone

The consistent use of the 410 status code also enables better monitoring and cleaner automation processes. Systems such as API gateways, SEO tools, crawlers, or CI/CD pipelines can capture the exact behaviour and thus more precisely explain why a redirect does not occur.

Disabled (active = false) → 404 Not Found

Another central component of the new active/inactive model is how deactivated shortlinks are handled. While expired links are signalled with the HTTP status code 410 Gone, the system deliberately uses a different status code for disabled links: 404 Not Found.

This difference is not accidental, but follows clear safety and operational considerations. A deactivated shortlink should behave as if it no longer exists for the end user, although it remains fully present, visible, and manageable internally.

This creates a behaviour ideal for maintenance phases, temporary blockades, campaign stops, or safety-related shutdowns. Developers and administrators retain complete control over the dataset without inadvertently revealing internal state information.

Cheers Sven

Advent Calendar 2025 – De-/Activate Mappings – Part 1

Why an active/inactive model for shortlinks?

For many users – especially those who work in the field of software development – shortlinks are much more than simple URL shorteners. They act as flexible routing mechanisms for campaigns, feature controls, test scenarios, and internal tools. The requirements for transparency, controllability and clean lifecycle management are correspondingly high.

The URL shortener receives an active/inactive model that directly supports these claims. A user can now specify, for each shortlink, whether it should be active, temporarily deactivated, or no longer allowed to reach users due to a planned expiration. At the same time, a clear distinction is made between a disabled and an expired shortlink – both in the UI and in the HTTP behaviour.

For the user, this creates a noticeable gain in control. An entire campaign can be taken offline with a single click, without losing data or having to edit multiple entries manually. The REST API or the Java client can be used to automatically control activity status externally, enabling integrations with existing development and deployment processes.

This introduction outlines the motivation for the new model. The following chapters detail how the UI, API, data model, and redirect logic have been extended, and how users benefit from the new active/inactive mechanism.

The source code for this project‘s status can be found on GitHub at the following URL: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-10

Overview of a URL shortener interface displaying shortcodes, URLs, creation dates, activity status, expiration dates, and action buttons.
Screenshot of a URL shortener overview interface displaying a table with shortcode, URL, created date, activity status, expiration, and action buttons.

Architecture overview of changes

The new active/inactive model does not work in isolation within a single module; it spans multiple layers of the application. This creates consistent behaviour for the user – regardless of whether a shortlink is edited via the UI, automatically managed via the API or accessed directly in the browser. To understand this interaction, it is worth examining the key areas where adjustments have been made.

The basis is the extended data model, which now includes an additional attribute to store the activity status. This feature serves as the common denominator for all shortlink-related operations. As a result, both the REST endpoints and the internal Java client have been extended to reliably transmit activity status when creating, modifying, or querying a shortlink.

These structural changes are also reflected in the user interface. The activity status can now be set directly when creating a shortlink, in the overview table, or in the detail dialogue. Actions such as activating or deactivating multiple entries simultaneously use the exact mechanisms as the API, creating a consistent application experience.

Last but not least, the redirect behaviour has also been adjusted. While expired shortlinks are clearly indicated, deactivated shortlinks are not. In this way, the user can not only better understand why a shortlink is unreachable, but also receive more precise feedback from both client and monitoring perspectives.

This architectural overview shows that the active/inactive model is not a one-off feature, but a consistent design principle across all layers of the system. In the following chapters, the respective areas are considered in detail and technically explained.

The Advanced Data Model

Before the active/inactive model becomes visible in the user interface, REST API, or redirect behaviour, the basic structure must be defined in the data model. Chapter 3 focuses on this basis and shows how a shortlink’s activity status is anchored in the core of the system.

The data model is the central place where all relevant information converges into a shortlink. Every user action – be it creating, editing, filtering or forwarding a shortlink – sooner or later falls back on this structure. Therefore, it is crucial to introduce an activity status that is consistently and reliably available.

This chapter explains how the new active attribute is mapped to domain objects, how it is transported in DTOs, and the serialisation and backwards-compatibility considerations involved. This clean anchoring in the data model makes the active/inactive model a stable part of the entire application, on which all further technical enhancements are based.

New active flag in the core domain model.

The introduction of the active/inactive model begins at the core domain model level. This defines how a shortlink stores its state and what information is transmitted within the application. The new active attribute lets users check at any time whether a shortlink is currently in use or has been deliberately deactivated.

The central element of this change is the extension of the ShortUrlMapping class. It outlines the essential characteristics of a short link, including the alias, the original destination URL, and the expiration dates. The new attribute enables the unique determination of the activity status within this structure.

In the original implementation, the relevant snippet looks like this:

This enhancement ensures that activity status is reliably available both in memory and in all downstream layers of the system. The model remains unchanged. The status is not changed in an existing object; instead, it is created as a new instance using the withActive method. This prevents unintended side effects and supports consistent, deterministic data behaviour.

This clear separation between active and inactive states provides a stable foundation for UI, server, and redirect logic. In the following subchapters, we will discuss how this information is mapped to the DTOs and the consequences for serialisation and transport behaviour.

Impact on DTOs (ShortenRequest, ShortUrlMapping)

The active/inactive model not only affects the domain core but must also be transported cleanly across the application boundary. Data Transfer Objects (DTOs) are crucial for this. They serve as the interface between the user interface, REST API, and Java client, ensuring that the activity status of a shortlink is transmitted correctly.

With the expansion of the data model, the DTO ShortUrlMapping also receives an additional attribute that reflects the status of a shortlink. This value is used both in communication from the server to the UI and between the server and the client. A relevant excerpt from the original implementation looks like this:

This extended DTO enables the user to view activity status in the UI and allows external systems to read the status and respond accordingly. By using a record, structure and serialisation remain lean and clearly defined.

The request to create or change a shortlink has also been adjusted. The ShortenRequest now also includes information on whether a shortlink should be made directly active or initially deactivated. This allows the user to control how the shortlink behaves during creation.

An excerpt from the corresponding record:

At this point, it is evident that the request uses a Boolean, whereas the mapping itself uses a primitive Boolean. This difference is deliberate: the value in the request may also be null if the user does not want to set an explicit state. In this case, the server logic decides on a default value. Mapping, on the other hand, always has a defined state, eliminating ambiguity.

Extending DTOs ensures the active/inactive model is uniformly available across all system boundaries. It forms the basis for the UI, API and client to be informed identically about the status of a shortlink, thus creating a consistent user experience.

Serialisation and backward compatibility

For the active/inactive model to function reliably, the new active value must not only be present in the internal data model but also correctly serialised and transported across different components. The extension of serialisation affects two areas in particular: JSON communication via REST and data exchange between the various application modules.

By using Java records in the DTOs, serialisation is primarily handled by the chosen JSON library. Once the active attribute is defined in the record constructors, it is automatically included in the JSON representation. This makes integration straightforward and minimises the need for additional configuration.

An example of the resulting JSON structure of a shortlink as served via the REST API:

The JSON structure makes it clear that the activity state is immediately visible to the user and can be retrieved without any additional logic. This increase in transparency is central to the new model.

An essential aspect of this change is backward compatibility. Systems or components that use older versions of the API and are not aware of the active field can continue to interact without issues. JSON consumers typically ignore unknown fields to avoid affecting older clients. At the same time, the server can set a sensible default value for requests that do not specify an active value – typically true.

This combination of clear serialisation and high backward compatibility ensures that the active/inactive model can be integrated into existing systems without disruption. At the same time, further development remains open to future extensions, such as more differentiated activity states or audit information.

Enhancements to the Admin REST API

The active/inactive model only unfolds its full effect through the extensions of the administrative REST API. This establishes the connection between the data model, the internal business logic, and the various clients that create, edit, or automatically manage shortlinks. To enable users to effectively use the new activity state, several API endpoints had to be adapted or supplemented.

Chapter 4 examines these extensions in detail. It shows how the new toggle endpoint enables targeted switching of activity status, how inactive entries can be queried via a dedicated list endpoint, and how the ActiveState filter extends existing query operations. It also explains how differentiated HTTP status codes clearly convey the semantic meaning of a deactivated or expired shortlink.

These API customisations create a coherent, well-integrated interface that can map all aspects of the active/inactive model, whether the requests come from a user interface, a Java client, or an automated system.

New Toggle Endpoint

To make the active/inactive shortlink model usable across the application, the server component needs a well-defined mechanism to change the activity status of an existing shortlink. For this purpose, a new toggle endpoint was introduced and implemented on the server side by ToggleActiveHandler. It forms the basis for allowing users to adjust the activity state of a shortlink directly from the REST API – whether from the user interface, automations, or external systems.

The new endpoint has a clear purpose: it enables the targeted switching of the activity status of a specific shortlink. The user can specify, via a simple request, whether a shortlink should be activated or deactivated without changing any other mapping properties. This not only makes the operation more efficient, but also less error-prone, as there is no need for full update payloads.

This endpoint integrates seamlessly with the existing API structure and uses the same transport models and validation mechanisms as other administrative operations. At the same time, it provides a clear separation between changes of activity status and other editing operations, simplifying both implementation and use.

The core component of this endpoint is the ToggleActiveHandler. It encapsulates the HTTP-specific processing and delegates the actual state change to the UrlMappingStore behind it:

The handler implements the HttpHandler interface and integrates project-specific logging via HasLogger. In the constructor, the dependency is injected into the UrlMappingStore, which is later used to execute the logic that changes the state.

The main logic is in the try block. First, the request body is completely read in via readBody(ex.getRequestBody()) and converted to an instance of ToggleActiveRequest using fromJson. This Request object contains the two relevant pieces of information: the shortCode and the new active value. It then checks whether the shortcode is empty or null. In this case, the handler responds immediately with a 400 Bad Request and a clear error message, preventing the store layer from encountering invalid data.

If the shortcode is valid, the desired new activity value is extracted and logged. The actual status change occurs via “store.toggleActive(shortCode, newActiveValue)". The result is encapsulated in a Result<ToggleActiveResponse> to ensure both success and failure cases are handled consistently.

In case of success (mapping.isPresent()), another success entry is written to the log, and the updated display of the shortlink is returned to the user as JSON with the HTTP status 200 OK. If the store does not return a result, the cause of the error is logged via ifFailed, and the user is sent a response with a 400 Bad Request status code and a generic error message. This clearly defines the error behaviour without revealing too many details internally.

If unexpected runtime errors occur in the handler itself, the catch block catches the exception, logs a warning, and returns a 500 Internal Server Error. The finally block ensures that the connection is closed cleanly via ex.close() in all cases and that a corresponding log entry is created. Overall, this creates a robust, clearly structured endpoint that reliably and comprehensibly updates a shortlink‘s activity status.

Query inactive links

In addition to targeting the activity status of an individual shortlink, the user needs the ability to view collected inactive entries. This provides an overview of shortlinks that have been deliberately or automatically disabled and are currently no longer redirecting.

For this purpose, a special API endpoint was introduced that returns only inactive shortlinks. This endpoint complements the general list endpoint and provides a clear, filtered view of all shortlinks with the “inactive” status.

The focus of this endpoint is on visibility and control. Users can quickly see which shortlinks are currently disabled without having to implement their own filtering logic or evaluate the complete list of shortlinks. This is a notable advantage, especially in larger installations or automated workflows, as inactive entries often require separate management processes.

The endpoint integrates seamlessly with the existing administrative API structure and uses the same data models and return formats as other query operations. It ensures that information about inactive shortlinks is consistently retrievable across all UIs and clients.

The technical implementation of querying inactive shortlinks is integrated into the existing list handler. The following excerpt shows the ListHandler responsible for this, which provides different list variants, including the inactive entries, via a common endpoint:

The ListHandler, like the toggle handler, implements the HttpHandler interface and uses HasLogger for consistent logging. A UrlMappingLookup is injected into the constructor and used for the data queries. The handle method handles routing based on the request path: Depending on whether the request ends in PATH_ADMIN_LIST_ALL, PATH_ADMIN_LIST_ACTIVE, or PATH_ADMIN_LIST_INACTIVE, a specialised list method is called.

The listInActive() method is responsible for querying inactive shortlinks. It determines the current time and calls filterAndBuild("inactive", ...) with a predicate that selects the desired entries. A shortlink is considered inactive if it is either explicitly marked as inactive (!m.active()) or if it is active but has already expired. The combination of the active flag and the expiration date allows a clean separation between currently usable and no longer usable entries.

The filterAndBuild method reads all entries from the UrlMappingLookup, applies the passed predicate, and converts the remaining mappings into a DTO-like structure. For this purpose, toDto is used to create a map of the most essential attributes from each ShortUrlMapping. In addition to shortCode, originalUrl, createdAt, and expiresAt, the active status and a derived field, status, are set here, which distinguishes between expired and active. This additional information makes it easier for the user to read the entry status directly from the response.

This structure allows queries for inactive links to fit seamlessly into the existing list concept. Instead of introducing a completely independent endpoint, the existing infrastructure is used and expanded to include a targeted view of inactive entries. The result is a precise, reusable mechanism that always gives users a complete overview of all currently inactive shortlinks.

Adjustments in the list request (ActiveState filter)

To allow users to search specifically for active, inactive, or all shortlinks, a dedicated activity status filter has been added to the existing list mechanism. This so-called ActiveState filter complements the previous filter criteria, such as text search, sorting, time periods, or pagination, and enables precise control of the search results via the API.

This extension provides the user with a flexible yet clearly defined way to set the desired status directly via a query parameter. Instead of retrieving the entire list of shortlinks and then filtering it on the client side, the request can now be restricted directly on the server. This saves resources, reduces unnecessary data transfers, and ensures consistent search results.

The ActiveState filter considers three possible states:

  • Active – The shortlink is active and, if there is an expiration date, has not yet expired.
  • Inactive – The shortlink has been disabled by the user or has expired.
  • Not set – The user does not provide a status indication, so the filter is not applied, and all relevant entries are considered.

This distinction is communicated via the active query parameter, which is passed to the standard list endpoint in a GET request. The server evaluates this parameter and creates a corresponding UrlMappingFilter object based on it, which controls entry selection.

The technical implementation of this filter logic is handled in the existing ListHandler, which already handles filtering and sorting shortlinks. The following excerpt shows how to process the active parameter and embed it in the UrlMappingFilter:

The central point is the line:

Here, the query parameter active is read, interpreted as an optional boolean. The process in detail:

  1. first(query, "active") reads the first value of the query parameter active – something like:
    1. active=true
    1. active=false
    1. or the parameter is missing completely.
  2. parseBoolean(...) converts this value to an Optional<Boolean> . This means that invalid or missing values can also be caught cleanly.
  3. .orElse(null) ensures that if input is missing or cannot be interpreted, the value null is transferred to the filter.

This results in the following meaning:

  • active=true → only active shortlinks
  • active=false → only inactive shortlinks
  • no parameter → no restriction

The UrlMappingLookup then processes the resulting UrlMappingFilter:

These methods replace the application of the filter to the saved shortlinks. By passing through the active state, the lookup layer can precisely select the entries corresponding to the desired activity state.

Finally, the results are transferred to the DTO structure and returned as a paginated JSON response:

With this extension, the ActiveState filter fits seamlessly into the existing filter system. Users can now target active or inactive shortlinks without additional endpoints or separate calls. This shows how ActiveState is processed and how it affects the final JSON result.

HTTP status code semantics: 410 vs. 404

With the introduction of the active/inactive model, the behaviour of the forwarding logic also changes. For the user, the state of a shortlink must be clearly and unambiguously communicated to the outside world – especially if a redirect cannot be done as expected. For this reason, the system uses two distinct HTTP status codes to distinguish expired and disabled shortlinks.

A shortlink can no longer be reached for two reasons:

  1. It has expired. The expiration date has passed, and the shortlink is no longer valid as planned.
  2. It has been disabled. The user has manually disabled the shortlink, regardless of the expiration date.

Although both result in no redirect, they differ in meaning. Expired shortlinks are intended to signal that their lifespan is ending clearly. Disabled shortlinks, on the other hand, have been deliberately taken out of service and may be in a temporary state.

Two HTTP status codes represent this semantic separation:

  • 410 Gone – for expired short links. This status indicates that the resource is permanently unavailable and will not become available again.
  • 404 Not Found – for disabled shortlinks. Although the shortlink exists, its redirect is intentionally not carried out. The 404 status code indicates a temporary or permanent error.

This distinction provides users, API clients, and monitoring systems with more precise feedback on the shortlink’s state. Understandably, a shortlink is not accessible due to a natural process or a manual decision.

Cheers Sven

Advent Calendar 2025 – Basic Login Solution – Part 2

What has happened so far?

In the first part of “Basic Login Solution”, the foundation for a deliberately simple yet structurally clean admin login was laid. The starting point was the realisation that even a technically lean URL shortener requires a clear separation between public-facing functions and administrative operations. The goal was not complete user management, but rather a minimal access barrier that integrates seamlessly with the existing Java and Vaadin architectures.

The central component of this solution is a lean, file-based configuration in auth.properties. With just two parameters – an activation switch and a password – the login can be fully controlled. The associated LoginConfigInitializer ensures that this configuration is read when the servlet container starts and remains consistently available to the application. This clearly defines whether and how the protection mechanism takes effect even before any UI is rendered.

Based on this, an independent login view was introduced that does not require MainLayout. It serves as a clear entry point into the administrative area and separates the login context from the rest of the UI, both visually and technically. The implementation focuses on a simplified user experience: a password field, a clear call to action, and clear feedback on success or failure. At the same time, an important architectural principle of today is already evident here: security logic and UI interaction remain neatly separated.

After completing Part 1, a functional login is available, but no full access control is yet in place. Without additional measures, unauthenticated users could continue to access administrative views directly, for example, via deep links or saved URLs. This is precisely where the second part comes in.

In Part 2, login is effectively enforced for the first time: via central route protection in MainLayout, consistent session management with SessionAuth, and a clean logout mechanism. This transforms an isolated login screen into a complete, end-to-end authentication flow that reliably protects the application’s administrative area.

The source code for this article can be found on GitHub at the following URL: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-09

Admin login interface with a prompt to enter the administrator password and a 'Login' button.
Admin interface of a URL shortener application displaying an overview of shortened URLs with options for searching, filtering, and user actions.
Header of an admin interface displaying the title 'EclipseStore', an item count of '3 items', and a 'Logout' button.

Route Protection & Access Logic

With the introduction of admin login, it is not enough to provide only a login page. The decisive factor in the protection’s effectiveness is that all administrative views are consistently protected and accessible only to authenticated users. This is precisely where Route Protection comes in: in conjunction with LoginConfig and SessionAuth, it ensures the application clearly distinguishes between logged-in and non-logged-in users.

Instead of securing each view individually, the implementation uses a single central location: the MainLayout. Since all relevant management views use this layout, it is a natural anchor point for bundling the access logic. As soon as a view with this layout is loaded, the MainLayout can check whether login is enabled and whether the current user is already authenticated. Only when these conditions are met is access to the target view granted.

There are several benefits to implementing route protection within the layout. On the one hand, the code in individual views remains lean because they do not have to perform security checks themselves. On the other hand, a uniform logic is implemented that applies equally to all protected areas. If the mechanism is later expanded or adapted, a change at a central location can affect behaviour across the entire application.

From a functional perspective, route protection follows a simple process: when a navigation enters a view that uses MainLayout, it checks whether login is globally enabled before rendering. If this is not the case, the application behaves as before and still allows direct access to all pages. If login is enabled, the logic checks whether the current session is marked as authenticated. If not, the user will be automatically redirected to the login page.

An exception is the login view itself. It must always be accessible without prior authentication, as it enables access to the protected area. If they were also subject to route protection, an infinite redirect loop would result. The implementation explicitly handles this special case by excluding the login route from the check.

Flowchart illustrating the navigation and authentication process in an admin login system. It includes decision points for checking if login is enabled, the target view, and session authentication status.

In practice, this mechanism results in transparent, predictable behaviour: non-logged-in users who open a deep link directly into the admin UI are automatically redirected to the login page. After a successful login, you will be taken to the desired administration page and can navigate the protected area without further interruptions. At the same time, the option to altogether turn off the login at the configuration level is retained – in this case, the application behaves as if route protection never existed.

Implementation in MainLayout

The technical implementation of route protection is anchored in the MainLayout. This not only serves as a visual framework for the administration interface but also assumes a central control function for all navigation leading to the protected area via the BeforeEnterObserver interface.

First, the layout is extended so that it can participate in navigation and, at the same time, receive logging functionality:

The implementation of BeforeEnterObserver enables the MainLayout to perform a check before each navigation to a View that uses it. At the same time, HasLogger provides a convenient logging API to make decisions and states in the log traceable.

The actual route protection is  bundled in the beforeEnter method:

The logic follows the process described above exactly. First, it is checked whether the login is active per the configuration. If login.enabled is set to false in the auth.properties file, the method returns immediately without any restrictions. In this mode, the application behaves as it did before Route Protection was introduced.

If login is enabled, the next step is to check the current navigation destination and verify whether the user is already authenticated—a short log entry records which view to navigate to and the current authentication status. If the target is the LoginView, the check is aborted – the login page always remains accessible, regardless of whether the session is already logged in or not.

For all other views, if the session is not marked as logged in by SessionAuth.isAuthenticated(), the user will be transparently redirected to the login page. The original target view is not rendered, so no administrative functions are visible without authentication.

Logout button in the header

Closely linked to route protection is the ability to clean end an existing session. This functionality is also implemented in the MainLayout and appears to the user as a logout button in the header. The creation of this button is linked to the configuration:

If the login is deactivated, no logout button is displayed because there is no state to terminate. If the login is active, MainLayout adds a simple inline logout button to the header. Clicking it calls SessionAuth.clearAuthentication(), removes the authentication attribute from the VaadinSession, and closes the session. The browser is then explicitly redirected to the login page via setLocation.

The combination of the beforeEnter check and the logout button creates consistent access logic: Users are directed to the login page when they first access the protected area, can move freely within the admin UI after successful login, and can end their session at any time in a visible, controlled manner.

In the next chapter, we will focus on the SessionAuth class, which encapsulates the login status in the session and provides a centralised access point.

Logout function in the header

A login mechanism is only complete if it also knows a clear way back. In practice, this means that anyone who logs in to the admin interface must be able to end their session just as consciously. The introduction of a logout function is therefore not only a technical addition but also an essential signal to users that access to the administrative area is considered a sensitive context.

In the URL shortener, this functionality is shown as a slim logout button in the header. It is visible only if login protection is enabled in the configuration and the application is in “protected mode”; in all cases where “login.enabled=false", the UI does not display this button because there is no session to log off. This deliberately keeps the interface tidy in simple development or demo scenarios.

From the user’s perspective, the logout button is inconspicuously integrated into the existing header. It does not appear as a prominent CTA, but it is always available in the admin area. The naming is deliberately kept clear: “Logout” leaves no room for interpretation and signals that the current authorisation context is ended here. In environments where multiple people work one after the other using the same browser and admin interface, this clarity is essential.

Technically, clicking the logout button performs two tasks: first, the authentication status in the current VaadinSession is reset; second, the browser is redirected to the login page. This ensures that the administrative views do not remain open in the browser; subsequent access still follows the login flow. Even an accidentally open tab loses its authorisation as soon as the user logs out.

The implementation adheres closely to the rest of the login architecture. The button does not check itself whether a session is authenticated; instead, it delegates this to the small helper class SessionAuth, which is also used elsewhere, such as for route protection. This keeps the logout consistent: the same abstraction that determines whether a session is considered logged in is also responsible for deleting this status.

A flowchart illustrating the logout process in a web application, showing user actions and system responses from selecting logout to ending the session.

From a UX perspective, the logout function also helps structure users’ mental model. As long as they are logged in, they are in an active administration context where changes to shortlinks and configurations are expected. When they log out, they deliberately revert to a neutral role, in which they use the generated URLs at most from the user’s perspective. This separation is particularly helpful in heterogeneous teams where not all participants need administrative rights simultaneously.

Implementation in MainLayout

The logout function is centrally integrated into the MainLayout. The header is built there, and the logout button can be added alongside the app title and the store indicator. Whether this button is visible depends directly on the login configuration:

In activated mode, a “Logout” button is created and added to the header’s HorizontalLayout. The look is based on the rest of the UI and uses the LUMO_TERTIARY_INLINE variant, so the button appears discreet and blends visually with the header.

The actual logoff logic is in the click listener: First, SessionAuth.clearAuthentication() is called to reset the current session’s authentication state. The browser is then explicitly redirected to the login view path. This means that the user is not just taken to a “blank” page after logging out, but is clearly recognisable back to the entry point of the admin area.

If the login function is globally deactivated, the button is omitted. The headerRow layout then consists only of toggle, title, spacer and StoreIndicator. This deliberately keeps the UI slim in scenarios without login protection and does not display a non-working logout button.

SessionAuth – Session state management

The SessionAuth helper class includes all accesses to the authentication state within the VaadinSession. It is used by both the route protection and the logout button and thus represents the central abstraction layer for login decisions:

The markAuthenticated() method is called after a successful login and sets the simple attribute authenticated to TRUE in the current VaadinSession. With isAuthenticated(), this state can be queried again later – for example, in the route protection of the MainLayout. Both methods use logging to quickly determine when a session was logged in or logged out, in the event of an error, or when analysing user behaviour.

For the logout, clearAuthentication() is crucial. It removes the attribute from the session and also closes the VaadinSession. This will also discard other session-related data, and an accidentally opened browser tab will lose its permissions. The next time it is accessed, a new, unauthenticated session is established and guided through the login flow.

This tight integration of MainLayout and SessionAuth creates a robust, yet manageable, logout implementation. The header provides a clearly visible exit from the admin context, and the session logic ensures this exit is implemented consistently from a technical standpoint.

In the next chapter, we will take a closer look at the SessionAuth class and examine its role in interacting with route protection and the login view within the overall application flow.

SessionAuth: Session-based authentication in the overall flow

Now that login protection, the login page, and the logout function have been introduced, a central question remains: Where is it actually recorded whether a user is authenticated? The answer lies in the small but crucial helper class SessionAuth, which anchors the authentication state in VaadinSession.

At its core, SessionAuth fulfils three tasks. It can first check whether a current session is already registered. Second, after successful login, it can mark the session as authenticated. And thirdly, it can remove the authentication status when logging out and close the session. These three operations form the basis of the login view, route protection in the MainLayout, and the logout button in the header, all of which access a standard truth value rather than maintaining their own state.

The chosen approach leverages the fact that Vaadin maintains a separate VaadinSession for each user. This session exists on the server side and is therefore less susceptible to client-side manipulation than, for example, a simple browser flag. By setting a dedicated attribute – such as “authenticated” – in this session, SessionAuth clearly links the login state to the current browser session. If the same user opens a new browser window, a new session is created; the user is not logged in again.

In the interaction between components, this results in a precise flow: if a user reaches the login page and enters a correct password, LoginView calls SessionAuth.markAuthenticated() after a successful check. It then navigates to the administrative overview. As soon as the user opens additional views from there, the MainLayout checks whether the session is still considered logged in, as part of the route protection, using SessionAuth.isAuthenticated(). If this is the case, navigation is allowed. Otherwise, the request will be redirected to the login page.

Finally, if the user clicks the logout button in the header, SessionAuth.clearAuthentication() removes the authentication attribute from the session and closes VaadinSession. For the server, this session is over. The following request establishes a new session, which is again considered unauthenticated from the application’s perspective, so Route Protection redirects the request back to the login view.

This session approach fits well with the project’s objective: it is deliberately kept simple, requires no additional infrastructure and is easy to understand. At the same time, it meets the requirements of a minimalist admin login, which is less about highly secured, distributed authentication procedures and more about a clear separation between “logged in” and “not logged in” within a running browser session.

In the rest of this chapter, we will go through the implementation of the three methods isAuthenticated, markAuthenticated and clearAuthentication and look at how they interact at the different points in the code – Login-View, MainLayout and Logout-Button.

The implementation of SessionAuth in detail

The SessionAuth class is deliberately kept compact. It encapsulates access to the VaadinSession and provides three static methods, each of which fulfils a clearly defined task:

isAuthenticated() – Check if a session is logged in

The isAuthenticated() method is the primary read-only interface. It is used, among other things, in the MainLayout to decide whether navigation should be allowed or redirected to the login view as part of route protection. Internally, she first asks about the current VaadinSession. If no session exists (for example, in the very early stages of a request), it returns false immediately.

If a session exists, the “authenticated” attribute is read. It is a simple object value set to “Boolean.TRUE” upon successful login. The method compares this value to TRUE and returns true or false accordingly. The additional log entry helps track how often and with what results this check is invoked in the running system.

markAuthenticated() – Mark session as logged in

After a successful password check in the LoginView, markAuthenticated() is called. The method fetches the current VaadinSession and sets the “authenticated" attribute to TRUE. This updates the session state so that subsequent calls to isAuthenticated() return true for that session.

Here, too, a log entry ensures that the registration time remains traceable. If unexpected conditions occur in the company – for example, because users report that they are “suddenly logged out again” – the logs can be used to understand better when sessions were marked or discarded.

clearAuthentication() – log out and close session

The third method, clearAuthentication(), is the counterpart to login. The logout button in the MainLayout invokes it and performs two tasks simultaneously. First, it removes the "authenticated" attribute from the current session by setting it to null. This means isAuthenticated() will return false for this session from this point on.

In the second step, “session.close()" is called. This invalidates the entire VaadinSession, including any other attributes that may have been set. This measure ensures that other session-related information is not forwarded and that a new request actually starts with a fresh session.

In combination with the explicit navigation back to the login page, this results in a clean logout flow: the session loses its authorisation, the user leaves the admin context, and must authenticate again to continue.

Interaction with Login View and MainLayout

In the overall flow of the application, SessionAuth is  used in several places:

  • In the LoginView, the attemptLogin() method calls SessionAuth.markAuthenticated() after a successful password comparison and then navigates to the overview page.
  • In the MainLayout, the beforeEnter() implementation uses SessionAuth.isAuthenticated() to intercept unauthorised access and, if necessary, redirect to the login view.
  • The logout button in the header calls SessionAuth.clearAuthentication() and then redirects the user out of the administration context to the login page.

In this way, SessionAuth centralises all access to the authentication status in a single place. Changes to the session model – such as a different attribute name or additional metadata – are made here and are then consistently available to all consuming components.

Conclusion & Outlook

With the introduction of a simple, configurable admin login, the application gains a clearly defined protection mechanism for all administrative functions without the complexity of a full-fledged authentication framework. The solution is deliberately tailored to the project’s characteristics: lightweight, comprehensible, and implemented using pure Java/Vaadin architecture. At the same time, it covers the most essential requirements for minimal access control – from password requests to route protection to a clean logout mechanism.

The overall system consists of a few clearly separated building blocks: a central configuration file, the LoginConfig for evaluating these values, a simple login view for user interaction, route protection in the MainLayout, and the SessionAuth class for managing session state. These components interlock like gears, creating a process that remains technically sound and intuitive for users.

At the same time, the implementation is deliberately extensible. The current approach stores the password in plain text in the configuration file and uses it, unchanged, as a byte sequence within the application. This is sufficient for development and internal scenarios, but it is clear that it is not suitable for higher security requirements. However, the architecture leaves room for the following points of expansion:

  • Password hashing: The stored passwords are hashed using a hash function such as SHA-256 or bcrypt, so they are not stored in plaintext on the server.
  • Multiple users or roles: The structure could be expanded to support various administrators with individual passwords.
  • Time-limited sessions: An automatic logout due to inactivity would further enhance security.
  • Two-Factor Authentication (2FA): For more demanding environments, a second level of security – for example via TOTP – could be added.
  • External identity providers: The application could be connected to OAuth 2.0/OpenID Connect in the long term, provided it fits the application scenario.

However, the strength of the solution lies precisely in its not attempting to anticipate these aspects. It provides a pragmatic, ready-to-use foundation that reliably protects the admin area from unintended access without unnecessarily complicating deployment or development.

Cheers Sven

« Older Entries