Category Archives: Serverside

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

Advent Calendar 2025 – Basic Login Solution – Part 1

Introduction

The administration interface of a URL shortener is a sensitive area where short links can be changed, removed, or assigned expiration dates. Although the system is often operated on an internal server or in a private environment, protecting this management interface remains a fundamental security concern. Accidental access by unauthorised users can not only lead to incorrect forwarding or data loss, but also undermine trust in the overall system.

With the introduction of a configurable login mechanism, precise access control is introduced, deliberately separating access to management functions from the rest of the system. The login serves as a lightweight security measure that does not require external dependencies, frameworks or time-consuming user management. It is precisely this simplicity – a single password, a simple configuration file and a centred login screen – that makes the solution particularly attractive for small deployments or personal projects.

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

A login page for an admin interface, featuring a password field and a login button. The header reads 'Admin Login' with an instruction below prompting the user to enter the administrator password.
Screenshot of the URL shortener overview page, displaying a list of created short links with options to manage them including actions like viewing, deleting, and filtering by URL.
Logo and navigation bar of EclipseStore showing items and logout option.

Objectives of the implementation

The introduction of a simple, configurable login mechanism aims to achieve several clearly defined goals that improve both the security and usability of the URL shortener. The focus is not on establishing a complex user and role model, but on creating a pragmatic protective layer that reliably controls administrative access without unnecessarily increasing development or operating costs.

First, the primary objective is to secure administrative functions. The management interface allows you to create, edit, and remove short links, as well as set or clean expiration dates. These functions must be protected against unauthorised access, particularly if the URL shortener is not operated exclusively within the closed network. A simple login prevents accidental or curious access from causing damage.

Another goal is to minimise the barrier to entry. The solution should work without additional frameworks, external identity providers, or complex configuration. The implementation is deliberately based on the project’s philosophy: Everything remains manageable, lightweight, and understandable. With a simple auth.properties file, the system can be turned on or off at will, and a single password suffices for access control.

In addition, the login mechanism is designed to ensure a predictable and consistent user experience. These include a well-designed login page, an immediate redirect on unauthenticated access to protected areas, and a transparent logout process that cleanly ends the session. All these aspects ensure that the login fits naturally into the existing UI and is still clearly perceived as a security measure.

Finally, the login also serves as a foundation for potential future expansions. Although the current implementation is deliberately minimalist, it provides the structural prerequisites to respond to stronger security requirements if necessary—for example, by adding hashing mechanisms, time-limited session tokens, or server-side password rotation. This leaves the system open for further development without losing its current simplicity.

Configurable login via auth.properties

The new login mechanism is based on a deliberately simple configuration file that enables centralised control of the application’s authentication behaviour. This file is named auth.properties and is located in the application’s resource directory. By using them, the login system can not only be conveniently activated or deactivated, but also quickly adapted to different deployment scenarios.

Flowchart illustrating the evaluation process of login configuration state, detailing the steps and conditions that lead to effective protection states.

The focus is on two configuration keys: login.enabled and login.password. While the first parameter checks whether the login system is active, the second parameter specifies the required access password. These values are automatically read when the application starts and henceforth determine the behaviour of all protected areas. It is precisely this mechanism that enables turning off login on short notice or changing the password without modifying the source code again.

The configuration is read in by the LoginConfigInitializer, which is executed when the servlet container starts. It checks whether the file exists, loads its contents and passes it to the central class LoginConfig, which manages these values and makes them available for later access.

This class is responsible for correctly configuring the application’s login behaviour at startup. This ensures that both the login page and the route protection can access a uniform configuration basis at all times.

After loading the file, the LoginConfig class assumes responsibility for persistently storing the values and performing password comparisons.

This form of configuration control not only simplifies operation but also supports different operating modes. For example, a fully disabled login system is suitable for purely internal development environments, while the activated mode protects against unauthorised changes in production or publicly available environments. Switching between the two modes is kept as low-threshold as possible – a simple value in a text file is enough.

Although this mechanism provides basic protection, it is not designed to be a highly secure authentication solution. Instead, the goal is to establish a minimum level of security to protect the administrative area from accidental access or unauthorised use. For productive environments with increased security requirements, additional measures such as password hashing, multi-factor authentication, or integration into established identity services would be necessary.

LoginConfig & Initialization

Central control of login behaviour is based on two closely interlinked components: the LoginConfig class itself and its upstream initialiser, LoginConfigInitializer. Together, they ensure that the configuration from the auth.properties file is correctly read, interpreted, and made available to the application runtime.

The first focus is on the LoginConfig class. It provides a minimalist yet coherent foundation for the login system. The approach is intentionally simple: there is no user base, roles, or profiles, just a single password that serves as an access threshold. The class manages this password and the information on whether the login should be active. The structure remains manageable to keep the entry barrier for administrators and developers low.

An essential detail is the division into two phases: first, when the application starts, the LoginConfigInitializer checks which settings are stored in the configuration file. These values are then passed to the static LoginConfig.initialise() method, which populates the appropriate fields and makes them available to the rest of the application.

This initialisation process occurs entirely when the servlet container starts. This ensures that all views loaded later, especially the route protection and the login page, have access to a consistent and fully initialised configuration. This avoids error conditions that could arise from missing or delayed loading of configuration values.

In the following, we will take a closer look at both components and refer to the source code for clarity.

LoginConfig – central configuration class

The core is the LoginConfig class, which stores the login switch and the expected password as byte data. It is  declared final and has a private constructor, so it is used exclusively through its static methods:

The initialise method is called only when the initialiser starts and determines whether login is enabled (loginEnabled) and whether a valid password is present. Invalid or empty passwords are consistently discarded; isLoginConfigured() only returns true if there is a usable configuration.

For the actual password comparison, matches(char[] enteredPassword) serves as the central function. It takes the entered password as a char[], converts it to a byte array in UTF-8 format, and compares it with the expected byte sequence using MessageDigest.isEqual. This enables constant-time comparison, making simple timing attacks more difficult. The temporary byte array is then overwritten using Arrays.fill to remove the sensitive data from memory, at least as effectively as possible.

LoginConfigInitializer – Load configuration on startup

For LoginConfig to work with meaningful values, the configuration file must be read when the servlet container starts. This task is performed by the LoginConfigInitializer class, which  is registered  as @WebListener and implements the ServletContextListener interface  :

The initialiser follows a straightforward process: first, it attempts to load the auth.properties file from the classpath. If this is not possible, the login is explicitly deactivated, and a warning log is written. If successful, the configuration values “login.enabled" and “login.password" are read, converted to appropriate data types, and forwarded to LoginConfig.initialise. Finally, depending on the configuration, additional log output is generated, providing quick information about the active mode during operation.

This separation of responsibilities creates a well-traceable initialisation path: LoginConfigInitializer handles loading and interpreting the configuration file. At the same time, LoginConfig encapsulates the actual login logic and is later used by other components, such as the login view or route protection.

The new login page

With the introduction of admin login, the application gains its own entry page that deliberately distinguishes itself from the rest of the UI. The new login page serves as the boundary between public functions, such as link forwarding, and sensitive administrative functions in the backend. The goal was to create a reduced, highly focused layout that integrates seamlessly with the application’s visual design while remaining clearly recognisable as a security barrier.

Flowchart depicting the login validation process for a URL shortener admin interface, highlighting user requests, authentication checks, password validation, and potential outcomes.

Instead of being embedded in the existing navigation layout, the login is rendered as a standalone view without MainLayout. Users will not see any page navigation, drawer or admin functions until they have successfully authenticated. The page fills the entire browser window; its contents are centred vertically and horizontally. This creates the impression of a classic login screen with a single task: requesting the password for the administrative area.

At the centre is a compact login panel comprising a headline, a short explanatory text, a password field, and a login button. The headline clearly conveys that this is admin access, while the accompanying text explains why a password is required and what area it protects. This improves transparency and reduces queries, especially when multiple users use the system.

The password field is configured to automatically receive focus when the page is loaded. Users can therefore type directly without first clicking with the mouse. In addition, a clear button lets you remove an accidentally entered string with a single click. Displaying the password in plain text is deliberately avoided to reduce the risk of shoulder-surfing, especially in shared work environments.

