Nebenläufigkeit & Thread-Sicherheit

Die Sicherheit von Threads ist das Erste, was erfahrene Java-Entwickler hinterfragen, wenn sie auf eine neue Abstraktion stoßen. Signals gehen diese Frage mit zwei grundlegend unterschiedlichen Ansätzen an, je nachdem, ob der Bundesstaat sitzungslokal oder sitzungsweit ist.

Lokale Signale haben eine eigene Sperre, die das Lesen und Schreiben zwischen dem UI-Thread einer Sitzung und den von dieser Sitzung ausgelösten Hintergrundaufgaben koordiniert. Daher ist jeder, der in derselben Sitzung ein ValueSignal aus einem CompletableFuture-Callback schreibt, sicher.

Geteilte Signale hingegen scheinen aus Anwendungssicht keine Sperre darzustellen. Sie verlassen sich auf die letztendliche Konsistenz über atomare Operationen hinweg und nicht auf kritische Abschnitte. Tief in der Infrastruktur existiert eine interne Sperre, aber der Anwendungscode beobachtet sie nie direkt: Es gibt keine synchronisierte Erfassung von Schließungen, keine explizite Erfassung und keine sichtbare Koordination. Stattdessen gibt jede mutierende Operation eine SignalOperation<T> zurück, deren Ergebnis asynchron bestätigt wird, sobald das Commit konfliktfrei ist.

Das Ergebnis ist eine API, bei der Thread-Sicherheit die Regel ist – kein Sonderfall, der später hinzugefügt wird.

Lokale Signale: Sitzungsgebunden, kein Teilen über Threads

Lokale Signale (ValueSignal, ListSignal) sind ausdrücklich nicht für Cross-Session-Zugriff ausgelegt – Cross-Thread-Zugriff innerhalb derselben Sitzung ist jedoch im Allgemeinen unproblematisch.

Der entscheidende Unterschied liegt in der Mutationsmethode. ‘set()’, ‘replace()’ und ‘update()’ arbeiten mit unveränderlichen Werten: Sie ersetzen die Referenz atomar und sind daher auch vor einer Hintergrundaufgabe derselben Sitzung sicher – wie einem ‘CompletableFuture’-Callback oder einem geplanten Job. Eine explizite Sperrung ist nicht erforderlich.

‘modify()’ ist ein Spezialfall: Diese Methode übergibt den aktuellen Wert direkt an eine Lambda, die ihn an Ort und Stelle ändern kann. Da eine veränderliche Objektreferenz ohne Sperrschutz an den Callback übergeben wird, erfordert ‘modify()’, dass die Sitzungssperre gehalten wird. Wenn dies nicht geschieht, erkennt ‘ValueSignal’ im Entwicklungsmodus die unsichere Nutzung und wirft eine ‘IllegalStateException’ – ein absichtliches Frühwarnsignal statt eines schwierig reproduzierenden Rennbedingungs.

‘modify()’ Für die meisten Anwendungsfälle wird daher empfohlen, unveränderliche Werte (z. B. Java-Datensätze) zusammen mit set() oder update() zu verwenden und modify() nur dann zu verwenden, wenn eine veränderliche Datenstruktur absolut notwendig ist und die Session-Sperre garantiert aufrechterhalten wird.

Local signal: use only in session lock

CODE
ValueSignal<String> title = new ValueSignal<>("Untitled");

//Correct: inside a Vaadin request (session lock held)
button.addClickListener(e -> title.set("Saved"));

//Correct: from background thread via     UI.access()
someExecutor.submit(() ->
    ui.access(() -> title.set("Updated from background"))
);

Auflistung 4.1 – Lokale Signale: Thread-Sicherheit durch Session Lock

Faustregel: Lokale Signale sind für den UI-Status gedacht, der nur den aktuellen Benutzer betrifft. Für einen Zustand, der mehrere Sitzungen teilen will, verwenden Sie immer Shared Signals.

Gemeinsame Signale: Asynchrone Signalbäume und Sitzungssperre

