Die Erzeugung von Kurzlinks wirkt auf den ersten Blick wie ein ausgesprochen einfacher Anwendungsfall. Eine Zieladresse wird eingegeben; das System erzeugt einen Shortcode, der unmittelbar verwendet werden kann. Solange sich dieser Ablauf auf einzelne Links beschränkt, bleibt die Benutzerführung überschaubar. Mit dem Übergang zur Massenanlage verändert sich die Situation jedoch grundlegend. Aus einer einzelnen Eingabe wird ein Arbeitsprozess, in dem mehrere URLs gleichzeitig erfasst, geprüft, bewertet und schließlich erzeugt werden müssen. Genau an diesem Punkt reicht es nicht mehr aus, eine bestehende Erfassungsmaske lediglich um weitere Eingabemöglichkeiten zu ergänzen. Je größer die Menge der zu verarbeitenden URLs wird, desto stärker rückt die Führung durch den Prozess in den Mittelpunkt.

Die Quellen zu diesem Projekt befinden sich

auf GitHub unter https://3g3.eu/url 

Gerade an Administrationsoberflächen zeigt sich, wie wichtig eine klare Benutzerinteraktion ist. Eine technisch korrekte Funktion ist noch nicht automatisch gut bedienbar. Für Endanwender ist entscheidend, ob die Oberfläche verständlich vermittelt, was gerade passiert, welcher Zustand vorliegt und welcher Schritt als Nächstes sinnvoll oder erforderlich ist. Erst wenn die Anwendung den Benutzer durch Prüfung, Bereinigung und Entscheidung leitet, entsteht ein Workflow, der auch im täglichen Einsatz tatsächlich durchgeführt wird.

Dieser erste Teil betrachtet die Entwicklung eines solchen Bulk-Workflows bewusst aus Sicht der Benutzer. Im Mittelpunkt stehen zunächst nicht Klassen, Endpunkte oder interne technische Entscheidungen, sondern die Frage, wie sich eine Massenfunktion in einer Vaadin-basierten Administrationsoberfläche so entwickeln lässt, dass sie nachvollziehbar, belastbar und praktisch nutzbar wird.

Ausgangslage: Die Grenzen einer klassischen Einzelerfassung

Die bestehende Einzelerfassung im URL-Shortener ist auf einen klar linearen Ablauf ausgelegt. Eine Ziel-URL wird eingegeben, optionale Eigenschaften wie Aktivstatus oder Ablaufzeit werden gesetzt, anschließend wird genau ein Kurzlink erzeugt. Für diesen Anwendungsfall ist eine klassische Create-Maske fachlich passend, da sie einen einzelnen Datensatz vollständig erfasst und speichert.

Für die Massenanlage reicht dieses Modell jedoch nicht aus. Sobald mehrere Ziel-URLs in einem zusammenhängenden Vorgang verarbeitet werden sollen, ändert sich die fachliche Anforderung. Es geht nicht mehr nur um die Erfassung einzelner Datensätze, sondern um die kontrollierte Bearbeitung einer Menge. Damit treten Anforderungen auf, die in einer klassischen Einzelerfassung nur unzureichend abgebildet werden können. Dazu gehören insbesondere die Prüfung mehrerer URLs in einem Lauf, die Behandlung fehlerhafter Einträge, die Erkennung von Dubletten innerhalb der Menge, der Abgleich mit bereits vorhandenen Shortlinks sowie die Bewertung fachlicher Warnzustände.

Technisch entscheidend ist, dass diese Fragestellungen nicht isoliert pro Eingabefeld beantwortet werden können. Sie entstehen erst aus dem Verhältnis der Einträge untereinander und aus ihrem Abgleich mit dem bestehenden Datenbestand. Eine Einzelerfassung arbeitet datensatzorientiert, ein Bulk-Workflow hingegen mengenorientiert. Genau daraus ergibt sich ein anderes Interaktionsmodell. Die Oberfläche muss nicht nur Eingaben annehmen, sondern auch eine Arbeitsmenge aufbauen, ihren Zustand sichtbar machen und Bearbeitungsschritte zwischen Validierung und Erzeugung unterstützen.

Für den URL-Shortener bedeutet das: Die Bulk-Funktion kann nicht als erweiterte Variante der bestehenden Create-View verstanden werden. Sie benötigt eine eigene Logik, weil sie nicht einen einzelnen Link erzeugt, sondern eine Menge von Kandidaten in einen konsistenten und freigegebenen Zustand überführen muss. Der Schwerpunkt liegt damit nicht auf dem einmaligen Speichern, sondern auf den Zwischenschritten der Prüfung, Korrektur und Entscheidung.

Ziel ist nicht die bloße Mehrfacheingabe, sondern ein fachlich geführter Verfahren, in dem eine Menge von URLs vor der eigentlichen Erzeugung validiert, bereinigt und strukturiert bearbeitet wird.

Warum ein Bulk-Workflow nicht nur eine größere Create-Maske ist

Eine klassische Create-Maske erfasst einen Datensatz, validiert ihn im Kontext dieses Falls und löst anschließend die Speicherung aus. Ein Bulk-Workflow verarbeitet dagegen eine Menge von Einträgen, deren Qualität sich nicht nur aus der syntaktischen Gültigkeit jeder einzelnen URL ergibt, sondern auch aus den Beziehungen der Einträge untereinander sowie ihrem Abgleich mit bereits vorhandenen Daten.

Mehrere URLs müssen zunächst gesammelt, anschließend validiert, gegebenenfalls korrigiert oder entfernt und erst danach erzeugt werden. Dieser Ablauf ist nicht mehr rein formularbasiert, sondern zustandsbasiert. Die Oberfläche muss also nicht nur Eingaben erfassen, sondern auch eine Arbeitsmenge verwalten, den Status pro Eintrag sichtbar machen und Übergänge zwischen verschiedenen Bearbeitungsphasen unterstützen.