The input behaviour is also designed to ensure a smooth process. Authentication can be done either by clicking on the login button or by pressing Enter. If the login fails, the view marks the password field as invalid and displays a clear error message indicating that the entered password is incorrect. This preserves context and allows the user to start a second attempt immediately without reloading the page.

Implementation of the LoginView

The technical implementation of the described login page is handled by the LoginView class. It is registered as a separate route and deliberately dispenses with a layout to enable a focused, full-surface login screen:

It is already evident from the declaration that LoginView is not embedded in MainLayout. Because the route annotation lacks a layout, the page is displayed in isolation; the layout properties in the constructor handle complete vertical and horizontal centring. The two central UI components – password field and login button – are kept as fields so that they can be further configured in the helper methods configureForm() and buildLayout().

The configuration of the form is deliberately designed for a lean but fluid user experience:

The password field automatically gains focus when the page loads and is set to a fixed width, so that the input and button visually form a single unit. The Clear button lets you quickly delete an incorrect entry; the password is displayed in plain text by default. The login button is marked as the primary button and, when pressed, triggers the attemptLogin() method. The value change listener ensures that a previously set error state is restored when typing again.

The structure of the panel that appears in the middle of the screen is defined in buildLayout():

The combination of the heading, explanatory paragraph, password field, and button forms exactly matches the compact login panel described in the previous section. Centring the inner VerticalLayout creates a clear focus on the input area, without visual distractions from other UI elements.

The actual login process is encapsulated in attemptLogin(). This is where configuration and user input are merged:

First, it is checked whether the login is enabled in the configuration. If this is not the case, the user receives clear feedback via notification and is redirected directly to the overview page. The next step is to check whether the login has been configured correctly. Only when both conditions are met is the entered password passed to LoginConfig.matches. If successful, the session is marked as authenticated via SessionAuth.markAuthenticated() and forwarded to the overview. In the event of an error, the view marks the password field as invalid and displays a clear error message – the user remains on the login page and can adjust the input.

Finally, the implementation of the BeforeEnterObserver ensures that the login page itself only remains visible when it makes sense:

This prevents authenticated users from being redirected to the login page again and ensures that a globally deactivated login system does not unnecessarily display a redundant password dialogue.

Another aspect is the interaction with the configuration. If login in the auth.properties file is disabled or not configured meaningfully, the view displays clear instructions. In one case, it indicates that login is currently disabled and redirects directly to the overview page. In the other case, it suggests that a valid configuration could not be loaded. The page thus serves not only as an input form, but also as a diagnostic point where misconfigurations become visible at an early stage.

Flowchart depicting the authentication process for a password login system, including steps for validation, password comparison, and return of authentication result.

Overall, the new login page ensures that access to the admin interface is structured, predictable and visually clearly distinguished from the rest of the system. It combines a deliberately simple interaction with a comprehensible security concept, thereby laying the foundation for the other building blocks of the login flow, particularly route protection and session management.

Cheers Sven

Advent Calendar 2025 – Mass Grid Operations – Part 2

What has happened so far…

In the previous part, the URL shortener overview was significantly expanded. The starting point was the realisation that the last UI was heavily optimised for single operations and thus quickly reached its limits as soon as larger volumes of shortlinks needed to be managed. To resolve this bottleneck, the grid was consistently switched to multiple selection, creating the technical basis for actual mass operations.

Based on this, a context-dependent bulk action bar was created, visible only when a selection is present. It serves as a central control for simultaneous multi-entry actions and seamlessly integrates them into the existing search, filtering, and grid interaction workflow. The decisive factor was not the mere addition of new buttons, but the clean coupling of UI state, selection and action permission.

The first concrete mass operation was the bulk delete. This function exemplified how efficiency and security can be combined: through explicit confirmation dialogues, meaningful previews of the affected entries, consistent feedback, and clear visual cues for destructive actions. This workflow has been supplemented with keyboard shortcuts that speed up deleting larger quantities without bypassing the control mechanisms.

This completes the transition from an entry-centric interface to a professional management view. The overview view now understands selections as a work context and can execute coordinated actions in a controlled manner. The following sections build on this foundation, examining further mass operations, their semantic differences, and their technical implementation in detail.

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-08

Overview interface of a URL shortener tool showing a list of short links, with details such as 'Shortcode', 'URL', 'Created', 'Expires', and action buttons.
Overview of the URL shortener interface, displaying a grid of short links with options to delete selected links or set expiration dates.

Bulk Clear Expiry: Reliably and Consistently Remove Expiration Dates

While setting a uniform expiration date is a typical administrative task, the targeted removal of such expiration dates also plays a central role. In many real-world scenarios, temporary restrictions lose their relevance: links should remain valid permanently, test or campaign parameters must be reset, or previously set durations have proven too restrictive. The ability to delete expiration dates for many shortlinks in a single step is, therefore, a crucial building block for effective mass management.

An overview screen of a URL shortener interface displaying a list of short links with selected options for managing their expiration dates, including a dialogue box asking for confirmation to remove expiration for two selected links.

The bulk clear expiry function follows a similar interaction pattern to setting an expiration date. Still, it is fundamentally different in its consistency and semantics: while setting introduces a new value, deleting an expiration date explicitly returns to the “does not automatically expire” state. The architecture must ensure that this state is clearly communicated and implemented correctly.

The removal of the expiration time must be understood explicitly, not as an implicit side effect, but as a deliberate operation that requires its own confirmation. This ensures both the security of user interactions and the traceability of changes.

The focus is on the interaction among dialogue logic, API calls, and the changed semantics of the edit endpoint, which now distinguishes between “set new expiration date” and “delete expiration date”. This makes the interaction between the UI and the backend clearly comprehensible and demonstrates how reliable mass changes can be integrated cleanly.

The entry point of the functionality lies in the method confirmBulkClearExpirySelected(), which – analogous to the bulk delete – first validates the current selection and then opens a confirmation dialogue:

The structure of this method makes it clear that removing the expiration time is modelled as a standalone operation that requires confirmation. First, an empty selection is excluded; then a dialogue is created whose title explicitly names both the operation type (“Remove expiry”) and the number of affected entries. The actual content of the dialogue explains the consequence of the action in plain language: The links “will no longer expire automatically”. This means that semantics are conveyed not only implicitly by the UI but also explicitly by language.

The actual implementation of the mass operation is carried out in the outsourced method bulkClearExpiry(...), which encapsulates the transition from the UI layer to the client API:

The signature of the edit call is particularly remarkable. In contrast to the bulk set expiry, only the shortcode and original URL are transferred; the instant and shortcode are not. This API is explicitly implemented in the URLShortenerClient in such a way that the absence of an expiration date is semantically interpreted as “delete expiry”:

Finally, on the server side, this semantics is anchored in the store implementations. Both the in-memory store and the EclipseStore-based persistence layer now interpret a null value for expiredAt as a signal to remove the expiration time entirely:

Instead of replacing a missing value with the previous expiration date, expiredAt is now consistently converted to Optional.ofNullable(…). The result is a clear division of semantics: if an instant is passed, the edit operation sets a new expiration date;  if null is passed, an empty optional is created, which corresponds to a complete removal of the existing expiration date. This change enables bulkClearExpiry(…) not only to overwrite expiry information covertly but also to delete it.

The combination of a confirmation dialogue, dedicated mass operation, and precisely defined edit semantics creates a continuous path from the user click to the permanently changed data structure. Bulk-Clear-Expiry is not just a “special case” of the edit function, but a deliberately modelled, independent mass operation with clear, technically and professionally comprehensible meaning.

Keyboard interaction as an accelerator for mass operations

With the introduction of mass grid operations, efficient interaction is becoming increasingly important. While mouse gestures and explicit buttons offer high visibility and explainability, keyboard shortcuts are most effective when users perform recurring actions. In the context of the Overview view, this means that central measurement operations should not only be accessible via the bulk bar but also be able to be triggered directly via the keyboard.

