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.



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:
public OverviewView() {
setSizeFull();
setPadding(true);
setSpacing(true);
add(new H2("URL Shortener – Overview"));
initDataProvider();
var pagingBar = new HorizontalLayout(prevBtn, nextBtn, pageInfo, btnSettings);
pagingBar.setDefaultVerticalComponentAlignment(CENTER);
HorizontalLayout bottomBar = new HorizontalLayout(new Span(), pagingBar);
bottomBar.setWidthFull();
bottomBar.expand(bottomBar.getComponentAt(0));
bottomBar.setAlignItems(CENTER);
VerticalLayout container = new VerticalLayout(searchBar, bottomBar);
container.setPadding(false);
container.setSpacing(true);
container.setWidthFull();
add(container);
add(bulkBar);
add(grid);
configureGrid();
addListeners();
addShortCuts();
try (var _ = withRefreshGuard(false)) {
searchBar.setPageSize(25);
searchBar.setSortBy("createdAt");
searchBar.setDirValue("desc");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
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:
grid.addSelectionListener(event -> {
var all = event.getAllSelectedItems();
boolean hasSelection = !all.isEmpty();
bulkBar.setVisible(hasSelection);
if (hasSelection) {
int count = all.size();
String label = count == 1 ? "link selected" : "links selected";
bulkBar.selectionInfoText(count + " " + label + " on page " + currentPage);
} else {
bulkBar.selectionInfoText("");
}
bulkBar.setButtonsEnabled(hasSelection);
});
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:
public void safeRefresh() {
logger().info("safeRefresh");
if (!suppressRefresh) {
logger().info("refresh");
dataProvider.refreshAll();
}
}
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:
private void configureGrid() {
logger().info("configureGrid..");
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT);
grid.setHeight("70vh");
grid.setSelectionMode(Grid.SelectionMode.MULTI);
configureColumShortCode();
configureColumUrl();
configureColumCreated();
configureColumActive();
configureColumExpires();
configureColumActions();
grid.addItemDoubleClickListener(ev -> openDetailsDialog(ev.getItem()));
grid.addItemClickListener(ev -> {
if (ev.getClickCount() == 2) openDetailsDialog(ev.getItem());
});
GridContextMenu<ShortUrlMapping> menu = new GridContextMenu<>(grid);
menu.addItem("Show details", e -> e.getItem().ifPresent(this::openDetailsDialog));
menu.addItem("Open URL", e -> e.getItem().ifPresent(m -> UI.getCurrent().getPage().open(m.originalUrl(), "_blank")));
menu.addItem("Copy shortcode", e -> e.getItem().ifPresent(m -> UI.getCurrent().getPage().executeJs("navigator.clipboard.writeText($0)", m.shortCode())));
menu.addItem("Delete...", e -> e.getItem().ifPresent(m -> confirmDelete(m.shortCode())));
}
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:
private void addListeners() {
ComponentUtil.addListener(UI.getCurrent(),
MappingCreatedOrChanged.class,
_ -> {
logger().info("Received MappingCreatedOrChanged -> refreshing overview");
refreshPageInfo();
safeRefresh();
});
grid.addSelectionListener(event -> {
var all = event.getAllSelectedItems();
boolean hasSelection = !all.isEmpty();
bulkBar.setVisible(hasSelection);
if (hasSelection) {
int count = all.size();
String label = count == 1 ? "link selected" : "links selected";
bulkBar.selectionInfoText(count + " " + label + " on page " + currentPage);
} else {
bulkBar.selectionInfoText("");
}
bulkBar.setButtonsEnabled(hasSelection);
});
btnSettings.addClickListener(_ -> new ColumnVisibilityDialog<>(grid, columnVisibilityService).open());
prevBtn.addClickListener(_ -> {
if (currentPage > 1) {
currentPage--;
refreshPageInfo();
safeRefresh();
}
});
nextBtn.addClickListener(_ -> {
int size = Optional.ofNullable(searchBar.getPageSize()).orElse(25);
int maxPage = Math.max(1, (int) Math.ceil((double) totalCount / size));
if (currentPage < maxPage) {
currentPage++;
refreshPageInfo();
safeRefresh();
}
});
}
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:
private void addShortCuts() {
var current = UI.getCurrent();
current.addShortcutListener(_ -> {
if (!grid.getSelectedItems().isEmpty()) {
bulkBar.confirmBulkDeleteSelected();
}
},
Key.DELETE);
}
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:
activeState.addValueChangeListener(_ -> {
holdingComponent.setCurrentPage(1);
holdingComponent.safeRefresh();
});
pageSize.addValueChangeListener(e -> {
holdingComponent.setCurrentPage(1);
holdingComponent.setGridPageSize(e.getValue());
holdingComponent.safeRefresh();
});
resetBtn.addClickListener(_ -> {
try (var _ = withRefreshGuard(true)) {
resetElements();
holdingComponent.setCurrentPage(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
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:
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();
}
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:
private void build() {
setPadding(false);
setSpacing(true);
bulk.setWidthFull();
bulk.setMinHeight("120px");
bulk.setValueChangeMode(ValueChangeMode.LAZY);
bulk.setClearButtonVisible(true);
bulk.setPlaceholder("e.g.\nnews-2025\npromo_x\nabc123");
insertBtn.addClickListener(_ -> parseBulk());
validateBtn.addClickListener(_ -> validateAll());
var toolbar = new HorizontalLayout(insertBtn, validateBtn);
toolbar.setSpacing(true);
configureGrid();
add(bulk, toolbar, grid);
}
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:
private void configureGrid() {
grid.addComponentColumn(row -> {
var tf = new TextField();
tf.setWidthFull();
tf.setMaxLength(64);
tf.setPattern(RX);
tf.setValue(Objects.requireNonNullElse(row.getAlias(), ""));
tf.setEnabled(row.getStatus() != Status.SAVED);
tf.addValueChangeListener(ev -> {
row.setAlias(ev.getValue());
validateRow(row);
grid.getDataProvider().refreshItem(row);
});
return tf;
}).setHeader("Alias").setFlexGrow(1);
grid.addColumn(r -> baseUrl + Objects.requireNonNullElse(r.getAlias(), ""))
.setHeader("Preview").setAutoWidth(true);
grid.addComponentColumn(row -> {
var lbl = switch(row.getStatus()) {
case NEW -> "New";
case VALID -> "Valid";
case INVALID_FORMAT -> "Format";
case CONFLICT -> "Taken";
case ERROR -> "Error";
case SAVED -> "Saved";
};
var badge = new Span(lbl);
var theme = switch (row.getStatus()) {
case VALID, SAVED -> "badge success";
case CONFLICT, INVALID_FORMAT, ERROR -> "badge error";
default -> "badge";
};
badge.getElement().getThemeList().add(theme);
if (row.getMsg() != null && !row.getMsg().isBlank()) badge.setTitle(row.getMsg());
return badge;
}).setHeader("Status").setAutoWidth(true);
grid.addComponentColumn(row -> {
var del = new Button("✕", e -> {
var items = new ArrayList<>(grid.getListDataView().getItems().toList());
items.remove(row);
grid.setItems(items);
});
del.getElement().setProperty("title", "Remove");
return del;
}).setHeader("").setAutoWidth(true);
grid.setItems(new ArrayList<>());
grid.setAllRowsVisible(false);
grid.setHeight("320px");
}
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:
private void parseBulk() {
var text = Objects.requireNonNullElse(bulk.getValue(), "");
var tokens = Arrays.stream(text.split("[,;\\s]+"))
.map(String::trim)
.filter(s -> !s.isBlank())
.distinct()
.toList();
if (tokens.isEmpty()) {
Notification.show("No aliases to insert", 2000, Notification.Position.TOP_CENTER);
return;
}
Set<String> existing = grid.getListDataView().getItems()
.map(Row::getAlias)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)));
var view = grid.getListDataView();
int added = 0;
for (String tok : tokens) {
if (existing.contains(tok)) continue;
var r = new Row(tok);
validateRow(r);
view.addItem(r);
existing.add(tok);
added++;
}
grid.getDataProvider().refreshAll();
bulk.clear();
Notification.show("Inserted: " + added, 2000, Notification.Position.TOP_CENTER);
}
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:
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("");
}
Together with the status enum and the inner row class, this results in a self-contained, easily comprehensible state machine for each alias:
public enum Status { NEW, VALID, INVALID_FORMAT, CONFLICT, ERROR, SAVED }
public static final class Row {
private string alias;
private status status = Status.NEW;
private String msg = "";
Row(String a) {
this.alias = a;
}
public String getAlias() { return alias; }
public void setAlias(String a) { this.alias = a; }
public Status getStatus() { return status; }
public void setStatus(Status s) { this.status = s; }
public String getMsg() { return msg; }
public void setMsg(String m) { this.msg = m; }
}
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





