Technisch führt das zu einer klaren Trennung zwischen Aufnahme, Validierung und Erzeugung. Neue URLs werden zunächst in eine Menge übernommen, dort gegen Regeln und Bestand geprüft und erst nach erfolgreicher Bereinigung zur Erstellung freigegeben. Ein Bulk-Workflow benötigt zusätzliche Zustände wie Dublette, ungültig, bereits vorhanden, warnend oder freigegeben.

Der Validate-Create-Ansatz als zentrales Bedienmodell

In der ersten Phase werden neu eingegebene URLs validiert und in eine kontrollierte Arbeitsmenge überführt. In der zweiten Phase werden nur noch die Einträge erzeugt, die einen tatsächlich erstellbaren Zustand erreicht haben. Die erste wichtige Konsequenz ist in der View selbst sichtbar. Der Hauptbutton funktioniert nicht statisch, sondern wechselt je nach Zustand der Arbeitsmenge zwischen „Validate“ und „Create“. Entscheidend ist dabei nicht nur, ob im Texteingabefeld Inhalt vorhanden ist, sondern auch, ob noch blockierende Einträge oder unbestätigte Warnzustände im Grid vorliegen.

CODE
private void updatePrimaryButton() {
  final boolean textAreaHasContent = urlsArea.getValue() != null
      && !urlsArea.getValue().isBlank();
  final boolean hasBlocking = workItems.stream().anyMatch(this::isBlocking);
  final boolean hasUnconfirmed = workItems.stream()
      .anyMatch(w -> w.getState() == WorkItemState.HAS_EXISTING && !w.isConfirmed());
  final boolean hasReadyToCreate = workItems.stream().anyMatch(this::isReadyToCreate);
  final boolean workSetEmpty = workItems.isEmpty();
  if (!textAreaHasContent && !hasBlocking && !workSetEmpty) {
    primaryButton.setText(tr(K_BTN_CREATE, "Create Links"));
    primaryButton.removeThemeVariants(ButtonVariant.LUMO_PRIMARY);
    primaryButton.addThemeVariants(ButtonVariant.LUMO_SUCCESS);
    primaryButton.setEnabled(!hasUnconfirmed && hasReadyToCreate);
  } else {
    primaryButton.setText(tr(K_BTN_VALIDATE, "Validate"));
    primaryButton.removeThemeVariants(ButtonVariant.LUMO_SUCCESS);
    primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
    primaryButton.setEnabled(textAreaHasContent || hasBlocking);
  }
}

Solange neue Eingaben im Textfeld stehen oder das Grid noch blockierende Einträge enthält, bleibt die Oberfläche im Validate-Modus. Erst wenn die Eingaben übernommen wurden, keine harten Fehler mehr vorliegen und die Menge nicht leer ist, wird aus der Prüfphase in den Erzeugungszustand übergegangen.

Damit diese Trennung sauber funktioniert, muss die Validierung nicht strikt persistiert werden. Genau das wird durch einen eigenen Validierungs-Endpoint erreicht. Im Server wird nicht sofort erzeugt, sondern zunächst geprüft, ob URLs leer, ungültig, zu lang oder bereits in der Arbeitsmenge enthalten sind. Zusätzlich wird der Datenbestand einbezogen, um bereits vorhandene Shortlinks oder Protokollvarianten zu erkennen.

CODE
if (url == null || url.isBlank()) {
  results.add(ValidationItemResult.empty(i));
  continue;
}
if (url.length() > BulkValidateRequest.MAX_URL_LENGTH) {
  results.add(ValidationItemResult.tooLong(i, url));
  continue;
}
final var validation = UrlValidator.validate(url);
if (!validation.valid()) {
  results.add(ValidationItemResult.invalid(i, url, validation.message()));
  continue;
}
if (gridUrls.contains(url)) {
  results.add(ValidationItemResult.duplicateInGrid(i, url));
  seenInBatch.add(url);
  continue;
}
final String counterpartUrl = swapProtocol(url);
if (counterpartUrl != null && gridUrls.contains(counterpartUrl)) {
  results.add(ValidationItemResult.duplicateInGrid(
      i, url,
      "URL already in work set as its " +
      (url.startsWith("https://") ? "http" : "https") +
      " counterpart: " + counterpartUrl));
  seenInBatch.add(url);
  continue;
}

Die Validierung bewertet also nicht nur den einzelnen Datensatz, sondern auch dessen Position innerhalb einer Menge. Eine URL ist nicht nur dann problematisch, wenn sie syntaktisch ungültig ist, sondern auch dann, wenn sie in der Arbeitsmenge bereits enthalten ist oder als HTTP- oder HTTPS-Variante vorliegt.

Zusätzlich wird der bestehende Datenbestand berücksichtigt. Statt lediglich eine Liste vorhandener Shortcodes zurückzugeben, liefert der Validate-Schritt inzwischen angereicherte Informationen zu bereits existierenden Shortlinks, darunter Aktivstatus, Ablaufzeit und die Kennzeichnung als Protokollvariante.

CODE
public static final class ExistingShortlinkInfo {
  private String shortCode;
  private boolean active;
  private Instant expiresAt;
  private boolean protocolVariant;
  public ExistingShortlinkInfo() {
  }
  public ExistingShortlinkInfo(String shortCode,
                               boolean active,
                               Instant expiresAt,
                               boolean protocolVariant) {
    this.shortCode = shortCode;
    this.active = active;
    this.expiresAt = expiresAt;
    this.protocolVariant = protocolVariant;
  }
  public String getShortCode() {
    return shortCode;
  }
  public boolean isActive() {
    return active;
  }
  public Instant getExpiresAt() {
    return expiresAt;
  }
  public boolean isProtocolVariant() {
    return protocolVariant;
  }
}

Der Validate-Schritt erzeugt damit nicht nur ein Ja-oder-Nein-Ergebnis, sondern auch eine strukturierte Bewertung jedes Eintrags. Die Oberfläche kann daraus Zustände wie VALID, INVALID_URL, DUPLICATE_IN_GRID oder HAS_EXISTING_SHORTLINKS ableiten und die Arbeitsmenge entsprechend aufbauen. Die Validierung dient also nicht der Speicherung, sondern der Erzeugung eines fachlich interpretierbaren Zustandsbilds.

