Advent Calendar 2025 – Introduction of multiple aliases – Part 1
Introduction: More convenience for users
With today’s development milestone for the URL Shortener project, a crucial improvement has been achieved, significantly enhancing the user experience. Up to this point, working with short URLs was functional, but in many ways it was still linear: each destination URL was assigned exactly one alias. This meant users had to create a new short URL for each context or campaign, even when the destination was identical. While this approach was simple, it wasn’t sufficiently flexible to meet real-world application requirements.
You can find the source code for this development status on Github under https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-06

In everyday life, users quickly reach their limits when they want to map the same destination address for different purposes – for example, to analyse access from other sources or to separate internal and external uses. Working with only one alias per target URL forced them to work around and led to unnecessary redundancy in the stored mappings. Especially in professional environments where tracking, traceability, and reusability are essential, this situation was unsatisfactory.
With today’s development, this restriction has been lifted. The system now allows multiple aliases to point to a common destination URL. This focuses on user convenience: Existing short URLs can now be extended with new aliases without creating a new dataset. The process is accompanied by an intuitive user interface that integrates seamlessly into the application’s detailed dialogue.


This extension is more than just a functional improvement. It changes the way users interact with the system. Whereas previously each alias represented a separate entity, a central destination URL with variable identifiers is now introduced. The user no longer thinks in terms of individual links, but rather in terms of a flexible alias space mapped to a destination address. This reduces cognitive load and noticeably accelerates typical workflows.

On a technical level, this development was supported by the introduction of new UI components and event mechanisms that enable dynamic synchronisation between the detail dialogue and the overview. But the real meaning lies in the user experience itself: the shortener becomes a tool that adapts to real-world usage patterns – not the other way around.
From single alias to multi-alias management
The introduction of multi-alias management marks a clear turning point in the URL shortener’s development. While previously each short URL was inseparably bound to a single alias, the system now supports a more flexible, yet more natural, assignment model. From a technical standpoint, this means a structural expansion of the data model, but from the user’s perspective, it means one thing above all: freedom to manage one’s own short-link landscape.