Keyboard interaction fulfils two roles. On the one hand, it serves as a direct accelerator of already established workflows. If you are used to working with keyboard shortcuts, you can trigger searches and deletions without detours via the mouse, reducing noticeable friction, especially with frequent context switches between code, console, and browser. On the other hand, it serves as a semantic condensation of the UI: the presence of a shortcut makes it clear that certain operations are not treated as edge cases but as primary interaction paths.

Today, this idea is being fleshed out in two prominent places. On the one hand, the global search in the overview view has a dedicated shortcut, allowing you to focus directly on the search field. This starts at the interaction’s beginning: a single keyboard shortcut is enough to switch from the web interface’s arbitrary state to a precisely defined search mode. On the other hand, the bulk delete operation is bound to the delete key only when a selection is present in the grid. The keyboard thus serves as an equal trigger for the same confirmation dialogue, which would otherwise be accessible only via the bulk bar.

It is essential for this integration that the keyboard shortcuts are not implemented as hidden, hard-to-discover functions, but as a consistent extension of the existing interaction model. They draw on concepts that have already been introduced – global search, multiple selection, bulk dialogues – and link them to a fluid, operable overall system. If you work exclusively with the mouse, you can use all functions as usual; those who prefer keyboard shortcuts, on the other hand, get much faster access to the same function space.

In the following, we will show how these shortcuts are technically integrated, how they interact with the grid state model, and which protective mechanisms prevent mass operations from being unintentionally triggered.

The central entry point is the addShortCuts() method, which bundles all global keyboard shortcuts of the Overview view:

The method defines two keyboard interactions, each of which fulfils different semantic roles:

Meta + K – Focus on the global search field

The combination of KeyModifier.META (⌘ on macOS, Windows logo key on Windows) and the letter K address an established UI pattern in code editors and command palettes. With a single keystroke, the user can go directly to the global search, regardless of which UI element was previously in focus.

The protection provided by the following conditions is remarkable:

This prevents the shortcut from forcing an interaction that would not make sense semantically in the current UI state – for example, if a dialogue is currently open or the search has been deactivated due to active filter modes. The keyboard interaction thus fits respectfully into the overall system.

Delete – Trigger bulk delete if there is a selection

The second central key combination binds the delete key directly to the bulk delete function. The shortcut applies to the entire overview view, but is only activated if there is actually a multiple or single selection:

This ensures that the delete dialogue appears only when there is an object context; therefore, it is not possible to trigger an operation without a target by randomly pressing Delete. This protection mechanism is essential for mass operations, as the delete key is often used unconsciously and intuitively.

Integration into the UI‑state model

Both shortcuts interfere with the grid’s state logic. The search shortcut uses the activation logic of the globalSearch field, while the delete shortcut is explicitly bound to the grid selection. This creates a natural coupling between keyboard interaction and UI state: Shortcuts are not detached from the UI, but follow the same semantic structure as mouse interaction via the bulk bar.

Why it matters: Acceleration without side effects

While shortcuts are often seen as optional conveniences, they play a crucial role in mass operations. Those who manage large amounts of data frequently switch between different tools – IDE, browser, and terminal. The ability to perform central operations without loss of focus and without visual navigation reduces cognitive load and increases speed. At the same time, the built-in protection queries ensure that no unintentional deletion actions are triggered.

This shows that keyboard interaction is not just a nice extra feature, but an integral part of an efficient, error-robust workflow for mass grid operations, how these shortcuts are integrated into the overview view, how they interact with the state model of the grid, and where protection mechanisms have been deliberately implemented to prevent unintentional mass operations.

A consistent visual language for high-risk mass operations

With the introduction of mass operations, both the functional and visual levels become more complex. Where previously individual actions were placed in isolation in the grid, there are now interconnected chains of interactions that have the potential to have a significant impact on the database – especially in operations such as deleting several shortlinks or changing all expiration dates. This increased impact requires a visual language that creates clarity, clearly highlights risks and helps users to perform each operation consciously and safely.

An essential goal of this visual language is to establish clear semantic distinctions among action types. While harmless operations – such as opening a detailed dialogue – may be unagitated and neutral, risky or destructive actions must be immediately recognisable as such. The introduction of the bulk action bar makes this differentiation imperative: it bundles both low-risk and high-risk actions in a small space, creating visual clarity, a prerequisite for safe usability.

The implementation of these principles follows a strict pattern: by targeting Lumo theme variants, buttons are not only styled but also semantically enhanced. Destructive actions are consistently marked with LUMO_ERROR, whereas accompanying or secondary actions are given the more subtle LUMO_TERTIARY_INLINE. This contrast creates an immediately apparent hierarchy, highlighting potentially dangerous actions without visually cluttering the interface.

The technical basis of this visual language is first evident in the implementation of the bulk action bar. A clear distinction is made here between destructive and non-destructive actions. The corresponding code from the OverviewView clearly shows this:

Only the Delete button bears the LUMO_ERROR mark. This signals at a glance that the action could have irreversible effects. The other two buttons – “Set expiry” and “Clear expiry” – deliberately remain inconspicuous. Their functions are operational, but not destructive, which is why LUMO_TERTIARY_INLINE offers the appropriate visual restraint here.

This differentiation also continues at the row level in the grid. Each row has two buttons that play completely different roles visually:

The delete button is red, making it immediately recognisable. The corresponding detail button is visually restrained:

The user immediately recognises which of the two actions is inconsequential and which is critical. This visual semantics is not cosmetic; it reduces misclicks and improves security, especially in fast workflows. This logic continues in the confirmation dialogues. In the case of bulk delete, the confirmation button is marked twice:

LUMO_PRIMARY marks the main action in the dialogue, LUMO_ERROR immediately makes it clear that this main action is destructive. The red colouring was deliberately chosen to increase attention and prevent accidental confirmations.

However, different semantics apply when removing an expiration date. The process is a mass operation, but not destructive. Therefore, the application explicitly does not use a red marking:

Here, only the primary theme is set – the action is essential, but not dangerous. The UI thus distinguishes between critical and relevant, which is crucial for intuitive usability.

In summary, the consistent use of Lumo themes creates a clear, recognisable hierarchy:

  • LUMO_ERROR → destructive, potentially irreversible
  • LUMO_PRIMARY → central affirmative action
  • LUMO_TERTIARY / LUMO_TERTIARY_INLINE → harmless or accompanying actions

This visual grammar helps users recognise the seriousness of an action without reading the text. It is precisely this clarity that is indispensable when operations are applied to many data sets rather than a single one.

Conclusion: From individual case to professional mass editing

With today’s update, the URL shortener’s overview view reaches a new level of functionality. What was previously a tool for individual case management is now developing into a full-fledged interface for professional mass editing. The fundamental expansion consists not only in the introduction of several bulk operations, but above all in how these operations are embedded within a consistent operating concept.

In the previous chapters, it became apparent that the introduction of multiple selection, bulk bar, dialogue mechanisms and a finely graded visual language is not a loose juxtaposition of individual features. Instead, all elements are intertwined: the search structures the data space, the selection defines the area of action, the bulk bar makes the mass operations visible, and the dialogues ensure conscious, comprehensible decisions. Finally, visual semantics provide orientation and support safe operation even in fast or routine workflows.

This combination of architecture and interaction design provides the foundation for functions that can be added in future expansion stages, including automated workflows, grouped changes, role-based sharing, and even domain-specific mass transformations. These changes thus not only provide a feature package but also the foundation for a productive, reliable, and extensible mass-operations architecture.

Cheers Sven

Advent Calendar 2025 – Mass Grid Operations – Part 1

The next stage of the Advent calendar focuses on further development, which becomes immediately noticeable once more shortlinks are used. The previous interaction patterns in the Overview view were geared toward individual operations and yielded only limited efficiency gains when processing many objects simultaneously. Today, this paradigm is being deliberately broken up and replaced by an interplay of new UI concepts that, for the first time, enable true multi-operation, context-sensitive work, and a much more stringent user interface.

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-08