Auf dieser Grundlage wird verständlich, warum das Grid im nächsten Schritt zum eigentlichen Arbeitsraum wird. Die neue Eingabe wird nicht sofort persistiert, sondern zunächst in eine Menge von WorkItems überführt, die jeweils ihren fachlichen Status tragen. Erst wenn diese Menge bereinigt und freigegeben ist, darf die eigentliche Erzeugung stattfinden.

Der Übergang in die zweite Phase ist ebenso bewusst gestaltet. Einträge mit Warnstatus, etwa bei bereits vorhandenen Shortlinks, sind nicht automatisch blockierend, müssen jedoch explizit bestätigt werden. Auch das ist Teil des Validate-Create-Modells: Nicht jede Auffälligkeit stoppt den Prozess, aber jede relevante Auffälligkeit muss vor der Erzeugung sichtbar und entscheidend sein.

CODE
item.setState(WorkItem.mapValidationStatus(r.getStatus()));
item.setExistingShortCodes(r.getExistingShortCodes());
item.setExistingShortlinkInfos(r.getExistingShortlinkInfos());
item.setMessage(r.getErrorMessage());
item.setConfirmed(false); // URL changed → previous confirmation no longer applies

Der Bestätigungsstatus ist damit kein kosmetisches Detail, sondern Teil des fachlichen Zustandsmodells. Sobald sich eine URL ändert oder erneut geprüft wird, verfällt eine frühere Bestätigung und muss neu erfolgen. Auch darin zeigt sich, dass Validate und Create nicht zwei lose aufeinanderfolgende Buttons sind, sondern zwei sauber getrennte Phasen eines kontrollierten Bearbeitungsprozesses.

Die eigentliche Erzeugung ist dadurch technisch vereinfacht. Sie muss nicht mehr klären, ob eine URL überhaupt verarbeitbar ist, sondern arbeitet ausschließlich mit einer Menge, die diese Prüfung bereits durchlaufen hat. Die Persistierung wird damit zum Abschluss eines vorbereiteten Zustands, nicht mehr zum ersten Moment, in dem Probleme sichtbar werden. Genau das macht den Ablauf aus Benutzersicht berechenbar.

Im URL-Shortener bildet die Trennung zwischen Validate und Create das eigentliche Bedienmodell der Bulk-Funktion. Sie ist keine nachträgliche Verfeinerung der Oberfläche, sondern die strukturelle Grundlage dafür, dass der Workflow kontrolliert, transparent und für wiederholte Korrektur- und Bearbeitungsschritte geeignet bleibt.

Das Grid als führende Arbeitsmenge in Vaadin

Mit der Trennung zwischen Validate und Create stellt sich zwangsläufig die Frage, wo der eigentliche Zustand des Bulk-Prozesses gespeichert wird. Ein einfaches Texteingabefeld reicht dafür nicht aus. Es kann neue URLs aufnehmen, eignet sich jedoch nicht dazu, Status, Konflikte, Warnungen sowie nachträgliche Bearbeitungsschritte strukturiert abzubilden. Aus diesem Grund wird das Grid im Bulk-Workflow zur führenden Arbeitsmenge.

Technisch bedeutet das, dass das Grid nicht nur der Anzeige dient, sondern auch den aktuellen Bearbeitungsstand der gesamten Menge darstellt. Jede übernommene URL wird dort als eigener Eintrag mit dem jeweiligen fachlichen Zustand geführt. Dazu gehören nicht nur die URL selbst, sondern auch Informationen wie Validierungsstatus, vorhandene Shortlinks, Fehlermeldungen, Warnungen sowie spätere Erzeugungsergebnisse. Damit verschiebt sich der Schwerpunkt der Oberfläche vom reinen Eingabemedium hin zu einem zustandsbehafteten Arbeitsraum.

Im Projekt wird das bereits an der Struktur der BulkCreateView erkennbar. Neben dem Texteingabefeld gibt es eine eigenständige Liste von WorkItems, die als Arbeitsmenge dient und im Grid dargestellt wird.

CODE
private final TextArea       urlsArea        = new TextArea();
private final DateTimePicker expiresDateTime = new DateTimePicker();
private final Checkbox       cbActive        = new Checkbox(true);
private final Span           previewLabel    = new Span();
private final Button         primaryButton   = new Button();
private final Button         resetButton     = new Button();
private final Grid<WorkItem> resultsGrid = new Grid<>(WorkItem.class, false);
private final List<WorkItem> workItems = new ArrayList<>();

Der entscheidende Punkt ist, dass nicht das Textfeld der führende Zustandsträger ist, sondern workItems. Das Texteingabefeld dient nur der Aufnahme neuer URLs. Nach einem Validate-Schritt werden die übernommenen Einträge in die Arbeitsmenge überführt und aus dem Textfeld entfernt. Genau dadurch entsteht eine saubere Trennung zwischen der neuen Eingabe und der bereits bearbeiteten Menge.

CODE
for (final ValidationItemResult r : response.getResults()) {
  workItems.add(WorkItem.fromValidation(r));
}
urlsArea.setValue(remainingText);
refreshGrid();
updatePrimaryButton();
updatePreview();

Damit wird das Grid zur eigentlichen Mitte des Workflows. Es zeigt nicht nur Ergebnisse an, sondern auch die Anzahl der aktuell zu bearbeitenden Einträge. Genau diese Rolle unterscheidet das Grid in diesem Szenario von einer rein tabellarischen Ausgabe.

In Vaadin ist dieses Modell besonders naheliegend, weil sich ein Grid nicht nur als Anzeigeelement verwenden lässt, sondern auch als interaktive Arbeitsfläche. Im Bulk-Workflow wird das Grid deshalb mit mehreren zustandsbezogenen Spalten aufgebaut: URL, Status, vorhandene Links, Fehlermeldung, erzeugter Kurzlink und Zeilenaktionen. Bereits die URL-Spalte zeigt, dass die Darstellung nicht neutral ist, sondern auf den fachlichen Zustand reagiert. Unsichere HTTP-Ziele werden beispielsweise direkt als markierte Links gerendert.