At the heart of this innovation is the MultiAliasEditorStrict component, designed to intuitively guide users through managing multiple aliases. The editor lets you add new aliases, modify existing ones, or remove erroneous entries without opening modal dialogues or new dialogues. Instead, the user remains in a consistent context – the detailed view of a target URL – and can act directly there. This decision was deliberate to avoid friction in the interaction and maintain the flow between viewing and editing.
The implementation of MultiAliasEditorStrict follows a clear principle: strict validation with maximum user-friendliness. Each alias entered is immediately checked for empty values, duplicate entries, and syntactic correctness. Errors are visualised directly in the input field, providing the user with immediate feedback. This direct feedback builds trust in the input and prevents incomplete or erroneous data from reaching the persistence layer.
A key feature of this component is its interaction with the application’s event infrastructure. As soon as a new alias is saved or an existing alias is changed, the editor triggers an event that propagates across the entire UI. This means that other views – such as the overview list – are also automatically updated without requiring a reload. This event control ensures a reactive user interface that always reflects the current state.
From a technical perspective, multi-alias management illustrates how consistently implemented user orientation and clean architecture reinforce one another. The system remains modular, with components clearly separated, yet the user perceives a consistent, organic interface. What was previously a sequential process – create, check, correct – becomes an interactive dialogue with the system, which reacts to the user’s actions in real time.
Thus, MultiAliasEditorStrict is not only the heart of the new alias logic but also a harbinger of future developments. Its architecture lays the foundation for further interactions such as mass edits, alias groupings or time-controlled alias activations. In this sense, the component exemplifies the project’s evolution from a functional tool to a mature, user-centred application.
Source code and explanations
MultiAliasEditorStrict – Core of Multialias Management (excerpt)
package com.svenruppert.urlshortener.ui.vaadin.components;
SNIPP
public class MultiAliasEditorStrict extends VerticalLayout {
private static final String RX = "^[A-Za-z0-9_-]{3,64}$";
private final Grid<Row> grid = new Grid<>(Row.class, false);
private final TextArea bulk = new TextArea("Aliases (comma/space/newline)");
private final Button insertBtn = new Button("Take over");
private final Button validateBtn = new Button("Validate all");
private final String baseUrl;
private final Function<String, Boolean> isAliasFree; Server check (true = free)
public MultiAliasEditorStrict(String baseUrl, Function<String, Boolean> isAliasFree) {
this.baseUrl = baseUrl;
this.isAliasFree = isAliasFree;
build();
}
==== Public API for the parent view ====
public void validateAll() {
var items = new ArrayList<>(grid.getListDataView().getItems().toList());
items.forEach(this::validateRow);
grid.getDataProvider().refreshAll();
}
public List<String> getValidAliases() {
return grid
.getListDataView()
.getItems()
.filter(r -> r.getStatus() == Status.VALID)
.map(Row::getAlias)
.collect(Collectors.toList());
}
public void markSaved(String alias) { setStatus(alias, Status.SAVED, "saved"); }
public void markError(String alias, String message) {
setStatus(alias, Status.ERROR, (message == null ? "error" : message));
}
public long countOpen() {
return grid.getListDataView().getItems()
.filter(r -> r.getStatus() != Status.SAVED).count();
}
public void clearAllRows() { grid.setItems(new ArrayList<>()); }
private void setStatus(String alias, Status s, String msg) {
grid.getListDataView().getItems().forEach(r -> {
if (Objects.equals(r.getAlias(), alias)) {
r.setStatus(s);
r.setMsg(msg);
}
});
grid.getDataProvider().refreshAll();
}
}
Explanation. The component encapsulates all alias management within a standalone UI component. The constructor injects the baseUrl and a server-side availability-check function, keeping the editor testable and decoupling it from specific services. The public API provides the calling view with precise control points: validate all entries, extract valid aliases, set the status of individual rows to “SAVED” after successful saving, mark error states and count open works. The setStatus method updates the row status and triggers a UI refresh of the Grid DataProvider so that users can see the feedback immediately.
UIEvent for consistent refreshing
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
public class MappingCreatedOrChanged extends ComponentEvent<Component> {
public MappingCreatedOrChanged(Component source) { super(source, false); }
}
Explanation. This minimalist event is the link between the detail dialogue, create view and overview. After successfully saving new aliases, the UI fires this event; the OverviewView registers a listener and reloads its DataProvider. For the user, this means that changes are immediately visible without manual reloading.
Server-side redirect – consistent methodology control (excerpt)
@Override
public void handle(HttpExchange exchange) throws IOException {
if (! RequestMethodUtils.requireGet(exchange)) return;
// ... further code
}
Explanation. Recurring checks and error handling are outsourced to utility methods. This reduces dispersion, prevents copy-and-paste divergences and increases the robustness of the routes. This principle is also reflected in the UI: validation, preparation, and persistence are clearly separated and orchestrated through well-defined interfaces.
Note on the presentation. In this chapter, only the excerpts that are decisive for user guidance are shown. The complete files, as well as the context of the private helper methods (build(), validateRow(…), bulk input parser, row POJO, and status enum), are located in the feature/advent-2025-day-06 branch. The public API methods access them directly and map the validation-driven interaction flow.
Intelligent refresh of the overview
The integration of multi-alias management would be incomplete if changes to existing short links were not immediately visible in the overview. This is precisely where intelligent refresh comes in. The user should no longer be required to refresh the view after manually adding or changing aliases. Instead, the application automatically reacts to relevant events and ensures that the displayed data reflects the current state at all times.
Technically, this behaviour is implemented via Vaadin’s global EventBus. As soon as the MappingCreatedOrChanged event is triggered in the DetailsDialog, the OverviewView registers the signal and internally triggers the update process. The user thus experiences a seamless interaction between editing and overview. The logic responsible for this can be found directly in the constructor of the OverviewView:
ComponentUtil.addListener(UI.getCurrent(),
MappingCreatedOrChanged.class,
_ -> {
logger().info("Received MappingCreatedOrChanged -> refreshing overview");
refreshPageInfo();
refresh();
});
This code shows how the browser dynamically responds to changes in other UI components. The crucial part is registering the event listener with ComponentUtil.addListener. Every time a MappingCreatedOrChanged event is raised—for example, by the DetailsDialog after saving new aliases—the OverviewView receives it. The refreshPageInfo() and refresh() methods ensure that the displayed data is reloaded. As a result, status indicators, counters, and table contents always remain up to date without requiring user intervention.
Essential to this implementation is its simplicity and independence. The OverviewView does not know the source of the change. It only reacts to the generic event and fetches the data fresh from the server. This loosely coupled design prevents circular dependencies and makes the system robust to extensions. Future components that also trigger alias changes will be able to use the same event without requiring an adjustment to the overview.
It is also noteworthy that the EventBus system in Vaadin enables an apparent decoupling between UI elements. The DetailsDialog and the OverviewView share neither direct references nor mutual knowledge of their respective states. They communicate exclusively about events. This architecture is not only elegant, but also scalable. It enables future expansion of the system with additional listeners, such as notifications, logging, or statistics updates.
From the user’s perspective, this results in a reactive, immediate application experience. Changes to aliases or destination addresses are reflected in the overview without any noticeable delay. The user remains in the workflow and is always sure that what he sees reflects the current data state. This seemingly minor adjustment transforms the URL shortener’s operation into a modern, responsive interaction model that combines transparency and efficiency.
Improvements in the Create View
As part of this development step, the Create View also underwent a comprehensive overhaul. The aim was to adapt the process for creating new short URLs more closely to the updated user logic and, at the same time, to standardise workflows. While the detail dialogue is primarily responsible for maintaining existing mappings, the Create View serves as an entry point for new entries, now with the same convenience functions as the edit view.