A screenshot of a URL shortener overview dashboard, displaying a list of shortened URLs with corresponding shortcodes, creation dates, and expiration status. The interface includes search functionality, pagination controls, and action buttons for managing entries.
Overview of the URL Shortener application displaying selected shortlinks with options for deleting, setting expiration dates, and clearing expirations.

The changes focus on three aspects: first, the introduction of a valid multiple selection, which was previously missing and now forms the basis for all new mass operations. Second, the contextual action bar, which appears only when relevant and blends seamlessly into the workflow. Third, a visibly improved depth of interaction, which is reflected in consistent visual cues, reduced refresh cycles, and the ability to perform more complex processing steps – such as setting or removing expiration dates – for any number of entries at the same time.

These improvements should not be viewed as a collection of isolated features, but rather as a coordinated step toward a more mature UI architecture. They address a common issue in production environments: users manage not just one but dozens or hundreds of shortlinks. Any optimisation that reduces friction losses here directly affects work speed and quality. This is exactly where this step has its impact: the application is not only faster to use but also more tailored to professional use cases.

Multiple selection as the foundation for mass grid operations

The transition from a single to a multiple selection marks a fundamental change in the interaction logic of the Overview view. Until now, working with shortlinks has been limited to isolated operations that always followed the same sequence: an entry was selected, edited, confirmed, and the process started over again. This structure was functional but proved to be a stumbling block when handling a larger number of entries in real-world applications.

With the introduction of multiple selection, this bottleneck will be resolved. The grid transforms from a linear list into a workspace where various objects can be brought into focus simultaneously. This capability is not only an ergonomic improvement, but also forms the technical and conceptual foundation for all subsequent mass operations. Only the possibility of marking any number of shortlinks at the same time makes context-sensitive work possible at all.

The technical anchoring of this multiple selection is initially done at a very inconspicuous point in the grid configuration. The overview view explicitly switches from single to multi-selection mode, which means that the grid is internally able to keep several entries as selected in parallel:

With this change, the Vaadin grid’s selection mechanism will be expanded from the ground up. While in single-selection mode, the focus is technically always on exactly one entry in Grid.SelectionMode.MULTI allows multiple selections simultaneously. The basic interaction pattern mustn’t change for the user: the choice is still made via familiar gestures, such as clicks, supplemented by keyboard input as needed. The difference lies in the underlying representation: the grid now manages many marked shortlinks rather than a single active element.

To ensure that this multiple selection is not only visually effective, but also functionally reflected, it is integrated into the rest of the interface via a selection listener:

Here, the abstract multiple selection is converted into a concrete UI state. The getAllSelectedItems() method returns the quantity of shortlinks currently marked in the grid. From this, a boolean aggregate state hasSelection is derived, which controls whether the downstream bulk bar is visible and whether the associated action buttons are enabled. At the same time, the selection size provides immediate feedback to the user: the number and context of the selection are summarised in a text component that makes the current page and the number of marked links visible. In this way, multiple-choice is not only managed internally but also translated into a clearly traceable, state-based interaction.

Of particular relevance is the fact that multiple selection has been seamlessly integrated into the existing UI model. The choice is made via established patterns, such as checkbox clicks or keyboard shortcuts, so users can work intuitively and productively immediately without adapting. At the same time, the visual feedback remains precise: selected rows are clearly highlighted, and changes in the selection immediately update the subsequent contextual UI components.

Thus, multiple selection provides the necessary foundation for bulk operations, such as deleting, setting or removing expiration times, and other future functions, to be implemented securely and consistently. It is the architectural pivot point for transitioning a purely entry-oriented tool into a scalable management tool for professional use cases.

The bulk action bar is a context-sensitive control centre.

With the introduction of multiple selection, you can, for the first time, edit multiple short links at once. However, this capability alone is insufficient to establish a consistent, high-ergonomics mass-machining process. Only through the bulk action bar, which dynamically appears or disappears based on the current selection, does the overview view become a true hub for mass operations.

The central idea of this bar is that tools become visible only when they are semantically relevant. As long as no entry is selected, the interface remains tidy and slim. However, as soon as one or more rows have been selected, the function space opens for all operations that refer to a set of objects. This dynamic follows the principle of context-sensitive compression: the UI does not present all possible actions simultaneously, but only those that are meaningful and feasible at the current task.

In addition, the bulk bar is not only a visual addition but also has a clearly structured internal logic, which is already reflected at the field level of the view. In the overview view, the essential UI building blocks for mass operations are explicitly declared as separate components:

This structure makes it clear that the bulk bar is treated as an independent UI element with its own buttons and a separate information component. The actual integration takes place immediately after the search bar, so that the mass actions are visually and functionally in proximity to global navigation and filtering:

In this way, a vertical structure is created in which search, bulk bar, and grid deliberately follow one another: First, the data space is filtered; then the selected elements are contextualised via the bulk bar before the detailed interaction takes place in the grid.

The behaviour of the bulk bar itself is encapsulated in a dedicated factory method that defines both the visual appearance and the initial functional state:

In this method, the semantic roles of the individual elements are concretised. The theme variants particularly highlight the delete button, which is provided with an error theme and thus clearly distinguishes itself from less risky operations. The tooltips also enhance the explainability of actions by precisely describing each button’s purpose. All buttons start in a deactivated state; only the selection in the grid – as shown in the previous chapter – unlocks it in a targeted manner.

The layout definition makes it clear that the bulk bar is intended to be a full-width, horizontally aligned control centre. The selectionInfo area is slightly smaller but serves as a permanent context anchor that shows the user which mass scenario they are currently in. The visibility of the entire bar is initially set to false and controlled solely by the grid’s selection state. This ensures the bulk bar is visible only when it is needed. It first provides precise information on the selection status by displaying the number of selected shortlinks and the current page. Subsequently, the buttons that can be executed on this basis are unlocked. This mechanism prevents operational errors and ensures the user immediately recognises which actions can currently be performed.

Bulk Delete: Secure and scalable deletion operations in the grid

With the transition to mass operations, deleting multiple shortlinks simultaneously is a key use case. The previous UI was clearly designed for deleting individual entries, which was insufficient for many professional use cases. Especially when it comes to administrative activities, migrations, or cleaning test data, there is a natural need to remove many entries in a single step. This is precisely where the new bulk delete function comes in.

The key requirement for this function is to balance two aspects: on the one hand, maximum efficiency when handling large volumes of entries, and on the other, high robustness against unintentional or incorrect entries. The system should work quickly, but never delete data carelessly. The implementation, therefore, follows a two-stage interaction pattern, deliberately designed to enable mass processing while ensuring the necessary operational safety.

The bulk delete always starts with the multiple selection in the grid. Once many shortlinks have been marked, the bulk action bar is activated, and the “Delete selected links…” option becomes available. Only when this action is deliberately chosen does a separate confirmation dialogue open. It does more than ask a simple query; it displays sample shortcodes of the selected entries, making the deletion process transparent and verifiable for the user. This preview is essential for larger datasets, as it helps validate the selection’s accuracy without reading the entire list of selected items.

Confirmation dialog for deleting selected short links, showing two selected items with their shortcodes and URLs, along with options to delete or cancel.

The confirmation pattern is designed to trigger the deletion process in a targeted manner without interrupting the workflow. Once confirmed, all highlighted shortlinks will be deleted one by one via the client API. The architecture supports both parallel and sequential deletion patterns; the current implementation uses a sequential approach to capture errors and report them to the UI precisely. Each deletion operation is evaluated, and the user receives a summary of the results that clearly documents both successful and failed deletions.

This combination of efficiency, security and transparency makes bulk delete a robust tool in a professional context. In the following, the relevant source texts are used to concretise and explain in detail how dialogue logic, API integration and feedback mechanisms interlock to realise a precisely controlled deletion process.

The entry point for bulk deletion is the confirmBulkDeleteSelected() method in the Overview view. It integrates the grid’s current multi-selection with the dialogue logic. It prepares both the preview of the entries to be deleted and the subsequent execution of the delete operations:

The first block already clarifies the implementation’s security concept. If, contrary to expectations, no selection is made, the process will be aborted with an explicit notification. The actual dialogue instance receives a header that explicitly shows the number of affected shortlinks, thereby clarifying the scope of the action.