CODE
final var urlCol = resultsGrid.addComponentColumn(item -> {
  final String url = item.getUrl();
  if (url != null && url.startsWith("http://")) {
    return UiLinks.httpAwareLink(url, url);
  }
  return new com.vaadin.flow.component.html.Span(url != null ? url : "");
})
    .setHeader(tr(K_COL_URL, "Original URL"))
    .setFlexGrow(3)
    .setResizable(true);

Gerade in diesem Grid werden Fehler, Dubletten und Warnzustände nicht nur angezeigt, sondern auch fachgerecht verarbeitet. Der Bulk-Workflow arbeitet deshalb mit einem expliziten Statusmodell. Das Validierungsergebnis liefert nicht nur ein generisches Erfolg- oder Fehlersignal, sondern auch einen klaren fachlichen Zustand pro Eintrag. Dazu gehören beispielsweise gültige Zeilen, ungültige URLs, zu lange URLs, Dubletten innerhalb des aktuellen Batchs, Dubletten in der bereits bestehenden Arbeitsmenge sowie vorhandene Shortlinks für dieselbe Zieladresse.

CODE
public enum ValidationStatus {
  VALID,
  EMPTY,
  INVALID_URL,
  TOO_LONG,
  DUPLICATE_IN_BATCH,
  DUPLICATE_IN_GRID,
  HAS_EXISTING_SHORTLINKS
}

Diese Statuswerte bilden die Grundlage dafür, dass die Oberfläche differenziert reagieren kann. Eine leere oder ungültige URL ist ein anderer Fall als ein bereits vorhandener Shortlink. Eine Dublette innerhalb des aktuellen Eingabeblocks ist fachlich etwas anderes als eine Dublette gegenüber der bereits bestehenden Arbeitsmenge. Und ein Warnzustand ist bewusst nicht automatisch mit einem blockierenden Fehler gleichbedeutend.

Die Zuordnung dieses fachlichen Status zum internen Arbeitsmodell erfolgt direkt beim Aufbau eines WorkItems. Die Validierungsergebnisse werden also nicht unverändert an das Grid weitergeleitet, sondern in ein zustandsbehaftetes View-Modell überführt.

CODE
public static WorkItem fromValidation(ValidationItemResult r) {
  final var item = new WorkItem();
  item.url = r.getNormalizedUrl() != null ? r.getNormalizedUrl() : r.getOriginalUrl();
  item.state = mapValidationStatus(r.getStatus());
  item.existingShortCodes = r.getExistingShortCodes() != null
      ? r.getExistingShortCodes()
      : List.of();
  item.existingShortlinkInfos = r.getExistingShortlinkInfos() != null
      ? r.getExistingShortlinkInfos()
      : List.of();
  item.message = r.getErrorMessage();
  return item;
}

Erst durch diese Überführung in ein internes Arbeitsmodell wird aus dem einmaligen Rückgabewert eines Endpunkts ein bearbeitbarer Zustand innerhalb der Oberfläche. Das Grid arbeitet folglich nicht direkt auf den Rohdaten des Validate-Responses, sondern auf einer stabilen Menge an WorkItems, die jeweils ihren aktuellen Status tragen.

Die eigentliche Validierung betrachtet nicht nur die einzelne URL isoliert, sondern auch deren Kontext innerhalb der Arbeitsmenge. Das wird bei Dubletten besonders deutlich. Neben der Erkennung identischer Einträge innerhalb des aktuellen Eingabeblocks wird auch geprüft, ob dieselbe URL bereits im bestehenden Work Set enthalten ist. Darüber hinaus werden sogar die HTTP- und HTTPS-Gegenstücke als konfliktrelevant behandelt.

CODE
if (gridUrls.contains(url)) {
  results.add(ValidationItemResult.duplicateInGrid(i, url));
  seenInBatch.add(url);
  continue;
}
final String counterpartUrl = swapProtocol(url);
if (counterpartUrl != null && gridUrls.contains(counterpartUrl)) {
  results.add(ValidationItemResult.duplicateInGrid(
      i, url,
      "URL already in work set as its " +
      (url.startsWith("https://") ? "http" : "https") +
      " counterpart: " + counterpartUrl));
  seenInBatch.add(url);
  continue;
}
if (seenInBatch.contains(url)) {
  results.add(ValidationItemResult.duplicateInBatch(i, url));
  continue;
}

An dieser Stelle wird deutlich, dass der Bulk-Workflow mengenorientiert arbeitet. Ein Eintrag ist nicht nur dann problematisch, wenn er für sich genommen ungültig ist, sondern auch dann, wenn er innerhalb der Arbeitsmenge oder im Verhältnis zu bereits bestehenden Varianten Konflikte erzeugt. Genau diese Sicht fehlt einer klassischen Einzelerfassung.

Wichtig ist dabei, dass nicht jeder problematische Zustand automatisch blockiert. Der Bulk-Workflow unterscheidet bewusst zwischen harten Fehlern und Warnungen. Eine ungültige URL oder eine Dublette verhindert die Erzeugung; ein bereits vorhandener Shortlink dagegen führt zunächst nur zu einem Warnzustand. Technisch wird dies in der View über unterschiedliche Prüfmethoden umgesetzt.

CODE
private boolean isBlocking(WorkItem item) {
  return switch (item.getState()) {
    case INVALID_URL, TOO_LONG, DUPLICATE_BATCH, DUPLICATE_GRID -> true;
    default -> false;
  };
}
private boolean isReadyToCreate(WorkItem item) {
  return switch (item.getState()) {
    case VALID, CONFIRMED_EXISTING -> true;
    default -> false;
  };
}

