Advent Calendar â 2025 â Persistence â Part 02
Today, we will finally integrate the StoreIndicator into the UI.
Vaadin integration: live status of the storeImplementation of the StoreIndicatorRefactoring inside â The MappingCreator as a central logic.EclipseStore â The Persistent FoundationAdditional improvements in the coreBefore & After â Impact on the developer experience The source code for this version can be found on GitHub at https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-02
Hereâs the screenshot of the version weâre implementing now.
Vaadin integration: live status of the store
Now that communication between the client and server has been established via the AdminClient, the focus is on integrating with the Vaadin interface. The goal is to make the current system state visible to the user without requiring manual updates. The mechanism is based on cyclic querying, event control, and a reactive UI concept, all fully implemented in Vaadin.
A central component of this integration is the StoreIndicator component. It serves as a visual interface between the user and the system state. Its design combines UI representation, data retrieval, and state management. The structure is implemented within the Vaadin component model, with the indicator implemented as an independent class, âStoreIndicatorâ. This class extends HorizontalLayout and adds several sub-elements to the layoutâan icon, a text label, and optional status information. Their behaviour is closely linked to Vaadinâs lifecycle control.
The central entry point is the onAttach method, which is executed whenever the component is added to the UI tree. At this moment, the polling mechanism starts, calling the AdminClient every 10 seconds to get the current status from the server. Communication remains fully controlled on the server side, while the UI is automatically synchronised.
@Overrideprotected void onAttach(AttachEvent attachEvent) { refreshOnce(); UI ui = attachEvent.getUI(); ui.setPollInterval(10000); ui.addPollListener(e -> refreshOnce());}Analogous to the onAttach method, onDetach is also an essential part of the StoreIndicatorâs lifecycle. This method is executed when the component is removed from the UI tree, such as when switching between views or closing the session. Here, the polling mechanism is cleanly terminated, and resources such as event subscriptions or listeners are released. This prevents background processes from continuing to send data to a defunct UI component. The code illustrates this process:
@Overrideprotected void onDetach(DetachEvent detachEvent) { super.onDetach(detachEvent); if (subscription != null) { try { subscription.close(); } catch (Exception ignored) { } subscription = null; }}This implementation ensures that no polling tasks or event listeners remain active after the component is removed. This is especially crucial in server-side frameworks like Vaadin to avoid leaks and unnecessary thread activity. The StoreIndicator thus acts like a well-behaved UI citizen, completing its tasks properly when leaving the scene.
Each polling cycle triggers the refreshOnce method, which uses the AdminClient to determine the current memory state. This checks whether the server returns the mode âEclipseStoreâ or âInMemoryâ. The colour scheme is adjusted accordingly, and the symbol is highlighted. The design is based on Vaadinâs Lumo theme system, which enables clean integration with the applicationâs corporate design via CSS variables.
Another central element is the internal event system, implemented via the StoreEvents and StoreConnectionChanged classes. It allows for loose coupling between components, so that any change to the memory state can be published globally and consumed by other UI elements. The StoreIndicator uses this system to propagate state changes, while the OverviewView, for example, acts as a subscriber and automatically updates itself when the memory state changes.
StoreEvents.publish(new StoreConnectionChanged(newMode, info.mappings()));
This architecture avoids direct dependencies between UI components. Instead of explicit references or state propagation via view constructors, an event model is used that reduces complexity while increasing the applicationâs responsiveness. The result is a highly modular, reactive UI structure that reflects data changes on the backend.
The way in which Vaadin is used here as a reactive framework deserves special attention. While classic web frameworks propagate state changes via HTTP requests and complete page reloads, Vaadin uses server-side push to deliver targeted UI updates. In combination with the polling model of the StoreIndicator, a hybrid solution is created: On the one hand, the architecture remains comprehensible and straightforward, and on the other hand, it gives the impression of a live application that reacts to changes in real time.
This integration of event model, UI lifecycle, and data transfer is an example of modern component-oriented design in the Java ecosystem. It shows that reactive behaviour patterns can also be implemented without external reactive frameworks such as Vert.x or Reactor.
Implementation of the StoreIndicator
The indicatorâs graphical structure is already fully defined in the constructor. It extends HorizontalLayout and uses Vaadin components such as Icon and Span to create a compact, semantically transparent status display. The structure follows the typical Vaadin composition logic: The graphic elements are created, styled and then inserted into the layout via the add method. Particular attention is paid to legibility and visual consistency:
public StoreIndicator(AdminClient adminClient) { this.adminClient = adminClient; setAlignItems(FlexComponent.Alignment.CENTER); setSpacing(true); setPadding(false); dbIcon.setSize("16px"); dbIcon.getStyle().set("color", "var(--lumo-secondary-text-color)"); badge.getStyle() .set("font-size", "12px") .set("font-weight", "600") .set("padding", "0.2rem 0.5rem") .set("border-radius", "0.4rem") .set("background-color", "var(--lumo-contrast-10pct)") .set("color", "var(--lumo-body-text-color)"); details.getStyle() .set("font-size", "12px") .set("opacity", "0.8"); add(dbIcon, badge, details);}The actual logic of the status update is contained in the refreshOnce method. It calls the AdminClient to determine the current server state. This returns a StoreInfo object containing the mode and the number of saved mappings. The mode dynamically adjusts the indicatorâs colour schemeâ green for persistent, blue for volatile, and red for erroneous states. This colour coding is based on Vaadinâs Lumo theming and provides immediate visual feedback.
public void refreshOnce() { getUI().ifPresent(ui -> ui.access(() -> { try { StoreInfo info = adminClient.getStoreInfo(); boolean persistent = "EclipseStore".equalsIgnoreCase(info.mode()); badge.setText(persistent ? "EclipseStore": "InMemory"); if (persistent) { badge.getStyle() .set("background-color", "var(--lumo-success-color-10pct)") .set("color", "var(--lumo-success-text-color)"); dbIcon.getStyle().set("color", "var(--lumo-success-color)"); } else { badge.getStyle() .set("background-color", "var(--lumo-primary-color-10pct)") .set("color", "var(--lumo-primary-text-color)"); dbIcon.getStyle().set("color", "var(--lumo-primary-color)"); } details.setText("¡ " + info.mappings() + " items"); getElement().setAttribute("title", persistent ? "Persistent via EclipseStore" : "Volatile (InMemory)"); var newMode = persistent ? StoreMode.ECLIPSE_STORE : StoreMode.IN_MEMORY; if (newMode != lastMode) { lastMode = newMode; StoreEvents.publish(new StoreConnectionChanged(newMode, info.mappings())); } } catch (Exception e) { badge.setText("Unavailable"); badge.getStyle() .set("background-color", "var(--lumo-error-color-10pct)") .set("color", "var(--lumo-error-text-color)"); dbIcon.getStyle().set("color", "var(--lumo-error-color)"); details.setText(""); getElement().setAttribute("title", "StoreInfo endpoint unavailable"); if (lastMode != StoreMode.UNAVAILABLE) { lastMode = StoreMode.UNAVAILABLE; StoreEvents.publish(new StoreConnectionChanged(StoreMode.UNAVAILABLE, 0)); } } }));}This method is an example of reactive UI design without external frameworks. The combination of ui.access() and PollListener allows periodic updates without blocking the main UI thread model. Error cases are elegantly intercepted via the catch block and propagated via the event bus (StoreEvents.publish), so that other components can also react to failures.
Refactoring inside â The MappingCreator as a central logic.
With the introduction of EclipseStore and the parallel continued existence of the InMemory store, the need arose to standardise the generation logic for short URLs. Earlier versions of the application included this logic multiple times â both in the InMemory store and in the later EclipseStore implementation. This redundancy not only led to maintenance costs but also made uniform error handling more difficult. The solution to this problem was to introduce a dedicated component, the MappingCreator, which acts as a central element between validation, alias policy, and persistence mediation.
The MappingCreator covers the entire process chain from the alias check to the generation of a new short code to the transfer to persistent storage. This component follows the principle of functional composition and is designed to operate independently of the specific storage technology. This means that both the InMemory and EclipseStore approaches can use the exact generation mechanism without duplicating the logic.
The class is fully implemented in Core Java and does not require any external libraries. It defines a functional interface, ExistsByCode, that checks whether a specific shortcode already exists, and another interface, PutMapping, that stores a newly created mapping. This makes the Creator a generic tool that can take advantage of any implementation of UrlMappingStore . The following code snippet illustrates the structure:
public final class MappingCreator implements HasLogger { private final ShortCodeGenerator generator; private final ExistsByCode exists; private final PutMapping store; private final clock clock; private final Function<ErrorInfo, String> errorMapper; public MappingCreator(ShortCodeGenerator generator, ExistsByCode exists, PutMapping store, Clock clock, Function<ErrorInfo, String> errorMapper) { this.generator = Objects.requireNonNull(generator); this.exists = Objects.requireNonNull(exists); this.store = Objects.requireNonNull(store); this.clock = Objects.requireNonNullElse(clock, Clock.systemUTC()); this.errorMapper = Objects.requireNonNull(errorMapper); }The complete creation process is then displayed in the create method. The process follows a precisely defined scheme: First, it is checked whether the user has entered an alias. If so, it is checked and normalised via the AliasPolicyâs validate method. If the alias is invalid, the creator creates an error object with HTTP and application code and returns it as a failure result. If no alias is available, the creator automatically generates a unique shortcode with the ShortCodeGenerator, checks for collisions, and repeats the process if necessary.
public Result<ShortUrlMapping> create(String alias, String url) { logger().info("createMapping - alias='{}' / url='{}'", alias, url); final String shortCode; if (!isNullOrBlank(alias)) { var aliasCheck = AliasPolicy.validate(alias); if (aliasCheck.failed()) { var reason = aliasCheck.reason(); var reasonCode = switch (reason) { case NULL_OR_BLANK -> "ALIAS_EMPTY"; case TOO_SHORT -> "ALIAS_TOO_SHORT"; case TOO_LONG -> "ALIAS_TOO_LONG"; case INVALID_CHARS -> "ALIAS_INVALID_CHARS"; case RESERVED -> "ALIAS_RESERVED"; }; var errorJson = errorMapper.apply(new ErrorInfo("400", reason.defaultMessage, reasonCode)); return Result.failure(errorJson); } var normalized = normalize(alias); if (exists.test(normalized)) { var errorJson = errorMapper.apply(new ErrorInfo("409", "normalizedAlias already in use", "ALIAS_CONFLICT")); return Result.failure(errorJson); } shortCode = normalized; } else { String gen = normalize(generator.nextCode()); while (exists.test(gen)) { gen = normalize(generator.nextCode()); } shortCode = gen; } var mapping = new ShortUrlMapping(shortCode, url, Instant.now(clock), Optional.empty()); store.accept(mapping); return Result.success(mapping);}This method shows the functional structure of the component as an example. The creator does not create side effects outside its intended interfaces. It communicates exclusively via the function objects exists and store. Error handling is fully integrated into the result tree, allowing a clean separation between success and failure paths. In this way, logic remains deterministic, testable, and reproducible.
With the introduction of MappingCreator, the codebase has been significantly streamlined. Both the InMemoryUrlMappingStore and the EclipseStoreUrlMappingStore now use the same generation logic, which substantially improves consistency and extensibility. The Creator is thus not only a refactoring result, but also a strategic architectural element that combines functional abstraction, type safety and test-driven development.
EclipseStore â The Persistent Foundation
After the internal generation logic has been unified with the MappingCreator, the implementation of the EclipseStoreUrlMappingStore now serves as the basis for permanent storage of the generated short URLs. This class replaces volatile storage with a fully persistent object model that maintains the applicationâs state across reboots. While the InMemory store only kept all mappings in a ConcurrentHashMap, in the EclipseStore they are stored within a serializable object, the so-called DataRoot. This structure serves as an anchor for all stored data elements, ensuring the entire object graph remains consistent at all times.
The central idea of the EclipseStore approach is to implement storage logic not via relational mappings but via object-oriented persistence. This preserves the applicationâs model in its entirety, without requiring object or database table conversions. The following example shows the definition of the DataRoot:
public class DataRoot implements Serializable { @Serial private static final long serialVersionUID = 1L; private final Map<String, ShortUrlMapping> mappings = new ConcurrentHashMap<>(); public Map<String, ShortUrlMapping> mappings() { return mappings; }}In the EclipseStoreUrlMappingStore, when the server starts, it checks whether a root structure already exists. If not, it is recreated and registered as the root node of the memory instance. The following snippet shows the relevant section from the constructor:
var storagePath = Paths.get(storageDir);Files.createDirectories(storagePath);this.storage = EmbeddedStorage.start(storagePath);DataRoot r = (DataRoot) storage.root();if (r == null) { storage.setRoot(new DataRoot()); storage.storeRoot();}This initialisation ensures that the application has the same persisted state both during a cold boot and after a shutdown. All access to the mappings then takes place directly via the root object. When a new mapping is created, the store calls the storeMappingAndPersist method , which stores the object in the internal map and synchronises the memory.
private void storeMappingAndPersist(ShortUrlMapping m) { var dataRoot = (DataRoot) storage.root(); var mappings = dataRoot.mappings(); mappings.put(m.shortCode(), m); storage.store(mappings);}This immediate write process saves changes immediately. The EclipseStore automatically takes over transaction management at the object level and ensures atomic storage without requiring additional commit logic. This reduces the risk of errors and increases the traceability of the systemâs status.
In addition, the EclipseStoreUrlMappingStore implements all the methods of the UrlMappingStore interface, including find, delete, count, and existsByCode. These methods operate directly on the object graph and use helper classes, such as UrlMappingFilterHelper, to efficiently execute queries and perform sorting. The key difference to the InMemory store is that changes to the data structure take effect immediately in persistent memory and are therefore available again when the application restarts.
The implementation of the EclipseStoreUrlMappingStore makes it clear that persistence was not designed as an afterthought, but as an integral part of the architecture. The entire data flow â from alias generation to MappingCreator to storage in DataRoot â follows a consistent design. This not only achieves persistence, but also anchors it conceptually: the memory is not an external component, but part of the living object model of the application.
Additional improvements in the core
Parallel to the introduction of the EclipseStore and the standardisation of the mapping logic, the central helper classes in the core module have also been further developed to ensure more consistent data processing. The focus was not on expanding the range of functions, but on making internal processes more precise. In particular, JsonUtils, UrlMappingFilterHelper, and the internal validation logic of the AliasPolicy have been explicitly revised to improve integration between the REST layer, the UI, and memory.
The JsonUtils class has been fully refactored to improve the security and robustness of data object serialisation and deserialisation. Instead of resorting to external parsers or frameworks, the class implements a well-defined strategy for simple objects and for nested structures. The focus is on stability, readability and deterministic processing. A central component is the toJsonListingPaped method, which is used for tabular representation in the administration interface. It generates a JSON array from a list of mapping objects, including metadata such as the total number and paging information.
public static String toJsonListingPaged(List<ShortUrlMapping> list, int total) { var sb = new StringBuilder(); sb.append('{'); sb.append("\"total\":").append(total).append(','); sb.append("\"items\":["); for (int i = 0; i < list.size(); i++) { sb.append(toJson(list.get(i))); if (i < list.size() - 1) sb.append(','); } sb.append("]}"); return sb.toString();}This method exemplifies the classâs philosophy: complete control over serialisation and field order. This guarantees that the generated JSON structures correspond precisely to the expected format â an important point when the backend and UI are developed independently of each other. The use of StringBuilder is not a stylistic device, but a deliberate design to minimise garbage objects under high request load.
Another central component is the UrlMappingFilterHelper, which handles dynamic processing of filter and sorting parameters for REST endpoints. This class abstracts the transformation of the user-entered filters into internal predicate objects, which are then applied to the stored mappings at runtime. The implementation supports flexible queries without requiring specialised query languages or parsers. The following excerpt shows the method that evaluates a URL mapping based on several parameters:
public static boolean matches(ShortUrlMapping m, UrlMappingListRequest req) { if (req.codePart() != null && !m.shortCode().contains(req.codePart())) return false; if (req.urlPart() != null && !m.originalUrl().contains(req.urlPart())) return false; if (req.from() != null && m.createdAt().isBefore(req.from())) return false; if (req.to() != null && m.createdAt().isAfter(req.to())) return false; return true;}This compact logic ensures that filter requests are processed directly at the object level. The system does not have to generate intermediate representations or convert data. The result is efficient, storage-level filtering that scales to large datasets. In combination with the storeâs advanced counting methods, it provides a high-performance foundation for complex search and administrative functions.
This area is rounded off by the extension of the AliasPolicy, in particular by the introduction of the helper method failed(). This is used for precise error evaluation and simplifies the control logic in components such as the MappingCreator. Instead of laboriously checking validation results, the developer can directly ask if a validation has failed, and then take fine-grained action based on the return value. This not only improves the readability of the codebase but also the overall systemâs error resistance.
The adjustments above make it clear that there is a well-thought-out concept behind the supposed auxiliary classes. They are not marginal components, but supporting pillars of data consistency and system stability. Through targeted optimisations and a clear methodological framework, a core module is created that serves as the basis for the projectâs further development, both functionally and architecturally.
Before & After â Impact on the developer experience
With the integration of EclipseStore and the unification of generation logic via MappingCreator, the developer experience in this project has fundamentally changed. What began as a loose interplay of individual components has developed into a precisely orchestrated system that convinces both in its architecture and maintainability. This is particularly evident in the reduction of redundancies, improved testability and increased transparency in the code flow.
In the past, central operations â especially the creation and verification of new mappings â were scattered across multiple classes. The InMemory store contained its own validation and checking routines, while later extensions had to reimplement the same logic for persistent storage. With the MappingCreator, this process has now been transformed into a clearly defined, reusable unit. This not only eliminates duplicate code but also the risk of different implementations delivering divergent results. The following excerpt shows an example of how the creation of a mapping was previously realised within the store:
public ShortUrlMapping create(String alias, String url) { String shortCode = alias != null ? alias : generator.nextCode(); if (map.containsKey(shortCode)) { throw new IllegalStateException("Alias already exists: " + shortCode); } var mapping = new ShortUrlMapping(shortCode, url, Instant.now(), Optional.empty()); map.put(shortCode, mapping); return mapping;}This logic was functionally correct, but inflexible. Neither validation nor error objects nor alias rules were built in, and there was no uniform handling of failures or conflicts. With the introduction of MappingCreators and the centralised error structure, this process has been redesigned. The result is a deterministic, controlled, and traceable generation of short URLs, in which every step â from input to persistence â occurs via clearly defined interfaces.
The second significant difference concerns how the memory is addressed. Previously, the entire application was designed for volatile data retention, which meant that each reboot resulted in a complete loss of the stored mappings. With the EclipseStore, this state has been fundamentally changed. The following code shows how the Store initialises its persistent database when the application starts:
var storagePath = Paths.get(storageDir);Files.createDirectories(storagePath);this.storage = EmbeddedStorage.start(storagePath);DataRoot r = (DataRoot) storage.root();if (r == null) { storage.setRoot(new DataRoot()); storage.storeRoot();}These few lines mark the crucial difference between temporary and permanent system states. The DataRootâs data structure is loaded at startup, changes are immediately synchronised, and the state is maintained across reboots. From a developerâs point of view, this means that unit tests, integration tests and runtime behaviour can now consistently access the same database. The difference between development and production disappears because both use the same data flow and persistence logic.
This standardisation also directly affects the user interface. The Vaadin components, such as OverviewView and StoreIndicator, now obtain their data via clearly defined interfaces that are decoupled from the persistence layer. State changes of the store or new mappings trigger events that are consumed by the UI components, without explicit dependencies. This creates a consistent, reactive system that works coherently from the data source to the surface.
This development has fundamentally changed how developers interact with code. Bugs are easier to reproduce, tests are more stable, and extensions can be modular. The code is no longer a collection of independent methods, but a structured system in which each component has its place and responsibility.
Cheers Sven
#Advent2025 #EclipseStore #Java #Vaadin