Particularly interesting is how the preview of the entries to be deleted is generated. From the number of selected shortlinks, a sorted list of your shortcodes is created, which is limited to a maximum of five examples. This limitation ensures that the dialogue remains readable even with huge selections, while an appended ellipsis makes it clear that other elements are affected. This creates a concise but meaningful presentation of the selection.

Finally, the inner Confirm handler performs the actual deletion process. For each shortlink selected, the client API of the URL shortener backend is called via urlShortenerClient.delete(m.shortCode()). Successful and failed operations are counted; Exceptions are explicitly logged and taken into account as errors in the count. This count serves as the basis for the final user notification, which summarises the number of successfully deleted and failed entries.

After the loop completes, the dialogue is closed, the grid selection is cancelled, and a consistent update of the displayed data is triggered via safeRefresh(). The final notification provides precise, aggregated feedback on the operation outcome, thereby completing the interaction cycle of selection, confirmation, and presentation of results.

In addition, the bulk delete function can also be operated via the keyboard. The user can use a global shortcut to trigger the delete dialogue directly, provided that a selection has been made:

This integrates the bulk delete into the everyday workflow: pressing the Delete key triggers the same confirmation dialogue as clicking the corresponding button in the bulk bar. Mouse and keyboard interaction thus yields the same result, significantly speeding up operations for experienced users without compromising the security of the operation.

Bulk Set Expiry: Uniform expiration dates for multiple shortlinks

With the possibility of selecting several entries at the same time, it is possible for the first time to make even more complex metadata changes in a single step. One of the most functionally essential operations in this context is setting uniform expiration dates for multiple shortlinks. This feature is particularly relevant in administrative scenarios, such as when a large number of temporary links are set to expire simultaneously or when test or campaign data needs to be centrally controlled.

The design of the bulk set expiry function is intended to be both precise and efficient. Users should be able to set the date and time in a consistent dialogue without having to navigate through each entry individually. At the same time, the implementation must robustly handle erroneous inputs and provide clear feedback on the operation’s success or failure.

The dialogue-based setting of a common expiration date follows a three-step interaction pattern. First, the user selects all relevant shortlinks, which makes the bulk bar visible and enables the “Set expiry for selected…” option. In the second step, a custom dialogue opens to enter the date and time. Only in the third step, after the conscious confirmation, are the new expiration dates set for all selected entries and the grid is updated accordingly.

User interface of a URL shortener showing selected links with options to delete or set expiry, including a dialog box for setting the expiry date and time for multiple links.

The entry point into this functionality is the openBulkSetExpiryDialog() method, which is triggered directly from the bulk action bar. It encapsulates both the dialogue-based recording of the expiration time and the later forwarding to the server API:

The method header already includes a security prompt that prevents the dialogue from opening accidentally without a valid selection. The dialogue configuration is deliberately kept minimalist and focuses on the core input elements: a DatePicker for the date and a TimePicker for the time. The TimePicker’s step size is set to 15 minutes, which is a practical compromise between precision and usability.

The core logic begins with validating the date. If no date is selected, the execution is cancelled, and a corresponding notification is issued. If a date is specified, the method creates a ZonedDateTime from it, which is then converted to an instant. This instant represents the new expiration time to be assigned to all selected shortlinks.

The following loop block illustrates the interface to the backend. For each shortlink, the edit operation is called:

This operation is designed to change only the expiration time, while preserving the original URL value. Errors are handled individually and accounted for in both the UI and logging. Summing up the successful and failed updates results in aggregated feedback at the end of the process, which immediately tells the user how successful the bulk operation was.

Finally, grid.deselectAll() combined with safeRefresh() ensures that both the UI state and the grid data are updated consistently. The user is thus placed in a clean reset state without any manual intermediate steps, while the final Notification.show(…) summarises the operation results. The bulk set expiry function illustrates the interplay among the UI, server API, and error handling, and explains how this mechanism greatly simplifies managing large numbers of links.

Cheers Sven

Advent Calendar 2025 – From UI Interactions to a Deterministic Refresh Architecture

After the first part explained the conceptual basics and the new interactions among global search, search scopes, and advanced filters, the second part focuses on the technical mechanisms that enable these interactions. It is only the revised refresh architecture – above all the interaction of safeRefresh() and RefreshGuard – that ensures that the OverviewView remains calm, deterministic and predictable despite numerous potential triggers.

So while Part 1 describes what has changed in the user interface and why this structuring was necessary, Part 2 now shows in detail how the internal state machine works, how competing UI events are coordinated and why the View achieves the desired robustness in the first place.

With this foundation, the following chapters can be assigned clearly: they analyse the refresh architecture, the reset mechanism, and the validation and error-handling logic – the technical building blocks that ensure the UI concepts described above not only look good but also function reliably.

The source code for this development step can be found on GitHub

and can be found here: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-07

Here are some screenshots of the current development state.

Screenshot of the URL Shortener overview, featuring search bar, filter options, and a table listing shortcodes, URLs, created dates, expiry information, and action buttons.
Screenshot of the URL Shortener Overview interface, displaying search filters, pagination options, and a table listing shortened URLs along with their details.

Refresh architecture with RefreshGuard

With the introduction of advanced filtering logic, not only does the complexity of the user interface increase, but so does the number of potential conditions that can cause a grid reload. Any change to a filter field, any adjustment to the page size, the opening or closing of the Advanced area, or entering the view itself can trigger a new request to the server. Without additional control, this could trigger a cascade of overlapping refreshes, create unnecessary load, and noticeably degrade the user experience.

Against this background, the refresh architecture of the OverviewView has been specifically revised. The central goal was to reduce the number of possible triggers to a controllable mechanism that exhibits consistent, predictable behaviour across all relevant scenarios. Instead of allowing each UI event to have its own access to the DataProvider, a deliberate hard cut was made: the decision of whether and when a refresh may actually take place is bundled in a dedicated layer. This layer contains the RefreshGuard, which acts as a guardian that determines which processes are allowed to retrieve specific data and which are only allowed to change the internal state.

The technical basis of this control system is surprisingly simple at first. A single flag is introduced at the head of the class that controls whether refreshes are currently allowed:

This flag is evaluated by a small helper method that acts as the only allowed entry point for data updates:

Instead of reloading the grid or DataProvider from multiple locations, the view periodically calls safeRefresh(). The method first logs the refresh attempt, then checks the flag. Only if suppressRefresh is not set, refreshAll() is actually  executed on the DataProvider. This creates a clear separation between the semantic intent to update the content and the operational decision of whether it is allowed in the current context.

Conceptually, the RefreshGuard can be understood as a critical section that allows multiple UI operations to be combined. As long as the code is within such a protected phase, direct refresh calls are suppressed. Only at the end of the section does the guard decide whether an aggregated refresh is required and, if so, execute it in a controlled manner. In the source code, this mechanism is implemented as a small, inner helper class:

The constructor of the guard first stores the current state of suppressRefresh and then sets the flag to true. This means that all safeRefresh() operations called within this block are ineffective; they are logged but do not trigger a reload. When leaving the block (i.e., in the close() method), the previous flag value is restored. Optionally, a final refresh can then be triggered by the refreshAfter parameter. By implementing AutoCloseable, the guard can be used in a try-with-resources block, ensuring that the state is reset correctly even in exceptional cases.

The use of this pattern is particularly evident in the View lifecycle. When attaching the OverviewView to the UI, the column visibilities are first set based on the stored preferences. This process changes the grid state significantly but should not cause new data to be loaded from the server multiple times. Accordingly, the entire block is wrapped in a RefreshGuard :

Within the try block, suppressRefresh is set to true; all grid operations run without immediate data retrieval. Only when leaving the block and all column visibilities are set consistently does the guard provide a single final refresh with refreshAfter = true. As a result, the view is loaded exactly once after the first setup, rather than flickering through intermediate states during initialisation.

safeRefresh() also demonstrates its strengths when navigating between pages. The buttons for scrolling forward and backwards only change the internal page cursor and explicitly delegate the data update to the central method:

Here, the use of the guard is deliberately avoided, as each of these actions constitutes a clearly demarcated, independent refresh. Scrolling back or forward to a new page section, or flipping forward, should trigger a change in page size immediately. Nevertheless, all accesses continue to run via safeRefresh(), so that the mechanism remains centrally controllable. If the architecture changes in the future, an adjustment at this point is sufficient to modify the behaviour of the entire view consistently.

Taken together, the combination of safeRefresh() and RefreshGuard transforms the refresh behaviour of the OverviewView from a hard-to-predict byproduct of many UI events to a controlled, deterministic strategy. Complex operations such as initialisation and reset are packed into closed, atomic blocks, while simple actions such as page changes and field changes are explicitly allowed to trigger a refresh. This gives the view both stability and transparency: readers of the code can clearly see where data updates occur, and users of the interface perceive a quiet, responsive application that responds to inputs in a comprehensible way.

Reset Mechanism: Full State-Clear

The reset mechanism of the OverviewView plays a special role within the search and filter architecture. It forms the fastest way back to a clearly defined, neutral initial state – a state in which neither search fragments nor extended filters, sorting options, nor deviating page sizes are active. While earlier implementations often reset only partial aspects, the revised version takes a consistently holistic approach: clicking “Reset” deletes all user-changed parameters without exception and resets the view as if it were being opened for the first time.

Conceptually, this mechanism is directly integrated into the previously introduced refresh architecture. Since the reset involves a large number of individual steps – emptying text fields, resetting checkboxes and date entries, restoring the default sorting, resetting the page size and closing the Advanced area – each of these actions would immediately trigger a refresh when viewed in isolation. To prevent this, and to treat all changes as logical operations, the entire reset process is embedded within a RefreshGuard.

The reset button implementation directly reflects these considerations. The listener for the reset button encapsulates all the necessary steps in a single, clearly defined block:

The reset process begins by clamping a RefreshGuard with the parameter set to true. This first sets the suppressRefresh flag, so that all safeRefresh() calls indirectly triggered in this block ‑remain ineffective. Only when leaving the block (controlled by refreshAfter = true) is a final refresh executed, making the cumulative new state visible in the grid.

Within the block, all user inputs are systematically returned to their original state. First, all search and filter fields are cleared: the global search field, the shortcode and URL fragments in the Advanced area, and the case-sensitivity checkboxes. The date and time fields for the time window under consideration are then set to zero. This ensures that no old period inadvertently affects later requests.

In the next step, the sorting is reset to its default values. First, sortBy and dir are removed to avoid potential inconsistencies; then, createdAt is explicitly set as the sort field and desc as the sort direction. The page size is also deliberately set back to the default value of 25 entries per page, and the page cursor currentPage is set to one. This creates a state that corresponds to the first time you enter the view: no running filters, a defined sorting and a comprehensible page setting.

The global search logic is also reinitialised. The search scope is reset to URL, and the Advanced scope is closed by Advanced.setOpened(false ). Calling setSimpleSearchEnabled(true) re-enables simple mode, and globalSearch.focus() ensures that the cursor lands directly in the global search field. This results in an intuitive process for the user: After the reset, he sees a neutral overview and can immediately start a new, simple search.

This keeps the user interface completely quiet during the reset: no flickering, no multiple queries, no inconsistent intermediate layout. Only when the entire process is complete is a single, final refresh executed, which establishes a consistent initial state across the grid. This stability is not only crucial for the user experience but also facilitates extensibility, as additional reset steps can be added without introducing new side effects. As long as new fields are included in this guard block, they remain part of the same atomic operation.

In combination with the search and filter logic, this results in a reset mechanism that is both semantically and technically cleanly modelled. Semantic because the user has a clear expectation – “everything back to square one” – that is fully met. Technically, because the mechanism is embedded in the central refresh architecture by the RefreshGuard, it causes neither uncontrolled side effects nor hidden data retrievals. On this basis, subsequent chapters can now address error cases, validations, and logging in a more granular way without touching the basic reset path again.

Error handling, validation, and robustness

With the growing number of functions in OverviewView, robustness is increasingly critical. The user interface should not only work reliably in ideal scenarios but also handle incomplete, contradictory, or incorrect inputs. The combination of global search, extended filter area, time windows, sorting settings and page size in particular presents the system with the challenge of recognising and stably handling even complex and potentially conflict-prone states.

In the revised architecture, robustness is not treated as an afterthought, but as an integral part of the UI logic. Many validation and error-avoidance mechanisms are deeply embedded in interaction points: in ValueChange listeners, when switching between Simple and Advanced modes, and when resetting and deriving a binding search string. The view does not aim to take away all user freedom, but instead offers a controlled environment in which only consistent states can arise. The technical side deliberately avoids complex error messages in favour of clear, deterministic rules that become directly visible in the interaction of the input elements.

A central element of this robust logic is the automatic verification of consistency for the search and filter fields. The pattern of defensive synchronisation is already clearly evident in the global search field:

This logic ensures that the shortcode and URL fragment cannot be active simultaneously. As soon as the user enters something in the global search box, the value is interpreted as either a shortcode or a URL. The other field is consistently emptied. In this way, there are no conflicting filter states that would force the application to fulfil two search intentions simultaneously.

The Scope Selection listener reinforces this rule by ensuring that even subsequent changes to the search scope always result in a consistent state:

This prevents a user from typing a search query in URL mode, then switching to shortcode, thereby implicitly creating an invalid search model. The UI detects this state early and maps it to a clear, comprehensible model.

The validation and cleanup mechanisms are particularly evident in advanced mode when deriving a valid simple search state. The applyAdvancedToSimpleAndReset() method  is the technical condensation of this approach:

Several basic principles of robustness are intertwined here. First, it checks whether a shortcode or a URL fragment is set. If both are present, the shortcode fragment is given priority – a clear and comprehensible rule that avoids ambiguity. The entire Advanced area is then consistently cleaned up to ensure that no old or partially set values are unintentionally included in future filters.

In addition, RefreshGuard plays a special role in robust processes. Without the guard, the numerous changes within this method would trigger a series of refresh events. However, the guard suppresses these events selectively and triggers exactly one consistent refresh at the end. This prevents flickering UI transitions and ensures the user always sees the final state.

Another important component is validating time windows. The combination of DatePicker and TimePicker can naturally generate incomplete entries – such as a set date without time or vice versa. The logic in the backend transport takes care of these cases, but already in the UI code, a defensive determination of the timestamp prevents potential errors:

The method is generous, but clear: a time value without a date is not a valid filter. In case of doubt, times are set to midnight, which keeps the model stable even in incomplete scenarios. This type of defensive modelling prevents incomplete UI inputs from leading to inconsistent backend requests.

The event handlers provide additional technical protection for paging and navigation. Actions such as scrolling between pages or changing the page size have a direct effect on the database, but should not trigger any unexpected side effects. The consistent use of safeRefresh() ensures that these changes only take effect if the refresh context allows it:

Here, too, robustness is created by clear rules: A new page size resets the page cursor and triggers a controlled reload – never several, never via a detour.

Finally, logging also contributes significantly to diagnostic robustness. In many places in the code, logger().info() is deliberately used to signal when search changes, refresh processes, or state transitions occur. These traces enable precise reconstruction of complex error patterns in UI-backend interactions without requiring additional debugging mechanisms.

The result is a system that does not rely on errors that are subsequently intercepted, but on deliberately modelled, conflict-free states. The user guidance is designed to prevent invalid or contradictory situations, and the technical foundation ensures that even incomplete or ambiguous inputs are converted into stable, controlled processes. Thus, the combination of validation, synchronisation, and protection mechanisms provides a viable basis for further expansion of the application.

Conclusion and outlook

The revision to the OverviewView is more than a collection of individual improvements. It marks a structural change that fundamentally reshapes the interaction among search, filtering, data updates, and user interaction. From an initially heterogeneous interface with scattered responsibilities, a clearly modelled, consistent and technically robust view has emerged, whose behaviour is comprehensible and extensible in all essential dimensions.