Diese Unterscheidung ist für die Benutzerführung zentral. Sie verhindert andererseits, dass fehlerhafte Einträge unbemerkt in die Erzeugung gelangen. Andererseits vermeidet sie, dass fachlich sinnvolle Sonderfälle — etwa das bewusste Erzeugen eines weiteren Shortlinks zu einem bereits bekannten Ziel — unnötig blockiert werden. Das System erzwingt hier also keine starre Logik, sondern führt zu einer bewussten Entscheidung.

Wie relevant diese Differenzierung ist, zeigt sich auch bei der Behandlung bereits vorhandener Shortlinks. Der Validate-Schritt liefert hierfür nicht nur einen Status, sondern zusätzlich angereicherte Informationen zu bestehenden Zuordnungen, darunter Aktivstatus, Ablaufzeit und Protokollvariante.

CODE
public static ValidationItemResult hasExisting(int index,
                                               String originalUrl,
                                               String normalizedUrl,
                                               List<ExistingShortlinkInfo> existingInfos) {
  var r = new ValidationItemResult();
  r.index = index;
  r.originalUrl = originalUrl;
  r.normalizedUrl = normalizedUrl;
  r.status = ValidationStatus.HAS_EXISTING_SHORTLINKS;
  r.existingShortlinkInfos = List.copyOf(existingInfos);
  r.existingShortCodes = existingInfos.stream()
      .map(ExistingShortlinkInfo::getShortCode)
      .toList();
  return r;
}

Ein solcher Eintrag ist fachlich nicht „kaputt“, erfordert aber eine Entscheidung. Deshalb bleibt er zunächst im Warnzustand, bis der Benutzer ihn ausdrücklich bestätigt. Genau daraus ergibt sich eine weitere Ebene des Bearbeitungsprozesses: Problematische Zeilen müssen nicht nur korrigiert, sondern teilweise auch bewusst freigegeben werden.

Diese Arbeitslogik zeigt sich auch in den Zeilenaktionen des Grids. Bearbeitung und Korrektur finden direkt dort statt, wo der Eintrag bereits Teil der Arbeitsmenge ist. Der Benutzer muss fehlerhafte URLs nicht zurück ins Textfeld kopieren, sondern kann direkt an der betroffenen Zeile arbeiten.

CODE
final var editBtn = new Button(new Icon(VaadinIcon.EDIT));
editBtn.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_TERTIARY);
editBtn.getElement().setProperty("title",
    tr(K_ACTION_EDIT_TT, "Correct URL inline"));