Geteilte Signale basieren auf einem Signalbaum – einer internen Datenstruktur, die alle Signalwerte als Knoten speichert und Änderungen als Befehle serialisiert. Für den Einzelserver-Fall wird ein LocalAsynchronousSignalTree verwendet: Befehle werden lokal ausgeführt, aber asynchron bestätigt.

Dies erklärt den zentralen Unterschied zur lokalen API: Mutierende Methoden auf geteilten Signalen geben kein Void zurück, sondern eine SignalOperation<T> – einen CompletableFuture-basierten Handle, der sofort nach Bestätigung des Befehls aufgelöst wird. In der Single-Server-Konfiguration geschieht dies fast sofort; In einem zukünftigen Cluster-Szenario könnte die Bestätigung nach einer Netzwerk-Hin- und Rückreise erfolgen.

CODE
SharedNumberSignal counter = new SharedNumberSignal(0);
SignalOperation contains the confirmed result as CompletableFuture
SignalOperation<Double> op = counter.incrementBy(1);
op.result().thenAccept(result -> {
    if (result.successful()) {
        result.value() = previous value at the time of confirmation
        System.out.println("Before: " + result.value());
    } else {
            Validator rejected the operation
        System.out.println("Rejected: " + ((SignalOperation.Error<?>) result).reason());
    }
});
peekConfirmed(): reads the last confirmed value (without optimistic updates)
double confirmed = counter.peekConfirmed();
peek(): reads including unconfirmed local changes
double optimistic = counter.peek();

Auflistung 4.2 – SignalOperation und peekConfirmed(): bestätigt vs. optimistischer Wert

Abbildung 4.1 – Asynchroner Signalbaum: Einreichen vs. bestätigen, optimistisch vs. bestätigt

Automatische wiederholbare Lesungen

Wiederholbare Lesungen gelten ausschließlich für gemeinsame Signale; Lokale Signale unterliegen keinem vergleichbaren Mechanismus, da sie nur innerhalb einer einzigen Sitzung verwendet werden.

Das Verhalten im Detail: In einem reaktiven Kontext – also einer Wirkung oder Transaktion – sieht der Code stets eine konsistente Momentaufnahme des Signalzustands. Änderungen innerhalb desselben Threads sind sofort sichtbar: Ein ‘incrementBy(1)’ gefolgt von einem ‘get()’ im selben Effekt gibt den aktualisierten Wert zurück. Änderungen, die während dieser Zeit von anderen Threads oder Sitzungen eintreffen, sind jedoch nicht sichtbar – sie werden nur im nächsten neu erstellten Kontext betrachtet, also während der nächsten Effektiteration oder der nächsten Transaktion.

Das ist die genaue Bedeutung von “wiederholbarer Lese” in diesem Kontext: nicht “der Wert friert ein”, sondern vielmehr “externe Schreiboperationen unterbrechen den laufenden Kontext nicht.” Das Ergebnis ist dasselbe wie bei ‘wiederholbarer Lesung’ in einer Datenbank – kein inkonsistenter Zwischenzustand, selbst wenn konkurrierende Änderungen eintreten.

CODE
//Automatic Repeatable Reads in Practice
//Assumption: counter = 5 at the beginning of the request

Signal.effect(this, () -> {
    int first = counter.getAsInt();  = 5
    //Other session writes counter = 6 (asynchronous)
    int second = counter.getAsInt();  //= 5(Repeatable Read!)
    //No inconsistent intermediate state possible
    assert first == second;
});
//Outside of a reactive context: peek() reads current state
int live = counter.peek();  //can be 6

Auflistung 4.3 – Wiederholbare Reads: Konsistente Reads innerhalb eines Effekts

Transaktionen: Gruppenoperationen atomar

Mehrere Signaloperationen können in einer Transaktion verknüpft werden. Die Transaktion wird entweder vollständig ausgeführt oder gar nicht ausgeführt – klassisches Alles-oder-Nichts-Prinzip. Wenn die Transaktion einen Signalwert ausliest, wird dieser als Voraussetzung registriert: Wenn eine konkurrierende Sitzung diesen Wert vor der Transaktion ändert, schlägt die Transaktion fehl.