A key result of this revision is the standardisation of the search logic. The introduction of a global search box, together with scope selection, forms a small, self-contained state machine that provides an intuitive entry point for the user. Complemented by the extended filter area, a flexible system is created that supports both fast search queries and more complex filter combinations – without competing with each other. The clear switch between advanced and straightforward mode prevents contradictory states and keeps cognitive effort low.

Equally important is the redesigned refresh architecture. With safeRefresh() and the RefreshGuard, a mechanism has been introduced to stabilise the application’s refresh behaviour. Complex operations such as initialisation or reset are bundled into atomic, deterministic processes, while simple interactions can still react directly. This pattern operates in the background and is particularly noticeable to the user when the operation is quieter and less erratic.

The grid itself has also been further developed in terms of functionality and ergonomics. Copy functions, context-sensitive opening of details, dynamic flow badges and an improved column layout transform the table into an interactive workspace that not only provides information, but also enables action. This proximity between data and interaction reduces the need for additional dialogue changes, thereby contributing to a more fluid workflow.

The robustness of the view ultimately results from a variety of small but effective mechanisms: automatic synchronisation of filter fields, defensive evaluation of incomplete entries, clear prioritisation rules, and consistent logging. All these aspects ensure that the application remains reliable even under unusual input combinations and that the causes of errors can be traced if necessary.

This structural basis enables broad future expansion. The clear separation of UI logic, filter model, and refresh strategy provides a stable foundation on which subsequent features can be built without breaking points. Among other things, the following are conceivable:

– a server-side full-text search that extends the Simple/Advanced model, – colour or iconographic markings of other states such as “soon to expire”, – bulk actions for multiple selections, – a modular sorting and filtering pipeline, – tagging or labelling functions for short URLs, – advanced column settings or custom views.

The new OverviewView thus not only represents an improvement over the status quo but also marks a structural turning point. It creates clarity where implicit or scattered logic previously prevailed and establishes mechanisms that keep the system stable over the long term. In its entirety, the revision represents an important step towards a modern, scalable, and maintainable UI architecture that meets the requirements of growing use cases.

Cheers Sven

Advent Calendar 2025 – From Simple Search to Expert Mode: Advanced Filters and Synchronised Scopes for Power Users

Since its inception, the URL shortener’s continuous development has focused on two core goals: a robust technical foundation without external frameworks and a modern, productive user interface that is both intuitive and efficient for power users. As part of the current development stage, an essential UI module has been revised – the OverviewView, i.e. the view in which users search, filter and manage all saved shortenings.

In the previous version, it became increasingly clear that the search and filtering components were decoupled. The basic search offered limited functionality, while the advanced filters were present but not integrated into a real control flow. In addition, the interaction between user actions such as reset, filter changes, paging, or opening the detail view was not sufficiently stabilised, which sometimes led to multiple refreshes or contradictory UI states.

The revision now implemented aims to achieve several structural objectives. The central focus was on standardising interactions: the search function was redesigned and integrated into a unified concept, including a global search bar with a clearly defined scope and a structured area for advanced filters. In parallel, the technical architecture of the refresh mechanisms has been revised to deliver a quieter, more reliable user experience.

This introduction first outlines the rationale for the changes and situates them within the broader development context. In the following chapters, the new global search is first described, followed by the synchronisation mechanisms, extended filters, internal refresh architecture, and grid improvements. The goal is not only a functional description but also a technical analysis of the mechanisms that enable these improvements and underpin future expansions.

The source code for this development step can be found on GitHub

and can be found here: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-07

The new global search

The introduction of global search marks a central step towards a consistent yet flexible operating logic within the OverviewView. While in the previous version the search function consisted of several independent elements, a uniform interaction surface has now been created, whose behaviour is clearly defined and technically cleanly modelled. The basis is a single text field, supplemented by a search-area selection, which eliminates the prior separation between URL and shortcode searches.

Screenshot of the URL Shortener Overview page displaying a search bar, options for filtering search results, and a table showcasing shortened URLs, their original URLs, creation dates, and expiration status.

In the source code, this standardisation is first reflected in the explicit introduction of two central UI components, which are declared as fixed components of the view:

This clearly defines that there is precisely one global search field and exactly one selection for the search area. Both components are created early in the view’s life cycle and are therefore available to all subsequent configuration steps. The actual design is done in the buildSearchBar() method, in which placeholders, width, and interaction behavior are specifically defined:

The global search is not treated as an arbitrary text field, but is deliberately modelled as a central control element. The placeholder “Search all…” makes it clear that, regardless of the specific search area, the user initially enters only one search term. The actual routing of this value into the appropriate technical field handles the logic associated with the search field and scope selection. Choosing LAZY mode, combined with an explicit timeout, ensures that server-side filter requests are triggered only when the user has completed input.

URL shortener overview interface with a search bar, option to search by URL or shortcode, and a table displaying shortcodes with their corresponding URLs, created dates, expiration statuses, and action buttons.

The search box serves as the starting point for all queries. As soon as the user enters, the content is assigned to either the URL or the shortcode component of the filter model, depending on the currently selected search area. This assignment is not only a UI-side mechanism but also a clear rule within the internal logic: the value of the global search field is always bound to exactly one of the two fields in the request object. The central implementation of this coupling is carried out via the ValueChangeListener of the global search field:

Here, each new value is first converted to a safe, non-null variant. The value of the ComboBox searchScope then decides whether the search term is interpreted as a shortcode filter (codePart) or a URL filter (urlPart). Only one of the two fields may be occupied at a time; the other is cleared automatically. This avoids ambiguous search situations in which multiple filters are applied simultaneously and uncoordinatedly. The global search field thus becomes the sole source for exactly one specific logical filter state.

Another major innovation is the tight coupling between the search field and the search area. The selection of the range – URL or shortcode – directly determines which part of the filter model is active. To ensure that this relationship remains consistent in both directions, the scope selection also reacts to changes and reflects the current search value in the appropriate field:

While the text field listener reacts when the search term changes, it is responsible for maintaining consistent search state when the user subsequently changes the search scope. Both implementations follow the same pattern: a standard source value is interpreted as either a shortcode or a URL, and the inactive field is consistently emptied. This keeps the search interface not only visually clear, but also logical.

The behaviour of the global search is also designed to enable clear prioritisation when combined with the advanced filters. As long as the advanced filters are closed, the global search box controls the filter state independently. When the user opens the advanced area, the global search loses its active role and is relegated to the background, both visually and technically. This separation is encapsulated by a small helper method that explicitly determines the state in which the simple search may be:

This method not only controls the activation and deactivation of the input elements but also provides context-sensitive help text that explains why global search is unavailable in open Advanced mode. This prevents two parallel filter sources from competing with each other and destabilizing the overall state. At the same time, the operating concept remains transparent, as the UI clearly communicates its status.

With this revamped global search, an intuitive, clear entry point has been created with a well-defined function from both the user and technical architecture perspectives. The search field, the scope selection, and the associated filter fields form a small, self-contained state machine whose behaviour is explicitly modelled in the code. The following chapters now examine how this search interacts with the other components and how the underlying synchronisation logic ensures consistent state transitions.

Search scopes and synchronisation logic

While the global search offers a clear entry point, its real strength only becomes apparent through interaction with the underlying synchronisation logic. It is crucial that the selected search scope – i.e. the decision between URL and shortcode – does not remain just a visual interface detail, but is consistently transferred to the internal state model. The goal of this layer is to ensure that at any given time, it is clear which filter is active and which parts of the UI represent that filter.

From the user’s perspective, the behaviour can be divided into two central scenarios. In simple mode, the combination of the global search field and scope selection directly controls the filter state. The user implicitly determines, via the input context, whether to search for a target URL or a shortcode. In advanced mode, by contrast, the global search is reversed, and the detail fields fully control the filter state. This transition between modes is the core of synchronisation logic.

Overview of the URL shortener interface, featuring a search bar, advanced filters for shortcode and original URL, and a table displaying shortcodes, URLs, creation dates, expiration status, and action buttons.