editBtn.addClickListener(_ -> {
  editor.editItem(item);
});
final var removeBtn = new Button(new Icon(VaadinIcon.TRASH));
removeBtn.addThemeVariants(
    ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
removeBtn.getElement().setProperty("title",
    tr(K_ACTION_REMOVE_TT, "Remove row from work set"));
removeBtn.addClickListener(_ -> {
  workItems.remove(item);
  refreshGrid();
  updatePrimaryButton();
  updatePreview();
});

Gerade dieser Punkt macht deutlich, warum das Grid im Bulk-Workflow nicht als Ergebnisliste verstanden werden darf. Es ist der Ort, an dem die Arbeitsmenge fachlich verändert wird. Entfernen, korrigieren, bestätigen und später erneut erzeugen sind keine Hilfsfunktionen neben der Oberfläche, sondern ihre eigentliche Aufgabe.

Dasselbe zeigt sich auch bei der Revalidierung bearbeiteter Einträge. Nach einer Änderung wird nicht das gesamte Textfeld erneut ausgewertet, sondern gezielt das betroffene WorkItem aktualisiert und wieder in den Zustand der Arbeitsmenge zurückgeführt.

CODE
final ValidationItemResult r = response.getResults().get(0);
item.setState(WorkItem.mapValidationStatus(r.getStatus()));
item.setExistingShortCodes(r.getExistingShortCodes());
item.setExistingShortlinkInfos(r.getExistingShortlinkInfos());
item.setMessage(r.getErrorMessage());
item.setConfirmed(false);
refreshGrid();
updatePrimaryButton();

Auch hier wird der Vorteil des Grid-basierten Arbeitsmodells deutlich. Der Bulk-Prozess ist nicht an einen monolithischen Einmalablauf gebunden, sondern erlaubt partielle Korrektur und gezielte Wiedereinordnung einzelner Zeilen innerhalb derselben Arbeitsmenge.

Aus Endanwendersicht ist genau diese Struktur entscheidend. Neue URLs werden oben eingefügt, die eigentliche fachliche Arbeit erfolgt jedoch im Grid. Dort wird sichtbar, welche Einträge gültig sind, welche problematisch bleiben, welche bereits vorhandene Shortlinks haben und welche bereits erfolgreich erzeugt wurden. Das Grid ist damit nicht nur ein komfortables Anzeigeelement, sondern auch die technische und fachliche Mitte des gesamten Bulk-Workflows.

Gerade in Vaadin spielt dieser Ansatz seine Stärke aus. Das Grid wird nicht nur zur Darstellung von Datensätzen verwendet, sondern auch als Träger eines zustandsbehafteten, interaktiven Bearbeitungsmodells. Erst dadurch wird aus einer Massenanlage ein kontrollierter Arbeitsprozess, in dem die Menge selbst im Zentrum steht und nicht mehr das einzelne Eingabefeld.

Ein Bulk-Workflow ist nicht nur dann anspruchsvoll, wenn Eingaben fehlerhaft sind. Ebenso relevant sind Situationen, in denen eine URL zwar formal gültig ist, aber bereits einen fachlichen Kontext mit sich bringt. Genau das gilt für Zieladressen, zu denen bereits Shortlinks existieren, oder für Fälle, in denen sich Einträge nur im Protokoll unterscheiden. Solche Konstellationen sind keine klassischen Fehler, dürfen jedoch nicht stillschweigend behandelt werden. Für Endanwender entsteht hier eine Entscheidungssituation, die von der Oberfläche sichtbar gemacht und unterstützt werden muss.

Im URL-Shortener wird dieser Kontext bereits im Validierungs-Schritt erstellt. Statt nur festzustellen, ob bereits Einträge zu einer Ziel-URL existieren, liefert die Validierung angereicherte Informationen zu den bestehenden Shortlinks zurück. Dazu gehören der Shortcode, der Aktivstatus, ein mögliches Ablaufdatum sowie die Information, ob es sich um eine Protokollvariante handelt.

CODE
public static final class ExistingShortlinkInfo {
  private String shortCode;
  private boolean active;
  private Instant expiresAt;
  private boolean protocolVariant;
  public ExistingShortlinkInfo() {
  }
  public ExistingShortlinkInfo(String shortCode,
                               boolean active,
                               Instant expiresAt,
                               boolean protocolVariant) {
    this.shortCode = shortCode;
    this.active = active;
    this.expiresAt = expiresAt;
    this.protocolVariant = protocolVariant;
  }
  public String getShortCode() {
    return shortCode;
  }
  public boolean isActive() {
    return active;
  }
  public Instant getExpiresAt() {
    return expiresAt;
  }
  public boolean isProtocolVariant() {
    return protocolVariant;
  }
}

Diese Struktur ist fachlich wesentlich aussagekräftiger als ein bloßer Hinweis auf die Existenz eines Eintrags. Für die Benutzer macht es einen Unterschied, ob bereits ein aktiver und gültiger Shortlink vorliegt oder ob der vorhandene Eintrag inaktiv oder abgelaufen ist. Ebenso ist es relevant, ob es sich um dieselbe Zieladresse handelt oder nur um deren HTTP- bzw. HTTPS-Gegenstück. Erst durch diese Informationen entsteht ein sinnvoller Entscheidungskontext.

Die Validierung erzeugt daraus bewusst keinen harten Fehler, sondern einen eigenen Warnzustand. Das zeigt sich an der Konstruktion des entsprechenden Validate-Ergebnisses.

CODE
public static ValidationItemResult hasExisting(int index,
                                               String originalUrl,
                                               String normalizedUrl,
                                               List<ExistingShortlinkInfo> existingInfos) {
  var r = new ValidationItemResult();
  r.index = index;
  r.originalUrl = originalUrl;
  r.normalizedUrl = normalizedUrl;
  r.status = ValidationStatus.HAS_EXISTING_SHORTLINKS;
  r.existingShortlinkInfos = List.copyOf(existingInfos);
  r.existingShortCodes = existingInfos.stream()
      .map(ExistingShortlinkInfo::getShortCode)
      .toList();
  return r;
}

Damit wird ein vorhandener Shortlink fachlich als Hinweis behandelt, nicht als automatische Sperre. Genau diese Unterscheidung ist für die Benutzerführung wichtig. Ein bereits bekannter Ziel-Link bedeutet nicht zwingend, dass kein weiterer Shortlink erzeugt werden darf. Gerade in einem System wie einem URL-Shortener kann es gute Gründe geben, mehrere Kurzlinks auf dieselbe Zieladresse zuzulassen, etwa für Kampagnen, getrennte Kommunikationskanäle oder organisatorische Trennung. Die Oberfläche darf deshalb nicht dogmatisch blockieren, sondern eine bewusste Entscheidung ermöglichen.

Wie diese Differenzierung technisch genutzt wird, zeigt die Arbeitslogik in der View. Dort gelten Einträge mit vorhandenen Shortlinks zunächst nicht als unmittelbar erstellbar, sondern müssen ausdrücklich bestätigt werden.

CODE
private boolean isBlocking(WorkItem item) {
  return switch (item.getState()) {
    case INVALID_URL, TOO_LONG, DUPLICATE_BATCH, DUPLICATE_GRID -> true;
    default -> false;
  };
}
private boolean isReadyToCreate(WorkItem item) {
  return switch (item.getState()) {
    case VALID, CONFIRMED_EXISTING -> true;
    default -> false;
  };
}

Der entscheidende Punkt ist, dass HAS_EXISTING für die Erzeugung noch nicht ausreicht. Erst nach einer expliziten Bestätigung wechselt ein solcher Eintrag in einen Zustand, der tatsächlich erstellt werden darf. Aus Endanwendersicht ist das ein klarer Gewinn: Die Oberfläche macht sichtbar, dass hier ein Sonderfall vorliegt, ohne die eigentliche Arbeitsabsicht unnötig zu blockieren.

Diese Bestätigung erfolgt nicht abstrakt, sondern auf Basis einer konkreten Detailansicht. Im Dialog zu vorhandenen Shortlinks werden nicht nur die Shortcodes angezeigt, sondern auch der Aktivstatus, der Ablaufstatus und die Protokollvarianten. Damit erhält der Benutzer genau die Informationen, die für eine begründete Entscheidung relevant sind.

CODE
for (final ExistingShortlinkInfo info : infos) {
  final String fullUrl =
      com.svenruppert.urlshortener.core.DefaultValues.SHORTCODE_BASE_URL + info.getShortCode();
  final var linkRow = new HorizontalLayout();
  linkRow.setSpacing(false);
  linkRow.getStyle().set("gap", "8px");
  linkRow.setAlignItems(Alignment.CENTER);
  final var linkComp = UiLinks.httpAwareLink(fullUrl, info.getShortCode());
  linkRow.add(linkComp);
  final var activeBadge = new Span(info.isActive() ? "✓ Active" : "✗ Inactive");
  activeBadge.getElement().getThemeList().add("badge pill small");
  activeBadge.getElement().getThemeList().add(info.isActive() ? "success" : "error");
  linkRow.add(activeBadge);
  final var expiryStatus = ExpiryBadgeFactory
      .computeStatusText(java.util.Optional.ofNullable(info.getExpiresAt()));
  final var expiryBadge = new Span(expiryStatus.text());
  expiryBadge.getElement().getThemeList().add("badge pill small");
  expiryBadge.getElement().getThemeList().add(expiryStatus.theme());
  linkRow.add(expiryBadge);
  if (info.isProtocolVariant()) {
    final var variantBadge = new Span("⚠ Protocol variant");
    variantBadge.getElement().getThemeList().add("badge pill small error");
    variantBadge.getElement().setProperty("title",
        "This shortlink targets the http/https counterpart of your URL");
    linkRow.add(variantBadge);
  }
  content.add(linkRow);
}

An diesem Ausschnitt wird deutlich, dass der vorhandene Datenbestand nicht nur als technische Vorabprüfung verstanden wird, sondern auch als Teil der eigentlichen Benutzerführung. Die Anwendung gibt dem Benutzer nicht einfach den Hinweis „bereits vorhanden“, sondern macht sichtbar, in welchem Zustand die vorhandenen Einträge sind und warum sie für die aktuelle Entscheidung relevant sein könnten.

Besonders wichtig ist dabei die Behandlung von HTTP und HTTPS. Für Endanwender wirkt eine URL häufig als dieselbe Adresse, unabhängig davon, ob sie mit http:// oder https:// beginnt. Technisch sind das jedoch unterschiedliche Werte. Würde die Anwendung diese Differenz unkommentiert behandeln, könnten beide Varianten nebeneinander existieren, ohne dass der Benutzer den inhaltlichen Zusammenhang oder das Sicherheitsproblem erkennt. Genau deshalb werden Protokollvarianten im URL-Shortener bewusst als Dubletten- und Warnkontexte modelliert.

Bereits im Validate-Handler wird geprüft, ob das Gegenstück einer URL mit umgekehrtem Protokoll bereits in der Arbeitsmenge vorhanden ist.

CODE
final String counterpartUrl = swapProtocol(url);
if (counterpartUrl != null && gridUrls.contains(counterpartUrl)) {
  results.add(ValidationItemResult.duplicateInGrid(
      i, url,
      "URL already in work set as its " +
      (url.startsWith("https://") ? "http" : "https") +
      " counterpart: " + counterpartUrl));
  seenInBatch.add(url);
  continue;
}

Zusätzlich wird der bestehende Datenbestand so indexiert, dass ein vorhandener Shortlink nicht nur unter seiner exakt gespeicherten URL gefunden wird, sondern auch unter der jeweiligen HTTP- bzw. HTTPS-Variante. Dabei wird der Fall ausdrücklich markiert.

CODE
private Map<String, List<ExistingShortlinkInfo>> buildExistingUrlIndex() {
  final Map<String, List<ExistingShortlinkInfo>> index = new HashMap<>();
  try {
    for (var mapping : store.findAll()) {
      final ExistingShortlinkInfo info = new ExistingShortlinkInfo(
          mapping.shortCode(), mapping.active(), mapping.getExpiresAt(), false);
      index.computeIfAbsent(mapping.originalUrl(), _ -> new ArrayList<>()).add(info);
      final String counterpart = swapProtocol(mapping.originalUrl());
      if (counterpart != null) {
        final ExistingShortlinkInfo variantInfo = new ExistingShortlinkInfo(
            mapping.shortCode(), mapping.active(), mapping.getExpiresAt(), true);
        index.computeIfAbsent(counterpart, _ -> new ArrayList<>()).add(variantInfo);
      }
    }
  } catch (Exception e) {
    logger().warn("Could not load existing mappings for duplicate-check; proceeding without", e);
  }
  return index;
}

Damit wird ein technischer Unterschied in einen fachlich sichtbaren Sicherheitskontext übersetzt. Die Anwendung erkennt nicht nur, dass zwei Ziele inhaltlich zusammengehören, sondern markiert zugleich, dass die HTTP-Variante sicherheitsrelevant problematisch ist. Genau an dieser Stelle zeigt sich, dass Sicherheitsbewusstsein nicht erst in Backend-Regeln oder Dokumentation beginnt, sondern bereits in der Oberfläche selbst verankert werden kann.

Diese Sicht wird zusätzlich durch eine wiederverwendbare UI-Hilfe unterstützt. HTTP-Links werden nicht neutral dargestellt, sondern direkt mit einem Warnsymbol versehen.

CODE
public static Component httpAwareLink(String url, String displayText) {
  if (url == null || url.isBlank()) {
    return new Anchor("", displayText != null ? displayText : "");
  }
  final String text = displayText != null ? displayText : url;
  final var a = new Anchor(url, text);
  a.setTarget("_blank");
  a.getElement().setProperty("title", url);
  if (url.startsWith("http://")) {
    final var warn = new Icon(VaadinIcon.EXCLAMATION_CIRCLE_O);
    warn.getElement().getStyle().set("color", "var(--lumo-error-color)");
    warn.getElement().getStyle().set("font-size", "var(--lumo-icon-size-s)");
    warn.getElement().getStyle().set("flex-shrink", "0");
    warn.getElement().setProperty("title", "⚠ Insecure HTTP – data is not encrypted");
    final var wrap = new HorizontalLayout(warn, a);
    wrap.setSpacing(false);
    wrap.getStyle().set("gap", "4px");
    wrap.setAlignItems(Alignment.CENTER);
    return wrap;
  }
  return a;
}

Aus Sicht der Benutzer ist das ein wichtiger Unterschied. Ein unsicherer Link erscheint nicht mehr als normaler Eintrag unter vielen, sondern als explizit markierter Sonderfall. In Kombination mit der Protokollvarianten-Erkennung und der Detailanzeige vorhandener Shortlinks entsteht ein UI-Modell, das nicht nur die Gültigkeit bewertet, sondern auch Entscheidungssituationen sichtbar macht.

Im Bulk-Workflow des URL-Shorteners bedeutet das: Die Oberfläche arbeitet nicht nur mit Kategorien wie korrekt oder fehlerhaft. Sie unterstützt auch dort, wo fachlich gültige Eingaben zusätzlichen Kontext benötigen. Bereits vorhandene Shortlinks, Sicherheitswarnungen und Protokollvarianten werden deshalb nicht versteckt, sondern in einen expliziten Entscheidungsprozess überführt. Genau das macht den Workflow aus Sicht des Endanwenders belastbar. Die Anwendung überlässt die Entscheidung nicht dem Zufall, sondern schafft die Voraussetzungen dafür, dass sie bewusst und informiert getroffen werden kann.

Erkenntnisse aus Endanwendersicht und Fazit

Mit der Weiterentwicklung des Bulk-Workflows im URL-Shortener zeigt sich deutlich, dass die Qualität einer Massenfunktion nicht in der bloßen Anzahl gleichzeitig verarbeitbarer URLs liegt. Entscheidend ist vielmehr, ob die Oberfläche den Zustand der Arbeitsmenge nachvollziehbar macht und den Benutzer sicher durch Prüfung, Bereinigung, Entscheidung und Erzeugung führt. Genau in diesem Punkt unterscheidet sich der aktuelle Stand grundlegend von einer naiven Massenanlage, die lediglich mehrere Eingaben entgegennimmt und am Ende ein Ergebnis zurückliefert.

Aus Endanwender-Sicht ist vor allem der Übergang von einer bloßen Eingabemaske hin zu einer geführten Arbeitsmenge spürbar. Die eigentliche Verbesserung liegt nicht darin, dass mehrere URLs gleichzeitig verarbeitet werden können, sondern darin, dass diese Anzahl während der Bearbeitung kontrollierbar bleibt. Der Validate-Create-Ansatz sorgt für eine klare Trennung zwischen Prüfen und Erzeugen. Das Grid übernimmt die Rolle des zentralen Arbeitsraums, in dem Einträge mit ihrem fachlichen Zustand sichtbar und bearbeitbar bleiben. Fehlerhafte URLs, Dubletten und Warnzustände verschwinden damit nicht mehr in einer nachgelagerten Fehlermeldung, sondern werden Teil des laufenden Bearbeitungsprozesses.

Ebenfalls deutlich verbessert hat sich die Qualität der Entscheidungen, die Benutzer im Bulk-Workflow treffen können. Bereits vorhandene Shortlinks werden nicht mehr nur als abstrakter Hinweis behandelt, sondern mit zusätzlichem Kontext angereichert. Aktivstatus, Ablaufzustand und Protokollvarianten geben den Benutzern eine belastbare Grundlage, um zu entscheiden, ob tatsächlich ein weiterer Shortlink erzeugt werden soll. Damit wird der Workflow fachlich präziser, weil die Anwendung nicht pauschal blockiert, sondern bewusst geführte Entscheidungen unterstützt.

Hinzu kommt, dass Sicherheitsaspekte inzwischen sichtbar Teil der Oberfläche geworden sind. Die Unterscheidung zwischen HTTP und HTTPS ist nicht mehr nur eine technische Nebenbedingung, sondern wird in der Benutzerführung explizit kenntlich gemacht. Unsichere HTTP-Ziele werden nicht mehr als neutrale Eingaben angezeigt, sondern als markierte Sonderfälle. Gleiches gilt für Protokollvarianten derselben Zieladresse, die sowohl als potenzielle Dublette als auch als Sicherheitshinweis verstanden werden. Für Endanwender ist das ein wichtiger Fortschritt, weil die Oberfläche dadurch nicht nur Daten verarbeitet, sondern auch Sicherheitsbewusstsein vermittelt.

Gerade in einer Vaadin-basierten Administrationsoberfläche zeigt sich an diesem Beispiel ein wesentlicher Gestaltungsgrundsatz: Eine gute Backoffice-Funktion entsteht nicht allein durch korrekte Geschäftslogik im Backend, sondern durch eine UI, die fachliche Zustände klar und konsistent sichtbar macht. Der Bulk-Workflow profitiert hier besonders davon, dass Grid, Dialoge, Statusmodelle und interaktive Komponenten in einem gemeinsamen, zustandsbehafteten UI-Modell zusammenwirken. Dadurch wird aus einer zunächst technischen Massenfunktion ein fachlich geführter Prozess.

Zugleich machen die bisherigen Änderungen auch sichtbar, welche Details für die praktische Nutzbarkeit entscheidend sind. Wiedererkennbare Icons, verständliche Button-Zustände, vollständige Feldbeschriftungen und konsistente Eingabemuster wirken auf den ersten Blick wie kleine UI-Themen. In der tatsächlichen Nutzung entscheiden sie jedoch darüber, ob eine Funktion selbsterklärend und belastbar wirkt oder zusätzlichen Interpretationsaufwand erfordert. Gerade im Bulk-Kontext, in dem mehrere Einträge gleichzeitig geprüft und bearbeitet werden, haben solche Details eine deutlich größere Wirkung als in linearen Einzelmasken.

Der bisherige Ausbau des Bulk-Workflows zeigt deshalb vor allem eines: Gute Massenfunktionen entstehen nicht dadurch, dass eine bestehende Create-Maske vergrößert oder um weitere Felder ergänzt wird. Sie entstehen durch ein eigenes Interaktionsmodell, das die Menge selbst in den Mittelpunkt stellt. Validierung, Arbeitsmenge, Warnungen, Sicherheitskontext und bewusste Freigabe sind keine Zusatzfunktionen neben der eigentlichen Erzeugung, sondern deren fachliche Voraussetzungen.

Damit ist zugleich der zentrale Befund aus Endanwender-Sicht formuliert. Die Verbesserung des Bulk-Workflows liegt nicht primär in mehr Funktionalität, sondern in mehr Führung. Die Oberfläche übernimmt heute deutlich stärker die Aufgabe, problematische Zustände sichtbar zu machen, Entscheidungen vorzubereiten und die Qualität der Arbeitsmenge vor der eigentlichen Erzeugung sicherzustellen. Genau darin liegt der eigentliche Fortschritt dieses Ausbaus — und genau darin zeigt sich, warum Vaadin für solche zustandsbehafteten Backoffice-Workflows eine besonders passende Grundlage bietet.

Happy Coding