Heute werden wir den StoreIndicator final in die UI integrieren.
Der Quelltext für diese Version befindet sich auf GitHub unter https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-02
Hier ist der Screenshot der Version, die wir nun implementieren.

Vaadin-Integration: Live-Status des Stores
Nachdem die Kommunikation zwischen Client und Server über den AdminClient etabliert wurde, rückt nun die Integration in die Vaadin-Oberfläche in den Mittelpunkt. Das Ziel ist, den aktuellen Systemzustand für den Benutzer sichtbar zu machen, ohne dass eine manuelle Aktualisierung erforderlich ist. Der Mechanismus basiert auf zyklischer Abfrage, Ereignissteuerung und einem reaktiven UI-Konzept, die vollständig in Vaadin implementiert wurden.
Zentraler Bestandteil dieser Integration ist die Komponente StoreIndicator. Sie dient als visuelle Schnittstelle zwischen dem Benutzer und dem Systemzustand. In ihrer Konstruktion vereint sie UI-Darstellung, Datenabruf und Zustandsverwaltung. Der Aufbau erfolgt im Rahmen des Vaadin-Komponentenmodells, wobei der Indikator als eigenständige Klasse „StoreIndicator“ realisiert wird. Diese Klasse erweitert HorizontalLayout und fügt dem Layout mehrere Unterelemente hinzu – ein Symbol, eine Textbeschriftung und optionale Statusinformationen. Ihr Verhalten ist eng mit der Lebenszyklussteuerung von Vaadin verknüpft.
Der zentrale Einstiegspunkt ist die Methode onAttach, die immer dann ausgeführt wird, wenn die Komponente dem UI-Baum hinzugefügt wird. In diesem Moment startet der Polling-Mechanismus, der alle 10 Sekunden den AdminClient aufruft, um den aktuellen Status vom Server zu beziehen. Die Kommunikation bleibt damit vollständig serverseitig kontrolliert, während das UI automatisch synchronisiert wird.
@Override
protected void onAttach(AttachEvent attachEvent) {
refreshOnce();
UI ui = attachEvent.getUI();
ui.setPollInterval(10000);
ui.addPollListener(e -> refreshOnce());
}
Analog zur Methode onAttach ist auch onDetach ein essenzieller Bestandteil des Lebenszyklus des StoreIndicator. Diese Methode wird ausgeführt, wenn die Komponente aus dem UI-Baum entfernt wird, etwa beim Wechsel zwischen Views oder beim Schließen der Session. Hier wird der Polling-Mechanismus sauber beendet und Ressourcen wie Event-Subscriptions oder Listener werden freigegeben. Dadurch wird verhindert, dass Hintergrundprozesse weiterhin Daten an eine nicht mehr existierende UI-Komponente senden. Der Code verdeutlicht diesen Ablauf:
@Override
protected void onDetach(DetachEvent detachEvent) {
super.onDetach(detachEvent);
if (subscription != null) {
try {
subscription.close();
} catch (Exception ignored) {
}
subscription = null;
}
}
Diese Implementierung stellt sicher, dass nach dem Entfernen der Komponente keine Polling-Tasks oder Event-Listener aktiv bleiben. Das ist insbesondere in serverseitigen Frameworks wie Vaadin entscheidend, um Leaks und unnötige Thread-Aktivität zu vermeiden. Der StoreIndicator agiert damit wie ein wohlerzogener UI-Bürger, der seine Aufgaben beim Verlassen der Szene ordentlich beendet.
Jeder Polling-Zyklus löst die Methode refreshOnce aus, die den AdminClient verwendet, um den aktuellen Speicherstatus zu ermitteln. Dabei wird geprüft, ob der Server den Modus “EclipseStore” oder “InMemory” zurückmeldet. Entsprechend wird das Farbschema angepasst und das Symbol farblich hervorgehoben. Das Design orientiert sich am Lumo-Themesystem von Vaadin, das über CSS-Variablen eine saubere Integration in das Corporate Design der Anwendung ermöglicht.
Ein weiteres zentrales Element ist das interne Eventsystem, das über die Klassen StoreEvents und StoreConnectionChanged realisiert ist. Es erlaubt eine lose Kopplung der Komponenten, sodass jede Änderung des Speicherstatus global veröffentlicht und von anderen UI-Elementen konsumiert werden kann. Der StoreIndicator nutzt dieses System, um Statusänderungen zu propagieren, während beispielsweise die OverviewView als Subscriber agiert und sich automatisch aktualisiert, wenn der Speicherzustand wechselt.
StoreEvents.publish(new StoreConnectionChanged(newMode, info.mappings()));
Diese Architektur vermeidet direkte Abhängigkeiten zwischen UI-Komponenten. Statt expliziter Referenzen oder Zustandsweitergabe über View-Konstruktoren wird ein Ereignismodell genutzt, das die Komplexität reduziert und zugleich die Reaktionsfähigkeit der Anwendung erhöht. Im Ergebnis entsteht eine hochgradig modulare und reaktive UI-Struktur, die Datenänderungen auf Backend-Seite unmittelbar widerspiegelt.
Besondere Aufmerksamkeit verdient die Art und Weise, in der Vaadin hier als reaktives Framework genutzt wird. Während klassische Webframeworks Zustandsänderungen über HTTP-Requests und vollständige Seiten-Neurenderungen transportieren, verwendet Vaadin serverseitige Push-Mechanismen, um gezielte UI-Updates auszuliefern. In Kombination mit dem Polling-Modell des StoreIndicators entsteht eine hybride Lösung: Einerseits bleibt die Architektur einfach und nachvollziehbar, andererseits vermittelt der Eindruck einer Live-Anwendung, die in Echtzeit auf Änderungen reagiert.
Diese Integration von Ereignismodell, UI-Lifecycle und Datentransfer ist ein Beispiel für modernes komponentenorientiertes Design im Java-Ökosystem. Sie zeigt, dass sich reaktive Verhaltensmuster auch ohne externe Reactive-Frameworks wie Vert.x oder Reactor implementieren lassen.
Implementierung des StoreIndicator
Bereits im Konstruktor wird die grafische Struktur des Indikators vollständig definiert. Er erweitert HorizontalLayout und nutzt Vaadin-Komponenten wie Icon und Span, um eine kompakte, semantisch klare Statusanzeige zu erstellen. Der Aufbau folgt der typischen Vaadin-Kompositionslogik: Die grafischen Elemente werden erzeugt, gestylt und anschließend über die add-Methode ins Layout eingefügt. Dabei wird besonders auf Lesbarkeit und optische Konsistenz geachtet:
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);
}
Die eigentliche Logik der Statusaktualisierung ist in der Methode refreshOnce enthalten. Sie ruft den AdminClient auf, um den aktuellen Serverzustand zu ermitteln. Dieser liefert ein StoreInfo-Objekt, das den Modus und die Anzahl der gespeicherten Mappings enthält. Anhand des Modus wird das Farbschema des Indikators dynamisch angepasst – grün für persistente, blau für flüchtige und rot für fehlerhafte Zustände. Diese Farbkodierung orientiert sich am Lumo-Theming von Vaadin und sorgt für eine unmittelbare visuelle Rückmeldung.
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));
}
}
}));
}
Diese Methode ist ein Beispiel für reaktives UI-Design ohne externe Frameworks. Die Kombination von ui.access() und PollListener ermöglicht periodische Aktualisierungen, ohne das Haupt-UI-Thread-Modell zu blockieren. Fehlerfälle werden über den catch-Block elegant abgefangen und über den Eventbus (StoreEvents.publish) propagiert, sodass auch andere Komponenten auf Ausfälle reagieren können.
Refactoring im Inneren – Der MappingCreator als zentrale Logik
Mit der Einführung von EclipseStore und dem parallelen Fortbestehen des InMemory-Stores entstand die Notwendigkeit, die Erzeugungslogik für Kurz-URLs zu vereinheitlichen. Frühere Versionen der Anwendung enthielten diese Logik mehrfach – sowohl im InMemory-Store als auch in der späteren EclipseStore-Implementierung. Diese Redundanz führte nicht nur zu Wartungsaufwand, sondern erschwerte auch eine einheitliche Fehlerbehandlung. Die Lösung dieses Problems war die Einführung einer dedizierten Komponente, des MappingCreators, die als zentrales Element zwischen Validierung, Alias-Politik und Persistenzvermittlung fungiert.
Der MappingCreator umfasst die gesamte Prozesskette von der Alias-Prüfung über die Generierung eines neuen Kurzcodes bis zur Übergabe an die persistente Speicherung. Diese Komponente folgt dem Prinzip der funktionalen Komposition und wurde so entworfen, dass sie unabhängig von der konkreten Speichertechnologie funktioniert. Das bedeutet, dass sowohl der InMemory- als auch der EclipseStore-Ansatz denselben Erzeugungsmechanismus verwenden können, ohne dass sich die Logik duplizieren muss.
Die Klasse ist vollständig in Core Java umgesetzt und setzt keine externe Bibliotheken voraus. Sie definiert ein funktionales Interface ExistsByCode, das prüft, ob ein bestimmter Kurzcode bereits existiert, sowie ein weiteres Interface PutMapping, das die Speicherung eines neu erzeugten Mappings verantwortet. Dadurch wird der Creator zu einem generischen Werkzeug, das jede Implementierung von UrlMappingStore nutzen kann. Der folgende Codeausschnitt verdeutlicht die Struktur:
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);
}
In der Methode create wird anschließend der vollständige Erzeugungsprozess dargestellt. Der Ablauf folgt einem präzise definierten Schema: Zunächst wird geprüft, ob der Benutzer einen Alias angegeben hat. Falls ja, wird dieser über die validate-Methode der AliasPolicy geprüft und normalisiert. Ist der Alias ungültig, erzeugt der Creator ein Fehlerobjekt mit HTTP- und Applikationscode und gibt es als Failure-Result zurück. Ist kein Alias vorhanden, generiert der Creator automatisch einen eindeutigen Kurzcode mit dem ShortCodeGenerator, prüft auf Kollisionen und wiederholt den Vorgang bei Bedarf.
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);
}
Diese Methode zeigt exemplarisch den funktionalen Aufbau der Komponente. Der Creator erzeugt keinen Seiteneffekt außerhalb seiner vorgesehenen Schnittstellen. Er kommuniziert ausschließlich über die Funktionsobjekte exists und store. Die Fehlerbehandlung ist vollständig in die Result-Struktur integriert, was eine saubere Trennung zwischen Erfolgs- und Fehlerpfaden ermöglicht. Auf diese Weise bleibt die Logik deterministisch, testbar und reproduzierbar.
Durch die Einführung des MappingCreator wurde die Codebasis erheblich gestrafft. Sowohl der InMemoryUrlMappingStore als auch der EclipseStoreUrlMappingStore nutzen nun dieselbe Erzeugungslogik, was die Konsistenz und Erweiterbarkeit deutlich verbessert. Der Creator ist somit nicht nur ein Refactoring-Ergebnis, sondern auch ein strategisches Architektur-Element, das funktionale Abstraktion, Typsicherheit und testgetriebene Entwicklung vereint.
EclipseStore – Die persistente Grundlage
Nachdem die interne Erzeugungslogik mit dem MappingCreator vereinheitlicht wurde, bildet die Implementierung des EclipseStoreUrlMappingStore nun die Grundlage für die dauerhafte Speicherung der erzeugten Kurz-URLs. Diese Klasse ersetzt die bisher flüchtige Speicherung durch ein vollständig persistentes Objektmodell, das den Zustand der Anwendung über Neustarts hinweg beibehält. Während der InMemory-Store sämtliche Mappings lediglich in einer ConcurrentHashMap hielt, werden sie im EclipseStore innerhalb eines serialisierbaren Objekts, des sogenannten DataRoot, abgelegt. Diese Struktur fungiert als Ankerpunkt für alle gespeicherten Datenelemente und stellt sicher, dass der gesamte Objektgraph jederzeit konsistent bleibt.
Die zentrale Idee des EclipseStore-Ansatzes besteht darin, die Speicherlogik nicht über relationale Abbildungen, sondern über objektorientierte Persistenz zu realisieren. Damit bleibt das Modell der Anwendung vollständig erhalten, ohne dass Objekte und Datenbanktabellen konvertiert werden müssen. Das folgende Beispiel zeigt die Definition des 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;
}
}
Innerhalb des EclipseStoreUrlMappingStore wird beim Start des Servers geprüft, ob eine solche Root-Struktur bereits existiert. Falls nicht, wird sie neu angelegt und als Wurzelknoten der Speicherinstanz registriert. Der folgende Ausschnitt zeigt den relevanten Abschnitt aus dem Konstruktor:
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();
}
Diese Initialisierung gewährleistet, dass die Anwendung sowohl bei einem Kaltstart als auch nach einem Shutdown über denselben persistierten Zustand verfügt. Alle Zugriffe auf die Mappings erfolgen anschließend direkt über das Root-Objekt. Wird ein neues Mapping erzeugt, ruft der Store die Methode storeMappingAndPersist auf, die das Objekt in der internen Map ablegt und den Speicher synchronisiert.
private void storeMappingAndPersist(ShortUrlMapping m) {
var dataRoot = (DataRoot) storage.root();
var mappings = dataRoot.mappings();
mappings.put(m.shortCode(), m);
storage.store(mappings);
}
Durch diesen unmittelbaren Schreibvorgang werden Änderungen sofort gespeichert. Der EclipseStore übernimmt dabei automatisch das Transaktionsmanagement auf Objektebene und sorgt für eine atomare Speicherung, ohne dass zusätzliche Commit-Logik erforderlich ist. Dies reduziert die Fehleranfälligkeit und erhöht die Nachvollziehbarkeit des Systemzustands.
Darüber hinaus implementiert der EclipseStoreUrlMappingStore alle Methoden des Interfaces UrlMappingStore, darunter find, delete, count und existsByCode. Diese Methoden arbeiten direkt auf dem Objektgraphen und verwenden Hilfsklassen wie UrlMappingFilterHelper, um Abfragen und Sortierungen effizient umzusetzen. Der entscheidende Unterschied zum InMemory-Store liegt darin, dass Änderungen an der Datenstruktur unmittelbar auf dem persistenten Speicher wirksam werden und somit beim Neustart der Anwendung erneut zur Verfügung stehen.
Die Implementierung des EclipseStoreUrlMappingStore verdeutlicht, dass die Persistenz hier nicht als nachträgliche Erweiterung, sondern als integraler Bestandteil der Architektur konzipiert wurde. Der gesamte Datenfluss – von der Alias-Erzeugung über den MappingCreator bis zur Speicherung im DataRoot – folgt einem konsistenten, durchgängigen Design. Damit wird Persistenz nicht nur erreicht, sondern auch konzeptionell verankert: Der Speicher ist keine externe Komponente, sondern Teil des lebenden Objektmodells der Anwendung.
Ergänzende Verbesserungen im Core
Parallel zur Einführung des EclipseStore und der Vereinheitlichung der Mapping-Logik wurden zudem die zentralen Hilfsklassen im Core-Modul weiterentwickelt, um eine konsistentere Datenverarbeitung zu gewährleisten. Dabei stand nicht die Erweiterung des Funktionsumfangs im Vordergrund, sondern die Präzisierung der internen Abläufe. Besonders JsonUtils, UrlMappingFilterHelper und die interne Validierungslogik der AliasPolicy wurden gezielt überarbeitet, um die Integration zwischen der REST-Schicht, der UI und dem Speicher zu verbessern.
Die Klasse JsonUtils wurde vollständig refaktoriert, um die Serialisierung und Deserialisierung von Datenobjekten sicherer und robuster zu gestalten. Anstatt auf externe Parser oder Frameworks zurückzugreifen, implementiert die Klasse eine klar definierte Strategie für einfache Objekte und für verschachtelte Strukturen. Der Fokus liegt auf Stabilität, Lesbarkeit und deterministischer Verarbeitung. Ein zentraler Bestandteil ist die Methode toJsonListingPaged, die für die tabellarische Darstellung in der Administrationsoberfläche verwendet wird. Sie erzeugt aus einer Liste von Mapping-Objekten ein JSON-Array, das Metadaten wie die Gesamtanzahl und Paging-Informationen enthält.
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();
}
Diese Methode zeigt exemplarisch die Philosophie der Klasse: vollständige Kontrolle über die Serialisierung und die Feldreihenfolge. Dadurch kann garantiert werden, dass die erzeugten JSON-Strukturen dem erwarteten Format exakt entsprechen – ein wichtiger Punkt, wenn Backend und UI unabhängig voneinander weiterentwickelt werden. Die Verwendung von StringBuilder ist dabei kein Stilmittel, sondern ein bewusster Entwurf zur Minimierung von Garbage-Objekten bei hoher Request-Last.
Eine weitere zentrale Komponente ist der UrlMappingFilterHelper, der die dynamische Verarbeitung von Filter- und Sortierparametern in den REST-Endpunkten übernimmt. Diese Klasse abstrahiert die Transformation der vom Benutzer eingegebenen Filter in interne Predicate-Objekte, die anschließend zur Laufzeit auf die gespeicherten Mappings angewendet werden. Die Implementierung ermöglicht flexible Abfragen, ohne dass spezielle Query-Sprachen oder -Parser erforderlich sind. Der folgende Ausschnitt zeigt die Methode, die ein URL-Mapping anhand mehrerer Parameter evaluiert:
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;
}
Diese kompakte Logik sorgt dafür, dass Filteranfragen direkt auf Objektebene verarbeitet werden. Das System muss weder Zwischendarstellungen erzeugen noch Daten konvertieren. Das Ergebnis ist eine effiziente, speichernahe Filterung, die sich auch auf große Datenmengen skalieren lässt. In Kombination mit den erweiterten Count-Methoden des Stores bietet sich eine performante Grundlage für komplexe Such- und Administrationsfunktionen.
Abgerundet wird dieser Bereich durch die Erweiterung der AliasPolicy, insbesondere durch die Einführung der Hilfsmethode failed(). Diese dient der präzisen Fehlerauswertung und vereinfacht die Kontrolllogik in Komponenten wie dem MappingCreator. Anstatt Validierungsergebnisse umständlich zu prüfen, kann der Entwickler direkt nachfragen, ob eine Validierung fehlgeschlagen ist, und anschließend auf Grundlage des Rückgabewerts differenzierte Maßnahmen ergreifen. Damit verbessert sich nicht nur die Lesbarkeit der Codebasis, sondern auch die Fehlerresistenz des Gesamtsystems.
Die genannten Anpassungen verdeutlichen, dass hinter den vermeintlichen Hilfsklassen ein durchdachtes Konzept steckt. Sie sind keine Randkomponenten, sondern tragende Pfeiler der Datenkonsistenz und der Systemstabilität. Durch gezielte Optimierungen und eine klare methodische Strukturierung entsteht ein Core-Modul, das sowohl funktional als auch architektonisch die Grundlage für die weitere Entwicklung des Projekts bildet.
Before & After – Auswirkungen auf die Entwicklererfahrung
Mit der Integration des EclipseStore und der Vereinheitlichung der Erzeugungslogik über den MappingCreator hat sich die Entwicklererfahrung in diesem Projekt grundlegend verändert. Was zuvor als loses Zusammenspiel einzelner Komponenten begann, entwickelte sich zu einem präzise orchestrierten System, das sowohl in seiner Architektur als auch in seiner Wartbarkeit überzeugt. Besonders deutlich zeigt sich dies an der Reduktion von Redundanzen, der verbesserten Testbarkeit und der gestiegenen Transparenz im Codefluss.
Früher waren zentrale Operationen – insbesondere das Anlegen und Prüfen neuer Mappings – in mehreren Klassen verstreut. Der InMemory-Store enthielt eigene Prüf- und Validierungsroutinen, während spätere Erweiterungen dieselbe Logik für die persistente Speicherung erneut implementieren mussten. Durch den MappingCreator wurde dieser Prozess nun in eine klar definierte, wiederverwendbare Einheit überführt. Damit entfällt nicht nur doppelter Code, sondern auch das Risiko, dass unterschiedliche Implementierungen divergierende Ergebnisse liefern. Der folgende Ausschnitt zeigt exemplarisch, wie die Erzeugung eines Mappings zuvor innerhalb des Stores realisiert war:
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;
}
Diese Logik war funktional korrekt, aber unflexibel. Weder Validierung noch Fehlerobjekte noch Alias-Regeln waren integriert, und es gab keine einheitliche Behandlung für Fehlschläge oder Konflikte. Mit der Einführung von MappingCreators und der zentralisierten Fehlerstruktur wurde dieser Prozess neu gestaltet. Das Ergebnis ist eine deterministische, kontrollierte und nachvollziehbare Erzeugung von Kurz-URLs, bei der jeder Schritt – von der Eingabe bis zur Persistierung – über klar definierte Schnittstellen erfolgt.
Der zweite signifikante Unterschied betrifft die Art, wie der Speicher angesprochen wird. Früher war die gesamte Anwendung auf eine flüchtige Datenhaltung ausgelegt, wodurch jeder Neustart zu einem vollständigen Verlust der gespeicherten Mappings führte. Mit dem EclipseStore wurde dieser Zustand grundlegend geändert. Der folgende Code zeigt, wie der Store beim Start der Anwendung seine persistente Datenbasis initialisiert:
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();
}
Diese wenigen Zeilen markieren den entscheidenden Unterschied zwischen temporärem und dauerhaften Systemzuständen. Die Datenstruktur des DataRoot wird beim Start geladen, Änderungen werden unmittelbar synchronisiert und der Zustand bleibt über Neustarts hinweg erhalten. Aus Entwicklersicht bedeutet das, dass Unit-Tests, Integrationstests und das Laufzeitverhalten nun konsistent auf dieselbe Datenbasis zugreifen können. Der Unterschied zwischen Entwicklung und Produktivbetrieb verschwindet, weil beide denselben Datenfluss und dieselbe Persistenzlogik nutzen.
Auch in der Benutzeroberfläche wirkt sich diese Vereinheitlichung unmittelbar aus. Die Vaadin-Komponenten wie OverviewView und StoreIndicator beziehen ihre Daten nun über klar definierte, von der Persistenzschicht entkoppelte Schnittstellen. Statusänderungen des Stores oder neue Mappings lösen Events aus, die von den UI-Komponenten konsumiert werden, ohne explizite Abhängigkeiten. Damit entsteht ein durchgängiges, reaktives System, das von der Datenquelle bis zur Oberfläche kohärent arbeitet.
Diese Entwicklung hat die Art und Weise, wie Entwickler mit dem Code interagieren, grundlegend verändert. Fehler lassen sich leichter reproduzieren, Tests sind stabiler, und Erweiterungen können modular erfolgen. Der Code ist nicht mehr eine Sammlung unabhängiger Methoden, sondern ein strukturiertes System, in dem jede Komponente ihren Platz und ihre Verantwortung hat.
Cheers Sven