From a technical standpoint, this logic is based on a few, clearly defined principles. First, there is exactly one source for the effective search string at any given time. In simple mode, this is the global search field, which is mapped to either the URL or the shortcode component of the filter model, depending on the scope. In advanced mode, the dedicated URL and shortcode fields in the Advanced area are transferred directly to the request object. Second, there must be no competing states: if the user is working in Advanced mode, global search items are disabled; when the user returns to simple mode, the state is derived from the previous detail values.

The technical implementation of this switch begins where the View establishes the Advanced area as a controlling element. Central is the listener, which reacts to the opening and closing of the details container:

These few lines model the entire state change between the modes. When the user opens the Advanced section, the simple search is disabled. When closing, not only is the Advanced area collapsed, but a consolidation step is also triggered via applyAdvancedToSimpleAndReset(), which converts the previous detailed configuration into a simple, global search state.

To ensure that disabling the simple search does not lead to an inconsistent UI impression, the View encapsulates the necessary adjustments in a small helper method:

The method controls both the interactivity of the search field and scope selection as well as the accompanying help text. Once the Advanced section is opened, the Basic Search values are retained, but cannot be changed. At the same time, the helper text makes it clear that global search is currently disabled. At this level, the first part of the principle described above is implemented: There is always only one active source that determines the effective filter state.

The opposite direction – from advanced mode back to simple mode – is more complex because a choice has to be made here. In the Advanced area, a shortcode fragment and a URL fragment can be entered at the same time. Both would be suitable as filters but cannot be readily combined into a single global search field. This is precisely where applyAdvancedToSimpleAndReset() comes in:

The method begins by evaluating the codePart and urlPart detail fields. Both values are defensively converted to strings and then checked for non-empty content. Two things are derived from this: a “winner” value and a “winner” scope. If a shortcode fragment is set, it takes precedence over any URL fragment. Only if there is no shortcode and only a URL is the URL considered a winner. If both are empty, an empty search string is used, and the scope is reset to “URL”. In this way, the prioritisation described in the running text is implemented in practice, without ambiguity.

In the second block of the method, all Advanced fields are consistently reset. In addition to the text fields for shortcodes and URLs, this applies to the case-sensitivity checkboxes and the time-slot and sorting fields. The Advanced range is thus returned to a defined initial state. Thanks to the RefreshGuard, this reset does not occur through multiple individual refreshes; instead, it is treated as an aggregated state change that culminates in a controlled reload.

Only then is the previously determined winner state reflected into the simple search. The global search scope is set to winnerScope; the global search string is either filled with winnerValue or left empty. Finally, the simple search is reactivated, and the focus is set to the global search field. This provides the user with a straightforward, reduced interface after closing the Advanced area, reflecting the active filter state derived from the previously selected detail values.

Overall, this yields a small but precise state machine. Opening the Advanced pane shifts control entirely to the detail fields and visibly disables global search. Closing triggers a controlled reduction to a single, easy-to-understand filter state. The synchronisation logic remains fully comprehensible in the code, is encapsulated in a few clearly structured methods, and can be extended with additional filter fields if necessary without violating the basic principle. On this basis, the following chapters can now examine other aspects of filtering in detail, such as the extended filter fields and the refresh architecture.

Advanced Filters: Conception and UI Design

The global search serves as a compact entry point to the OverviewView’s filter logic. However, their range of functions is deliberately limited to ensure a low barrier to entry and rapid operation. As soon as the requirements go beyond a simple text fragment, this model reaches its natural limits. This is where Advanced Filters come into play, giving users much finer control over short URL filtering while providing a structured, visually comprehensible interface.

Conceptually, the Advanced area was designed as a deliberately separate mode. It should not be understood as a mere extension of the existing search field, but rather as an independent filter context that becomes active only when the user explicitly opens it. This decision takes into account two considerations. On the one hand, the simple mode should not be burdened with options unnecessary in many everyday scenarios. On the other hand, advanced filter operations – such as the combination of a shortcode fragment, a URL substring, a time window, and sorting – should have a clearly identifiable workspace in which all associated input elements are spatially bundled.

In the source code, this concept already begins at the field level, where the components for the Advanced area are clearly declared separate from the global search:

These fields define the semantic dimensions of the advanced filters: textual filtering via shortcode and original URL (optional), an explicit time window, and sorting criteria. The fact that they are listed as separate attributes of the view expresses the separate mode described above: they do not belong to the global search but to an individual, extended view of the data space.

From a UI perspective, this separation manifests as a details container that makes the advanced filters collapsible. When closed, the Advanced section does not occupy any additional space and only indicates, via its header, that more options are available. Only when opened does a structured form unfold, which arranges the various filter dimensions into logically related groups. The concrete design begins with the configuration of the fields themselves:

The text fields for the shortcode and URL provide meaningful placeholders and, like the global search, use a lazy ValueChange mode with a timeout. This prevents a new filter run from being triggered immediately with each input, while at the same time the filters respond quickly to changes. The sorting pair sortBy is preassigned to you, with permissible values, and thus is embedded within a defined space of possible sorting strategies. Convenience functions supplement the date and time fields: Clear buttons, fixed 15-minute time grids, and placeholders for the time format help users enter data while reducing the risk of invalid values.

The spatial structure of Advanced Filters is designed to visually group related information. Instead of placing all components on a single long line, the implementation supports multiple upstream layouts. First, the date and time fields are merged into two groups:

The two horizontal layouts, „fromGroup“ and „toGroup“, ensure that date and time visually appear as a coherent unit. The vertical alignment at the bottom creates a calm, uniform appearance, even when field heights vary slightly. These groups are then embedded in a FormLayout, which forms the actual responsive structure:

Here, the textual filters – shortcode and URL – as well as the associated case sensitivity checkboxes are arranged together in a block. Below this are the groups for the time slot. ResponsiveSteps determines how many columns the layout may use at different widths. Below 32 rem, a single-column layout is selected; at medium width, two columns are available; and above 56 rem, the layout can be extended to three columns. In this way, the input mask remains readable and well-structured across wide desktop views and narrower windows or split screens.

The sorting control is deliberately visually decoupled from the search block, but positioned on the same horizontal axis. For this purpose, a separate toolbar area will be set up:

By removing the ComboBox labels and using placeholders, the interface remains compact without sacrificing intelligibility. The fixed width ensures stable alignment, while the horizontal grouping makes it clear that both fields functionally belong together. The alignment at the bottom blends with the rest of the header, where the filter boxes are also aligned on a common baseline.

Finally, the search block and sort bar are merged into a single header layout that constitutes the visible content of the Advanced area. This header layout is then embedded in a Details component:

The advHeader ensures the search block occupies the available space, while the sorting tools are anchored to the right edge. Enabling flex-wrap allows the header to wrap to multiple lines within a limited width without disrupting the logical proximity of the elements. The Details component includes this header and makes the entire Advanced section collapsible. When closed, only the title “Advanced filters” remains visible; when opened, the complete form unfolds. The application of the filled theme also provides the area with a visual demarcation from the surrounding layout.

In terms of content, the Advanced Filters design prioritises making the essential dimensions of the data directly accessible. Shortcodes and destination URLs serve as textual entry points, and the search can be configured as case-sensitive or case-insensitive. The temporal context of the short URL – such as the creation or expiration interval considered – is mapped via combined date and time fields that explicitly work in the user’s local context. Finally, the sort field and sorting direction provide fine control over the order of displayed entries, enabling newly created or soon-expiring links to be brought to the foreground.

This expanded range of functions should not leave the user confronted with a confusing number of control elements. The precise spatial separation within the hinged container, the well-thought-out grouping of the fields, and the responsive arrangement therefore not only provide an aesthetic but, above all, a cognitive relief. The user can choose whether to use the standard global search options or switch to expert mode, which provides more detailed control over the database.

On this basis, the Advanced area can be seamlessly embedded into the rest of the architecture in the following chapters. In particular, the connection between the synchronisation logic and the refresh architecture described above demonstrates how the UI design and the internal state engine work in concert to keep even more complex filter requests stable and easy to understand.

Cheer Sven

« Older Entries