Wichtig: Nur Shared Signals können in Transaktionen verwendet werden. ValueSignal und ListSignal werfen eine IllegalStateException aus, wenn sie innerhalb von Signal.runInTransaction() aufgerufen werden.

CODE
SharedNumberSignal balance = new SharedNumberSignal(100);
SharedNumberSignal reserved = new SharedNumberSignal(0);
//Transaction: subtract balance AND increase reserved – or neither.
TransactionOperation<Void> tx = Signal.runInTransaction(() -> {
    double amount = 25.0;
    //Read registered as precondition: fails if
    //balance changes concurrent
    if (balance.get() >= amount) {
        balance.incrementBy(-amount);
        reserved.incrementBy(amount);
    }
});
tx.result().thenAccept(result -> {
    if (result.successful()) {
        Notification.show("Reservation successful");
    } else {
        //Conflict: concurrent change, repeat transaction
        Notification.show("Conflict: " + result.reason());
    }
});
//Transaction with no return value
Signal.runInTransaction(() -> {
    counter.incrementBy(1);
    status.set("updated");
});

Auflistung 4.4 – Transaktionen: atomare Gruppen mit Vorbedingungsprüfung

Abbildung 4.2 – Transaktionsfluss: Phase, Precondition-Check, Commit oder Rollback

Serialisierbarer und Cluster-Ausblick

Shared Signals implementieren Serializables – nicht als Nebenprodukt, sondern als bewusste Vorbereitung auf Clustering. Die Implementierung von LocalAsynchronousSignalTree ist auf einen einzelnen JVM-Prozess beschränkt; der Befehlsbestätigungsmechanismus (submit/confirm) ist jedoch so gestaltet, dass ein externer Cluster-Koordinator dieselbe Schnittstelle implementieren kann.

Vaadin 25.1 verfügt noch nicht über eine produktionsreife Cluster-Implementierung –die Architektur ist jedoch so gestaltet, dass sie ohne Änderung der Signal-API implementiert werden kann. Der Quelltext von AsynchronousSignalTree beschreibt dies explizit: Befehle werden in einem Ereignisprotokoll gespeichert und können von einer anderen AsynchronousSignalTree-Instanz im Cluster bestätigt werden.

Today: Shared Signal on a JVM Process

CODE
SharedNumberSignal counter = new SharedNumberSignal(0);
//→ LocalAsynchronousSignalTree: Confirmation immediately
//Future (cluster): same API, different tree implementation
SharedNumberSignal counter = ClusteredSignalFactory
.forTopic("counters")
.number("page-visits");
//→ ClusterAsynchronousSignalTree: Acknowledgment after network round-trip
SignalOperation.result() //CompletableFuture resolves asynchronously

Auflistung 4.5 – Clustering-Aussicht: gleiche API, austauschbarer Baum-Implementierung

Praktische Anmerkung: Solange kein Cluster-Koordinator verfügbar ist, sind geteilte Signale auf den Lebenszyklus des Anwendungsservers beschränkt. Ein Neustart verliert alle gemeinsamen Signalwerte. Für den persistenten Zustand solltest du weiterhin auf eine Datenbank und die Service-Schicht zurückgreifen.

Migration aus Vaadin 25.0 / 24.x

Die Signals-API hat zwischen der Vorschau (24.8 / 25.0) und der produktionsfertigen Version (25.1) eine Reihe fehlerhafter Änderungen erhalten. Die meisten lassen sich mit einem einfachen Suchen und Ersetzen beheben – manche erfordern nur minimale Refaktorisierung. Dieser Abschnitt listet alle Änderungen vollständig auf, priorisiert nach Aufwand.