The new version of the Create View offers a more transparent structure and uses a split layout to arrange form elements and preview information side by side. This allows the user to immediately see which aliases they are creating and how they relate to the destination URL. In addition, MultiAliasEditorStrict has been fully integrated, allowing multiple aliases to be captured directly, even when creating a new short URL.
package com.svenruppert.urlshortener.ui.vaadin.views;
SNIPP all imports
import static com.svenruppert.urlshortener.core.DefaultValues.SHORTCODE_BASE_URL;
@Route(value = CreateView.PATH, layout = MainLayout.class)
public class CreateView
extends VerticalLayout
implements HasLogger {
public static final String PATH = "create";
private static final ZoneId ZONE = ZoneId.systemDefault();
private final URLShortenerClient urlShortenerClient = UrlShortenerClientFactory.newInstance();
private final TextField urlField = new TextField("Target URL");
private final DatePicker expiresDate = new DatePicker("Expires (date)");
private final TimePicker expiresTime = new TimePicker("Expires (time)");
private final Checkbox noExpiry = new Checkbox("No expiry");
public CreateView() {
setSpacing(true);
setPadding(true);
setSizeFull();
urlField.setWidthFull();
Button saveAllButton = new Button("Save");
saveAllButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button resetButton = new Button("Reset");
resetButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
configureExpiryFields();
FormLayout form = new FormLayout();
form.add(urlField, noExpiry, expiresDate, expiresTime);
form.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1),
new FormLayout.ResponsiveStep("900px", 2)
);
form.setColspan(urlField, 2);
HorizontalLayout actions = new HorizontalLayout(saveAllButton, resetButton);
actions.setWidthFull();
actions.setJustifyContentMode(JustifyContentMode.START);
Binder<ShortenRequest> binder = new Binder<>(ShortenRequest.class);
binder.forField(urlField)
.asRequired("URL must not be empty")
.withValidator((String url, ValueContext _) -> {
var res = UrlValidator.validate(url);
return res.valid() ? ValidationResult.ok() : ValidationResult.error(res.message());
})
.bind(ShortenRequest::getUrl, ShortenRequest::setUrl);
var editor = new MultiAliasEditorStrict(
SHORTCODE_BASE_URL,
alias -> {
try {
return urlShortenerClient.resolveShortcode(alias) == null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
);
editor.setSizeFull();
editor.getStyle().set("padding", "var(--lumo-space-m)");
saveAllButton.addClickListener(_ -> {
var validated = binder.validate();
if (validated.hasErrors()) return;
if (!validateExpiryInFuture()) return;
if (urlField.getValue() == null || urlField.getValue().isBlank()) {
Notification.show("Target URL is empty", 2500, Notification.Position.TOP_CENTER);
return;
}
editor.validateAll();
List<String> validAliases = editor.getValidAliases();
if (validAliases.isEmpty()) {
Notification.show("No valid aliases to save", 2000, Notification.Position.TOP_CENTER);
return;
}
Optional<Instant> expiresAt = computeExpiresAt();
int ok = 0;
for (String alias : validAliases) {
try {
logger().info("try to save mapping {} / {} ", urlField.getValue(), alias);
var customMapping = urlShortenerClient.createCustomMapping(alias, urlField.getValue(), expiresAt.orElse(null));
logger().info("created customMapping is {}", customMapping);
if (customMapping != null)
logger().info("saved - {}", customMapping);
else logger().info("save failed for target {} with alias {}", urlField.getValue(), alias);
editor.markSaved(alias);
ok++;
} catch (Exception ex) {
editor.markError(alias, String.valueOf(ex.getMessage()));
logger().info("failed to save URL with alias {}", alias);
}
}
Notification.show("Saved: " + ok + " | Open: " + editor.countOpen(), 3500, Notification.Position.TOP_CENTER);
});
resetButton.addClickListener(_ -> {
clearFormAll(binder);
editor.clearAllRows();
});
— SplitLayout
var leftCol = new VerticalLayout(new H2("Create new short links"), form, actions);
leftCol.setPadding(false);
leftCol.setSpacing(true);
leftCol.setSizeFull();
var rightCol = new VerticalLayout(new H2("Aliases"), editor);
rightCol.setPadding(false);
rightCol.setSpacing(true);
rightCol.setSizeFull();
SplitLayout split = new SplitLayout(leftCol, rightCol);
split.setSizeFull();
split.setSplitterPosition(40);
add(split);
}
//... SNIPP
private void clearFormAll(Binder<ShortenRequest> binder) {
urlField.clear();
noExpiry.clear();
expiresDate.clear();
expiresTime.clear();
binder.setBean(new ShortenRequest());
urlField.setInvalid(false);
}
}
This implementation makes it clear that the Create-View is no longer just a simple input form, but now has the same range of functions as the detail dialogue. The user can capture multiple aliases, validate them directly, and save them with a single click. The integration of the MappingCreatedOrChanged event also ensures that the overview is automatically updated whenever a new mapping is created.
The split layout supports a clear visual separation between data entry and alias management. The user remains in the context of their input while also seeing how the selected aliases behave relative to the target URL. The reset function lets you reset inputs without reloading the page.
This revision made the Create View an integral part of the consistent user experience. It shares the validation and event logic with the detailed dialogue and thus serves as the foundation for all alias management. The user benefits from consistent behavior in both contexts, while the code remains clearer, more maintainable, and more extensible by reusing key components.
Consistent validation and error feedback
The growing complexity of the URL shortener’s user interface required validation logic that was both reliable and transparent. With the addition of multiple aliases, it became essential to provide precise feedback to the user when inputs are incomplete, duplicated, or syntactically incorrect. This feedback had to appear directly within the context of the respective component to avoid interrupting the interaction flow.
The validation system was therefore consistently integrated into Vaadin’s existing binder mechanism. The central entry point for this is the Create View, where the target URL field undergoes a strict validation. The implementation uses a dedicated UrlValidator that checks syntactic validity and schema compliance. The following excerpt from the Create View shows the binding and validation of the URL field:
Binder<ShortenRequest> binder = new Binder<>(ShortenRequest.class);
binder.forField(urlField)
.asRequired("URL must not be empty")
.withValidator((String url, ValueContext _) -> {
var res = UrlValidator.validate(url);
return res.valid() ? ValidationResult.ok() : ValidationResult.error(res.message());
})
.bind(ShortenRequest::getUrl, ShortenRequest::setUrl);
This code demonstrates the interaction of mandatory field checking and semantic validation. The validator provides a precise error message for invalid inputs, which is displayed directly below the input field. The user can immediately see why an input was rejected. In this way, Vaadin’s classic form validation is supplemented with project-specific logic.
A similar principle is also used in MultiAliasEditorStrict, which checks each alias entry line by line. In addition to checking syntactic correctness, we also check whether an alias has already been assigned. Visual feedback is provided directly in the alias table, allowing the user to distinguish between valid, saved, and incorrect entries immediately. An excerpt from the corresponding class illustrates this mechanic:
public void validateAll() {
var items = new ArrayList<>(grid.getListDataView().getItems().toList());
items.forEach(this::validateRow);
grid.getDataProvider().refreshAll();
}
private void validateRow(Row r) {
var a = Objects.requireNonNullElse(r.getAlias(), "");
if (!a.matches(RX)) {
r.setStatus(Status.INVALID_FORMAT);
r.setMsg("3–64: A–Z a–z 0–9 - _");
return;
}
long same = grid.getListDataView().getItems()
.filter(x -> x != r && Objects.equals(a, x.getAlias()))
.count();
if (same > 0) {
r.setStatus(Status.CONFLICT);
r.setMsg("Duplicate in list");
return;
}
if (isAliasFree != null) {
try {
if (!isAliasFree.apply(a)) {
r.setStatus(Status.CONFLICT);
r.setMsg("Alias taken");
return;
}
} catch (Exception ex) {
r.setStatus(Status.ERROR);
r.setMsg("Check failed");
return;
}
}
r.setStatus(Status.VALID);
r.setMsg("");
}
The validateAll() method iterates over all alias lines and calls an internal check for each. This validation is strict but transparent to the user: each alias line is given its own status indicator and text message. This means that error states are not collected as a list; they are displayed directly within the visual context. This form of inline validation complies with modern UI principles and minimises cognitive interruptions during data entry.
Another strength of this architecture is the standardisation of error feedback. Whether it’s an invalid URL, an empty alias, or a duplicate name, every discrepancy is handled the same way and communicated through the exact mechanism. This reduces inconsistencies and creates reliable feedback behaviour that users can intuitively understand. At the same time, the code remains easily extensible: new check rules can be added without modifying the existing logic.
The interaction between binder validation and alias-related status display thus creates a coherent feedback system. The user immediately learns which inputs have been accepted, which need correction, and when a record has been successfully saved. As a result, validation is no longer perceived as a hurdle, but as an integral part of an accurate and trustworthy user experience.
Cheers Sven
Discover more from Sven Ruppert
Subscribe to get the latest posts sent to your email.