GebietVorschau / 24.x25.1 (Beta3)
Paketcom.vaadin.signals.*com.vaadin.flow.signals.*
Lokaler Wertneues ValueSignal<>(val, String.class)neues ValueSignal<>(val)
Lesensignal.value()signal.get() / signal.peek()
Schreibensignal.value(newVal)signal.set(newVal)
WirkungComponentEffect.effect (dies, fn)Signal.effect(this, fn)
Geteilte NummerSignalFactory.IN_MEMORY_SHARED.number(“key”)new SharedNumberSignal(0)
WritableSignalWritableSignal<T>-Schnittstelleweggelassen – direkt ValueSignal<T>
Feature-FlaggeFLOW_FULLSTACK_SIGNALS=wahrNicht anwendbar – standardmäßig aktiv
Gemeinsame ListeListSignal<T> (geteilt)SharedListSignal<T>
Gemeinsame KarteMapSignal<V>SharedMapSignal<V>

*   Tabelle 6.1 – Migrationsübersicht: Vorschau / 24.x → 25.1*

   Paketänderung: interne → öffentliche API

Die wichtigste Änderung ist die Paketänderung. In der Vorschau waren alle Signalklassen unter com.vaadin.flow.internal.signals.* – einem Paket, das absichtlich als intern ohne Stabilitätsgarantie gekennzeichnet war. In 25.1 befinden sich die öffentlichen Klassen in com.vaadin.flow.signals.* und deren Unterpaketen local.* und shared.* .

Before (Preview)

CODE
import com.vaadin.flow.internal.signals.NumberSignal;
import com.vaadin.flow.internal.signals.ValueSignal;
import com.vaadin.flow.component.ComponentEffect;

After (25.1)

CODE
import com.vaadin.flow.signals.local.ValueSignal;
import com.vaadin.flow.signals.shared.SharedNumberSignal;
import com.vaadin.flow.signals.Signal;  for Signal.effect()

*   Auflistung 6.1 – Paketwechsel: internal.signals → signals.local / signals.shared*

get() / set() statt value()

Die Methoden value() und value(T) wurden in get() und set(T) umbenannt. Die neue Benennung folgt den Java-Konventionen und unterscheidet klar zwischen reaktivem Lesen (get() – nur im reaktiven Kontext) und nicht-reaktivem Lesen (peek() – überall).

Before

CODE
String current = nameSignal.value();         //read
nameSignal.value("Max");                     //write
String safe = nameSignal.peek();             //unchanged

After

CODE
String current = nameSignal.get();           //read (reactive context)
nameSignal.set("Max");                       //write
String safe = nameSignal.peek();             //unchanged

*   Auflistung 6.2 – value() → get() / set(): einfaches Suchen und Ersetzen*

Regex for Find-and-Replace: \value\\ → .get() and \value\([^)]+)\ → .set($1). Apply in IntelliJ with Regex mode enabled.

Feature-Flagge FLOW_FULLSTACK_SIGNALS weggelassen

In der Vorschau mussten Signale explizit über ein Feature-Flag aktiviert werden. In 25.1 ist die API standardmäßig aktiv – die Flagge existiert nicht mehr.

Before: in vaadin-featureflags.properties or as a system property

com.vaadin.experimental.fullstackSignals=true

After: nothing necessary – signals are active by default

Auflistung 6.4 – Feature-Flag entfernen

Überprüfen: Nach dem Upgrade suchen und entfernen Sie fullstackSignals sowie FLOW_FULLSTACK_SIGNALS in Konfigurationsdateien und Systemeigenschaften.

SignalFactory ist nicht mehr notwendig: direkte Konstruktoren

In der Vorschau wurden geteilte Signale mit ‘SignalFactory.IN_MEMORY_SHARED.number(“demo:counter”)’ erzeugt – der String-Schlüssel diente als Suchmechanismus, um dieselbe Signalinstanz an mehreren Stellen im Code zu referenzieren. In Version 25.1 wurde die Factory entfernt; SharedSignals werden direkt instanziiert.

Für einfache Szenarien mit einer festen Anzahl bekannter Signale ist ein dedizierter ‘AppSignals’-Container die sauberste Lösung:

CODE
public final class AppSignals {
    public static final SharedNumberSignal PAGE_VISITS =
        new SharedNumberSignal(0);`
    private AppSignals() {}
}

Wenn Sie jedoch viele dynamische Signalinstanzen benötigen, die mit einem String-Schlüssel nachgeschlagen werden können – zum Beispiel ein Signal pro Raum in einer Chatanwendung oder ein Signal pro Sensor-ID – wird ein ConcurrentHashMap auf Service-Ebene empfohlen:

CODE
@ApplicationScoped // or static final in a utility class
public class RoomSignals {
    private final ConcurrentHashMap<String, SharedNumberSignal> counters
        = new ConcurrentHashMap<>();
    public SharedNumberSignal forRoom(String roomId) {
        return counters.computeIfAbsent(
            roomId, id -> new SharedNumberSignal(0));
        }
}

computeIfAbsent() ist threadsicher: Selbst wenn zwei Sitzungen gleichzeitig dieselbe roomID anfordern, wird die Signalinstanz genau einmal erstellt.

ComponentEffect.effect() → Signal.effect()

Der Einstiegspunkt für Effekte hat sich verschoben: ComponentEffect.effect(component, fn) wurde durch Signal.effect(component, fn) ersetzt. Die Signatur ist identisch.

Before

ComponentEffect.effect(this, () -> label.setText(counter.get()));

After

Signal.effect(this, () -> label.setText(counter.getAsInt() + “”));

*   Auflistung 6.6 – ComponentEffect.effect() → Signal.effect()*

UI.access()-Muster Schritt für Schritt

Bestehende UI.access()-Muster müssen nicht unbedingt sofort migriert werden. Signals und UI.access() können gleichzeitig existieren. Der empfohlene Migrationspfad ist inkrementell: Neue Funktionen mit Signals implementieren, bestehende UI.access()-Muster bei Bedarf neu schreiben.

Before: manual UI.access() for shared counter

CODE
private static final AtomicInteger counter = new AtomicInteger(0);
int newValue = counter.incrementAndGet();
VaadinSession.getCurrent().getUIs().forEach(ui ->
    ui.access(() -> label.setText("Counter: " + newValue))
);

After: SharedNumberSignal

CODE
private static final SharedNumberSignal counter =
    new SharedNumberSignal(0);
Signal.effect(this, () ->
    label.setText("Counter: " + counter.getAsInt())
);
counter.incrementBy(1);  //update all connected sessions automatically

*   Auflistung 6.7 – UI.access() + AtomicInteger → SharedNumberSignal + Signal.effect()*

Das Muster ist konsistent: Verschiebe den Zustand auf ein gemeinsames Signal, ersetze den UI.access()-Broadcast durch Signal.effect(), ruf die Mutationsoperation über die Signal-API auf. Der Broadcast-Mechanismus ist komplett abgeschafft – die Signal Tree-Infrastruktur übernimmt das.

Entscheidungsleitfaden

Signale, EventBus und Observer Pattern lösen verwandte, aber nicht identische Probleme. Die folgende Tabelle fasst die Entscheidungskriterien kompakt zusammen – als Referenz für Code-Reviews, Architektur-Entscheidungsaufzeichnungen und die Einarbeitung neuer Teammitglieder.

Immobilienvergleich

Situation / KriteriumLokales SignalGemeinsames SignalEventBusBeobachter
UI-Zustand eines einzelnen Benutzers   ✓
Teile den Stand mehrerer Sitzungen   ✓   (✓)   (✓)
Aktueller Wert zu jedem Zeitpunkt verfügbar   ✓   ✓
Automatisches UI-Update ohne Push   ✓   ✓
Change Atomic (kein verlorenes Update)   ✓
Transaktionen über mehrere Bereiche hinweg   ✓   ✓
Domain-Events (OrderPlaced usw.)   ✓   (✓)
Lose Kopplung von Publisher/Abonnent   ✓
Neue Abonnenten sehen den Anfangswert   ✓   ✓
Keine manuelle Abmeldung erforderlich   ✓   ✓
Serialisierbar / Cluster-vorbereitet   ✓
Verwendbar in Signaltransaktionen   ✓

Tabelle 7.1 – Eigenschaftsvergleich: Lokales Signal, Gemeinsames Signal, EventBus, Beobachter

   Legende: ✓ = vollständig unterstützt (✓) = möglich mit zusätzlichem Aufwand – = nicht unterstützt / nicht beabsichtigt

Schnelle Entscheidung: 7 Fragen

Die folgende Tabelle ist als schneller Entscheidungsbaum gedacht. Die erste Frage, auf die die Antwort “ja” lautet, gibt die Empfehlung.

FrageEmpfehlung
Ist es der UI-Zustand, der nur einen Nutzer betrifft?   → Lokales Signal (ValueSignal / ListSignal)
Müssen mehrere Sitzungen denselben Bundesstaat sehen?   → gemeinsames Signal (SharedNumberSignal, SharedValueSignal, …)
Brauche ich atomare Operationen (z. B. Inkrement)?   → SharedNumberSignal mit incrementBy()
Ist es ein Domain-Event (ist etwas passiert)?   → EventBus – Signale sind kein Ersatz für Domänenereignisse
Brauche ich Transaktionen über mehrere Signale hinweg?   → Gemeinsame Signale + Signal.runInTransaction()
    Möchte ich das bestehende UI.access()-Muster ersetzen?   → Gemeinsames Signal + Signal.effect()

Tabelle 7.2 – Schnelle Entscheidung: 7 Fragen zur korrekten Abstraktion

Wenn Signals explizit nicht tut

Signale sind kein universelles Werkzeug. Es gibt Situationen, in denen sie die falsche Wahl sind:

•  Domain-Ereignisse wie OrderPlaced, PaymentFailed oder UserRegistered gehören zu einem EventBus. Sie stehen für etwas, das passiert ist – keinen Zustand, der erhalten werden muss. Ein Abonnent, der ein solches Ereignis verpasst, sollte es nicht automatisch erhalten.

•  Der persistente Zustand gehört in eine Datenbank. Geteilte Signale sind speicherbasiert und verlieren ihren Zustand, wenn der Server neu gestartet wird. Sie sind kein Cache-Ersatz und kein Ersatz für einen verteilten Store wie Redis.

•  Komplexe Berechnungen gehören zur Dienstschicht und nicht zu einem berechneten Signal. Signal.cached() ist für kleine, sitzungslokale Transformationen gedacht – nicht für Datenbankabfragen oder externe API-Aufrufe innerhalb eines Effekts.

•  Dienstübergreifende Kommunikation im Kontext eines Microservices gehört zu einem Nachrichtenbroker (Kafka, RabbitMQ). Geteilte Signale sind für den Zustand innerhalb einer Vaadin-Anwendung konzipiert, nicht für dienstübergreifende Ereignisse.

Faustregel: Signale beantworten die Frage: “Wie ist der aktuelle Zustand?” Ein EventBus antwortet: “Was ist gerade passiert?” Wenn du über die Antwort nachdenken musst, ist das ein Hinweis darauf, welches Muster besser passt.

Fazit

Was Signale wirklich für serverseitige Java-UIs bedeuten

Vaadin Flow hatte schon immer einen klaren Vorteil: serverseitiges Rendering mit vollständiger Java-Sicherheit, ohne einen separaten Frontend-Stack. Der Preis dafür war bisher ein mentales Modell, das beträchtliche Boilerplate für Cross-Session-State erzeugt – UI.access(), manuelles Broadcast-Management, AtomicInteger als Zustandshalter zusätzlich zum eigentlichen UI-Code.

Signale beheben dieses strukturelle Problem. Sie sind kein syntaktischer Zucker und keine neue Designmode – sie stellen eine grundlegende Veränderung im Programmiermodell dar: Der Zustand wird erklärt, nicht zwangsläufig verwaltet. Der Entwickler beschreibt, was der Zustand ist und wie er transformiert wird. Das Framework sorgt dafür, wer wann und wie threadsicher informiert werden muss.

Signals beantwortet eine Frage, die Vaadin-Entwickler seit Jahren stellen: “Wie halte ich mehrere Browser-Tabs synchron, ohne UI.access() über mein gesamtes Backend zu verteilen?” Die Antwort lautet nun eine Linie: neues SharedNumberSignal(0).

Das ist keine Übertreibung. Der Unterschied zwischen dem klassischen Broadcast-Muster aus Kapitel 1 und dem SharedNumberSignal-Beispiel aus Kapitel 3 ist real: weniger Code, klare Aufgaben, keine vergessenen UI.access()-Aufrufe, keine Referenzlecks. Für Teams, die Vaadin-Anwendungen mit Live-Dashboards, kollaborativen Formularen oder Echtzeit-Feeds betreiben, ist dies ein erheblicher Qualitätsgewinn.

Was Vaadin mit dieser Veröffentlichung ebenfalls demonstriert: Das Signals-Konzept – bekannt aus JavaScript-Frameworks wie Solid.js, Angular Signals oder MobX – kann vollständig in einem typsicheren, JVM-nativen Programmiermodell implementiert werden. Kein Observable<T> von RxJava, kein reaktiver Strom, der sich durch den Stack ausbreitet. Nur Signal<T>, get(), set() und Signal.effect().

Offene Fragen: Cluster, Performance, Testbarkeit

So überzeugend die Veröffentlichung auch ist, gibt es offene Punkte, die erfahrene Java-Entwickler bei einer Produktionsentscheidung berücksichtigen sollten.

Cluster-Unterstützung. Die aktuelle Implementierung basiert auf LocalAsynchronousSignalTree – speicherresident, Einzel-JVM. Für hochverfügbare Deployments mit mehreren Instanzen gibt es noch keine produktionsbereite Cluster-Implementierung. Die Architektur (AsynchronousSignalTree mit Submit/Confirmation-Protokoll ) ist darauf ausgelegt, einen externen Koordinator zu verbinden – aber wann und in welcher Form dies geliefert wird, ist zum Zeitpunkt dieses Artikels noch offen.

Leistung unter Belastung. Gemeinsame Signale serialisieren Werte über JSON und Koordinatenänderungen über ein Signalbaum-Schloss. Für typische UI-Zustandsszenarien (Zustände, die sich in Intervallen von Sekunden oder Minuten ändern) ist dies kein Problem. Für Hochfrequenz-Updates (Ticks, Sensordaten, Streaming-Metriken) sollten Sie messen: Ab wann wird der Signalbaum zum Engpass? Vaadin hat dazu bisher keine öffentlichen Benchmarks veröffentlicht. Aber man muss bedenken, dass ein Wert, der häufiger als einmal pro Sekunde aktualisiert wird, bereits in der Serviceschicht debounced oder gedrosselt werden sollte.

Testbarkeit. Effekte, die an die Lebenszyklen der Komponenten gebunden sind, machen klassische Unit-Tests schwieriger. Ein Signal.effect (this, fn) läuft nur, wenn die Komponente angeschlossen ist. Wenn du Effekte isoliert testen willst, musst du entweder zu Signal.unboundEffect(fn) wechseln oder die neue BrowserlessTest-Infrastruktur (ehemals TestBench UiUnitTest) nutzen, die in 25.1 kostenlos verfügbar ist. Die Testabdeckung für signalbasierte Ansichten ist mit dieser Infrastruktur leicht machbar – erfordert jedoch ein Überdenken im Vergleich zu klassischen @Mock-basierten Tests.

Reife des Ordners. Die Integration von valueSignal() und validationStatusSignal() in den Ordner ist neu. In komplexen Formszenarien mit benutzerdefinierten Validatoren, Konverterketten und dynamischen Feldern sind Randfälle weiterhin zu erwarten, die in späteren Patch-Releases ausgeglättet werden.

Vaadin Signals 25.1 ist das erste Mal seit Jahren, dass sich das serverseitige Java-UI-Modell grundlegend weiterentwickelt hat – nicht durch neue Komponenten oder besseres Design, sondern durch eine neue Grundlage für das State-Management. Jeder, der heute die API lernt, investiert in etwas, das die Entwicklung von Vaadin in den kommenden Jahren prägen wird.

Der Code ist öffentlich, der Quellcode ist offen und die Community ist aktiv. Es ist ein guter Zeitpunkt, damit anzufangen.