Motivation: Was war bisher das Problem?
Jeder, der schon längere Zeit mit Vaadin Flow arbeitet, kennt zwei Arten von Schmerz – und beide werden angesprochen.
Das Erste ist offensichtlich: Eine Hintergrundoperation schreibt Daten, mehrere Browser-Tabs müssen synchronisiert werden, und plötzlich bist du tief in UI.access(), @Push und der Frage, warum Tab B nicht reagiert, obwohl Tab A gerade etwas geschrieben hat.
Die zweite ist subtiler, aber grundlegender: Selbst in rein lokalem, Einzelbenutzer-UI-Code – einem Formular, einem Einkaufswagen, einer detaillierten Ansicht – ist der Zustand über Komponentenvariablen verteilt, manuell synchronisiert und zwingend verwaltet. Wenn sich ein Wert ändert, ruft man component.setValue() auf, was ein Label aktualisiert und den Status des Buttons festlegt. Der Entwickler koordiniert, was geändert werden muss – nicht das Framework.
Signale verschieben dieses Modell grundlegend: hin zu “UI als Funktion des Zustands”. Der Zustand wird deklarativ beschrieben, die Benutzeroberfläche wird daraus abgeleitet, und das Framework übernimmt die Synchronisation – unabhängig davon, ob die Änderung lokal, aus einem Hintergrundthread oder aus einer anderen Sitzung stammt. Die Nutzung von mehreren Sitzungen ist keine Ausnahme mehr, sondern eine natürliche Folge derselben Architektur.
Bevor wir verstehen, wie das in der Praxis funktioniert, lohnt es sich, sich die bisher begegneten Schmerzpunkte anzusehen.
UI.access(), Push- und manuelle Synchronisation
Das Servermodell von Vaadin ist sitzungsgebunden: Jede HTTP-Sitzung erhält ihre eigene UI-Instanz, und Komponenten können nur innerhalb des entsprechenden Request-Threads modifiziert werden. Das steht fest – aber es erzwingt Boilerplate, sobald der Zustand aus einem anderen Thread in die Benutzeroberfläche wechselt.
Das klassische Muster für einen Split-Meter sieht in der Praxis so aus:
//Classic: manual UI.access() for cross-thread updates
private static final AtomicInteger sharedCounter = new AtomicInteger(0);
//Somewhere in the background thread or after a click in Tab A:
VaadinSession.getCurrent().getUIs().forEach(ui ->
ui.access(() -> {
counterLabel.setText("Value: " + sharedCounter.incrementAndGet());
})
);Auflistung 1.1 – Klassisches UI.access()-Muster für Cross-Session-Updates
Der Code funktioniert – weist aber strukturelle Schwächen auf. Die Broadcast-Logik ist manuell: Sie iterieren über alle aktiven UIs, müssen sich um die Sperrung kümmern (ui.access() erwirbt die Sitzungssperre intern) und sind dafür verantwortlich, dass keine UI-Referenzen verloren gehen. Wenn ein Nutzer sich abmeldet, muss die Registrierung aktiv bearbeitet werden. Außerdem sind der Zustand (AtomicInteger) und die Darstellung (Label) lose gekoppelt – es gibt keinen einzigen Wahrheitspunkt, der sie synchron hält.
Das eigentliche Problem ist nicht UI.access() selbst, sondern dass der Entwickler die Verbindung zwischen Zustands- und UI-Darstellung manuell aufrechterhalten muss – bei jeder Änderung, aus jedem Thread.
Observer & EventBus: Warum sie für UI-State nicht ausreichen
Das Beobachter-Muster koppelt Beobachter direkt mit dem Subjekt. Das ergibt für Domain-Events Sinn, ist aber für UI-State unpraktisch: Der Beobachter muss sich registrieren und wieder de-registrieren, den aktuellen Zustand separat abfragen, wenn das Ereignis eintritt, und dennoch UI.access() aufrufen, um die Komponente thread-safe zu aktualisieren.

Abbildung 1.1 – Beobachtermuster: Das Subjekt benachrichtigt alle registrierten Beobachter direkt per Notify(). Keine Zustandspersistenz – Der Beobachter muss get() auf das Subjekt selbst aufrufen.
Abbildung 1.1 – Beobachtermuster: enge Kopplung, kein reaktiver Zustand
Ein EventBus entkoppelt das noch weiter – Verlage und Abonnenten kennen sich nicht. Aber es ist von Natur aus stateless: Ein Abonnent, der sich nach dem letzten Event registriert hat, sieht keinen aktuellen Wert. Es gibt keine Antwort auf die Frage: “Wie ist der aktuelle Status?”
EventBus approach: stateless, no current value can be retrieved
eventBus.subscribe(CounterChangedEvent.class, event -> {
//What if we missed the first event?
ui.access(() -> counterLabel.setText("Value: " + event.newValue()));
});New tab opens — what value should it initially show?
The EventBus doesn’t know. You need a separate state source.
Listing 1.2 – EventBus: kein Anfangszustand, kein automatisches UI-Update

Abbildung 1.2 – EventBus: lose Kopplung, aber kein persistenter Zustand
In der Praxis erhält man eine Kombination aus einem EventBus zur Benachrichtigung, einem separaten Zustandsobjekt und UI.access() für den Threadwechsel. Drei Konzepte, die man konsistent halten muss.
| Herangehensweise | Kann der Zustand zurückgerufen werden? | Gewindesicherung | Automatische UI-Aktualisierung |
| UI.access() + AtomicXxx | ✓ Ja | Manuell | Manuell iterieren |
| Beobachtermuster | ✗ Ja | Manuell | manual + UI.access() |
| EventBus | ✗ Ja | Manuell | manual + UI.access() |
| Signale (25,1) | ** ✓ jederzeit über get()** | Eingebaut | automatisch über Effekt |
Signale ersetzen keinen EventBus für Domain-Events – OrderPlaced oder PaymentFailed gehören weiterhin auf einem Bus. Signale lösen das spezifischere Problem: einen reaktiven UI-Zustand, der über mehrere Sitzungen hinweg lesbar und beschreibbar ist, ohne dass der Entwickler selbst Threading, Broadcast oder Bereinigung koordinieren muss. Kapitel 2 zeigt, wie das konkret funktioniert.
Das Signalmodell – Konzept und Denkweise
Signale sind keine neue Idee. Reaktive Programmierung, Tabellenkalkulationszellen, MobX Solid.js – sie alle basieren auf demselben Kern: dem Zustand als beobachtbarer Wert, von dem abhängige Berechnungen automatisch abgeleitet werden. Was Vaadin 25.1 besonders macht, ist seine Implementierung in der JVM-Welt: typsicher, sitzungsbewusst, threadsicher – und ohne JavaScript in Sicht.
In der Praxis sind Signale auf zwei Arten mit der Benutzeroberfläche verbunden. Die gebräuchlichere Methode erfolgt über Signalbindungen: Methoden wie ‘bindText()’, ‘bindValue()’ oder ‘bindProperty()’ verknüpfen einen Signalwert direkt mit einer Komponenteneigenschaft, ohne dass ein expliziter Effekt erforderlich ist. Dies ist der Standardansatz für einfache reaktive Displays.
ValueSignal<String> title = new ValueSignal<>("Untitled");
// Binding: no explicit effect needed
span.getElement().bindText(title);
textField.bindValue(title, title::set);Der zweite Ansatz ist ein explizites ‘Signal.effect()’ – das allgemeinere, flexiblere Werkzeug für Fälle, in denen mehrere Signale kombiniert, Berechnungen durchgeführt oder Nebenwirkungen ausgelöst werden müssen. Ein späteres Kapitel behandelt Einbände im Detail; es erklärt zunächst das zugrunde liegende Modell, auf dem beide Ansätze basieren.
Ziehen statt schieben: Abhängigkeitsverfolgung unter der Haube
Das klassische Beobachter-Muster ist push-basiert: Das Subjekt ruft alle registrierten Zuhörer aktiv an. Signale funktionieren umgekehrt: Sie basieren auf Pull-Signalen. Niemand registriert sich explizit. Stattdessen ruft ein reaktiver Kontext get() auf – und das Signal merkt sich automatisch, wer liest.
Mit dem nächsten Set() macht das Signal alle Kontexte, die es zuvor gelesen hat, ungültig und plant deren erneutes Ausführen. Das Framework führt das Tracking komplett implizit aus – kein addListener(), kein removeListener(), kein Speicherleck durch vergessene Abmeldung.

Abbildung 2.1 – Pull-basierte Abhängigkeitsverfolgung: get() registriert die Abhängigkeit implizit, set() macht alle abhängigen Kontexte ungültig
Der entscheidende Unterschied: Beim Beobachtermuster muss sich der Abonnent registrieren. Bei Signals registriert sich das Framework automatisch bei der ersten Messung. Das Ergebnis ist dasselbe, aber der Code wird deklarativ statt imperativ:
Imperative (Observer): explicit registration, manual cleanup
counter.addObserver(this); easy to forget when closing the view
Declarative (signal): Dependency arises when reading
Signal.effect(this, () -> label.setText(String.valueOf(counter.get())));
Framework registers on the first get() and cleans up the view
Auflistung 2.1 – Beobachter vs. Signal: Implizite Abhängigkeitsverfolgung ersetzt manuelle Registrierung
Reaktiver Kontext: Wann get() legal ist – und wann nicht
Hier liegt der wichtigste konzeptionelle Unterschied zu einer normalen Variable: get() darf nur innerhalb eines reaktiven Kontexts aufgerufen werden. Außerhalb – zum Beispiel direkt im Konstruktor oder in einem Button-Click-Handler – wirft die API eine IllegalStateException.
Regel: signal.get() ist nur in Effekten und berechneten Signalen erlaubt. Verwenden Sie immer signal.peek() im Konstruktor, in Click-Handlern und in Service-Methoden – dies liest den aktuellen Wert aus, ohne eine Abhängigkeit zu registrieren.
ValueSignal<String> name = new ValueSignal<>(“Max”);
Correct: get() in the reactive context (Effect)
Signal.effect(this, () -> label.setText(name.get()));
Correct: peek() outside – reads without dependency registration
button.addClickListener(e -> {
String current = name.peek(); No reactive context needed
service.save(current);
});Error: get() out of reactive context
String val = name.get(); → IllegalStateException
Auflistung 2.2 – get() vs. peek(): der reaktive Kontext entscheidet über die richtige Methode
Diese Einschränkung ist kein Mangel, sondern ein bewusstes Design: Das Framework kann korrekte Abhängigkeiten nur nachverfolgen, wenn es weiß, in welchem Kontext ein Wert gelesen wird. Die IllegalStateException ist ein frühes Anzeichen für fehlplatzierten Code – besser zur Entwicklungszeit als ein stiller Fehler in der Produktion.

Abbildung 2.2 – Reaktiver Kontext: get() und peek() auf einen Blick
Berechnete Signale als typsichere Transformationskette
Berechnete Signale sind das leistungsstärkste Werkzeug im Signalmodell. Sie beschreiben einen abgeleiteten Wert als reine Funktion ihres Quellsignals – deklarativ, typsicher und zwischengespeichert. Das Framework berechnet den Wert nur neu, wenn sich mindestens eine Quelle geändert hat.
ValueSignal<String> firstName = new ValueSignal<>("Max");
ValueSignal<String> lastName = new ValueSignal<>("John Doe");
//Computed: automatically up-to-date, cached between changes
Signal<String> fullName = firstName.map(f ->
f + " " + lastName.get() // lastName is also dependency
);
//Chain: Signal → Computed → further Computed
Signal<String> greeting = fullName.map(n ->
n.isBlank() ? "Unknown user" : "Hello, " + n + "!"
);
//Effect consumes the result of the chain
Signal.effect(this, () -> welcomeLabel.setText(greeting.get()));Auflistung 2.3 – Berechnetes Signal als Transformationskette: Jede Stufe ist typsicher und zwischengespeichert
Ein berechnetes Signal ist immer schreibgeschützt. ‘signal.map()’ und ‘signal.cached()’ unterscheiden sich in dieser Hinsicht grundlegend: ‘map()’ erzeugt ein berechnetes Signal, das sein Lambda bei jedem Aufruf auf ‘get()’ neu bewertet – kein Caching, keine Memoisierung. ‘signal.cached()’ hingegen ist das Signaläquivalent einer ‘@Cacheable’-Methode mit automatischer Cache-Ungültigkeit: Der Rückgabewert wird wiederverwendet, solange sich keine Quelle geändert hat, und nur dann neu berechnet, wenn eine Abhängigkeit ungültig gemacht wurde.

Abbildung 2.3 – Transformationskette: Zwei ValueSignals fließen in ein berechnetes Signal, das wiederum ein weiteres berechnetes Signal einspeisen.
Das Modell ist nun vollständig: Quellsignale halten den Rohdatenzustand, berechnete Signale transformieren ihn deklarativ, Effekte verbinden das Ergebnis mit der Benutzeroberfläche – und das Framework kümmert sich um die gesamte Abhängigkeitsverfolgung. Kapitel 3 behandelt die spezifischen Signaltypen, die Vaadin 25.1 für diese drei Rollen bereitstellt.
Die Signaltypen sind detailliert
Vaadin 25.1 bietet kein monolithisches Signal<T> für alle Fälle, sondern ein abgestuftes System: lokale Signale für sitzungsgebundene Zustände und gemeinsame Signale für den Sitzungszustand. Die Unterteilung in lokal.* und geteilt.* ist kein Umsetzungsdetail, sondern eine bewusste architektonische Grenze.

Abbildung 3.1 – Das Signaltypsystem: lokale (sitzungsgebundene) und gemeinsame (Quer-Sitzungs-) Typen
ValueSignal<T> – der lokale Baustein
ValueSignal<T> ist sitzungslokal und kann nicht in Signaltransaktionen verwendet werden. Er eignet sich für den UI-internen Zustand, der nicht zwischen Sitzungen geteilt werden muss. Die Klasse implementiert Serializable – jedoch nur für die Sitzungsreplikation: Wenn eine Sitzung im Rahmen eines Failovers auf einen anderen Server migriert wird, kann der Zustand des Signals erhalten bleiben. Dies ermöglicht nicht die Nutzung über mehrere Sitzungen hinaus, sondern genau das Gegenteil: ValueSignal bleibt an genau eine Sitzung gebunden, selbst wenn diese Sitzung physisch verschoben wird. Nutzer, die den Zustand zwischen mehreren Sitzungen teilen möchten, sollten SharedValueSignal<T> verwenden.
//Constructor without class literal (since 25.1)
ValueSignal<String> name = new ValueSignal<>("Max");
ValueSignal<Boolean> visible = new ValueSignal<>(true);
//Java Records Recommended – Immutability Prevents Direct Mutation
record Address(String street, String city) {
Address withCity(String city) { return new Address(street, city); }
}
ValueSignal<Address> address =
new ValueSignal<>(new Address("Hauptstr. 1", "Berlin"));
//set() – atomic, triggers effects when value changes
address.set(new Address("Bahnhofstr. 5", "Hamburg"));
//replace() – atomic compareAndSet
address.replace(
new Address("Bahnhofstr. 5", "Hamburg"),
new Address("Bahnhofstr. 5", "München")
);
update() – Read-Modify-Write under Lock, no Lost Update possible
address.update(current -> current.withCity("Frankfurt"));
//Custom Equality: notify only when the city changes
ValueSignal<Address> dedup = new ValueSignal<>(
new Address("Hauptstr. 1", "Berlin"),
(a, b) -> a.city().equals(b.city())
);
//asReadonly() – read-only view of the same signal
Signal<Address> readOnly = address.asReadonly();Auflistung 3.1 – ValueSignal<T>: set(), replace(), update(), Custom Equality, asReadonly()
Wichtig: ValueSignal darf in Signal.runInTransaction() nicht verwendet werden. Für den transaktionalen Einzelwertzustand verwenden Sie SharedValueSignal<T>.
SharedNumberSignal – atomar und Cross-Session
Der grundlegende Baustein für einen sitzungsübergreifenden Einzelwertzustand ist ‘SharedValueSignal<T>’. Funktional entspricht es dem lokalen ‘ValueSignal<T>’ und beinhaltet jedoch die vollständige gemeinsame Signalinfrastruktur: Thread-Sicherheit durch das Signalbaum-Lock, Transaktionsunterstützung sowie einen asynchronen Bestätigungsmechanismus. Daher geben alle mutierenden Methoden – ‘set()’, ‘replace()’, ‘update()’ – kein ‘void’ zurück, sondern ein ‘SignalOperation<T>’: einen auf ‘CompletableFuture’-basierten Handle, der sofort nach Bestätigung des Befehls aufgelöst wird.
‘SharedNumberSignal’ ist eine Spezialisierung von ‘SharedValueSignal<Double>’ und erweitert diesen grundlegenden Baustein um atomare Inkrementoperationen sowie um ganzzahlige Hilfsmethoden. Dies ist der entscheidende Vorteil gegenüber einem generischen SharedValueSignal<Double>: incrementBy(delta) ist eine einzelne atomare Operation im Signalbaum, während ein manuelles set(peek() + delta) eine nicht atomare Lese-Änderung-Schreib-Operation wäre, die zu Last-to-Lost-Updates führt.
SharedNumberSignal counter = new SharedNumberSignal(0);
//Atomic – no lost update for N simultaneous sessions
SignalOperation<Double> op = counter.incrementBy(1);
op.result().thenAccept(r -> {
if (r.successful()) System.out.println("New value: " + r.value());
});
counter.incrementBy(-1); //Decrement
counter.set(0.0); //set(double)
counter.set(0); //set(int) – Convenience Overload
//Integer API Outside Reactive Context
int current = counter.peek().intValue();
//Within a reactive context
Signal.effect(this, () -> label.setText(String.valueOf(counter.getAsInt())));
map() to int values – no manual casting required
Signal<String> display = counter.mapIntValue(n -> "clicks: " + n);
//Access control via validator
SharedNumberSignal maxTen = counter.withValidator(
cmd -> counter.peek() < 10
);SharedNumberSignal readOnly = counter.asReadonly();
Listing 3.2 – SharedNumberSignal: incrementBy(), getAsInt(), mapIntValue(), SignalOperation, withValidator()

Abbildung 3.2 – Verlorenes Update durch Read-Modify-Write (links) vs. atomare IncrementBy() (rechts)
ListSignal und SharedListSignal – lokale vs. geteilte Listen
Für Listen gibt es zwei Varianten mit unterschiedlichen Garantien. ListSignal<T> ist session-lokal und benötigt keinen Klassentoken. SharedListSignal<T> ist cross-session, benötigt den Klassentoken und gibt auf Inserts eine InsertOperation zurück – einen Handle für das neue Eingabesignal, der sofort verwendet werden kann, noch bevor die Operation bestätigt wurde.
//Local list (session-local, no class token)
ListSignal<String> tags = new ListSignal<>();
ValueSignal<String> entry = tags.insertLast("java");
tags.insertFirst("vaadin");
tags.insertAt(1, "signals");
//Granular tracking: only affected entry triggers effect
entry.set("java-25");
tags.remove(entry);
tags.moveTo(entry, 0);
tags.clear();
//Stream over all values
tags.getValues().forEach(System.out::p rintln); //reactive
tags.peekValues().forEach(System.out::p rintln); //without tracking
//Split list (cross-session, class token required)
SharedListSignal<String> shared = new SharedListSignal<>(String.class);
InsertOperation<SharedValueSignal<String>> op = shared.insertLast("item");
SharedValueSignal<String> newSignal = op.signal(); //immediately usable
op.result().thenAccept(r -> { /* confirmed */ });
shared.remove(newSignal);
shared.moveTo(newSignal, SharedListSignal.ListPosition.first());Listing 3.3 – ListSignal (lokal) vs. SharedListSignal (geteilt): Konstruktor, Einfügungen, InsertOperation
Für Karten ist nur ‘SharedMapSignal<T>’ verfügbar – es gibt keine lokale Kartenvariante. Das Klassentoken im Konstruktor ist verpflichtend, wie bei ‘SharedListSignal’, da der Wert per JSON serialisiert wird.
Wenn Sie jedoch nur eine lokale Karte innerhalb einer Sitzung benötigen und keinen Cross-Session-Zustand benötigen, müssen Sie ‘SharedMapSignal’ nicht verwenden: ‘ValueSignal<Map<String, V>>’ in Kombination mit ‘ValueSignal.modify()’ ist eine ausreichende Alternative. ‘modify()’ übergibt die aktuelle Abbildung direkt an ein Lambda, die sie an Ort und Stelle mutieren kann und dann automatisch alle abhängigen Effekte benachrichtigt:
ValueSignal<Map<String, String>> config = new ValueSignal<>(new HashMap<>());
config.modify(map -> map.put("theme", "dark"));
Signal.effect(this, () -> applyTheme(config.get().get("theme")) );Der Unterschied zu SharedMapSignal: kein Klassentoken, kein JSON-Overhead, kein asynchroner Bestätigungsmechanismus – aber auch keine Unterstützung für Transaktionen und keine Cross-Session-Sichtbarkeit.
SharedMapSignal<String> config = new SharedMapSignal<>(String.class);
put() returns SignalOperation with the previous value
config.put("theme", "dark");
config.put("locale", "en-US");
putIfAbsent() returns PutIfAbsentResult
config.putIfAbsent("locale", "en-US")
.result()
.thenAccept(r -> System.out.println("Created: " + r.value().created()));
Single entry as SharedValueSignal
Signal.effect(this, () -> {
Map<String, SharedValueSignal<String>> map = config.get();
SharedValueSignal<String> themeSignal = map.get("theme");
if (themeSignal != null) applyTheme(themeSignal.get());
});
config.remove("theme");
config.clear();Auflistung 3.4 – SharedMapSignal: put(), putIfAbsent(), PutIfAbsentResult, granulare Verfolgung
Signal-Tools: computed(), cached(), not(), EffectContext
Die Signalschnittstelle ist mehr als nur ein Marker für reaktive Werte – sie ist auch die zentrale Fabrik für berechnete Signale, Caching und Effekte. Die folgenden statischen Helfermethoden sind besonders für erfahrene Java-Entwickler relevant, da sie Muster adressieren, die typischerweise manuell gelöst werden, wenn man die API erstmals begegnet: Boolesche Negation, kontrolliertes Lesen ohne Abhängigkeitsregistrierung und den Unterschied zwischen einem zwischengespeicherten und einem nicht gecachteten berechneten Signal.
Signal.computed() – Computed Signal with Type Inference
var fullName = Signal.computed(() -> firstName.get() + " " + lastName.get());
Signal.cached() – cached computed signal
Signal<String> expensive = Signal.cached(
Signal.computed(() -> expensiveComputation(firstName.get()))
);
Signal.not() – Boolean negation
ValueSignal<Boolean> loading = new ValueSignal<>(false);
Signal<Boolean> ready = Signal.not(loading);
Signal.effect(this, () -> submitButton.bindEnabled(ready));
Signal.untracked() -- Block without dependency registration
Signal.effect(this, () -> {
String current = Signal.untracked(() -> name.get());
String watched = counter.getAsInt() + ": " + current;
label.setText(watched);
});
//EffectContext – why is the Effect running right now?
Signal.effect(this, ctx -> {
label.setText(price.getAsInt() + " €");
if (ctx.isBackgroundChange()) {
label.addClassName("highlight");
} else if (ctx.isInitialRun()) {
label.removeClassName("highlight");
}
});Auflistung 3.5 – Signal.computed(), cached(), not(), untracked(), EffectContext
Tipp: Signal.cached() und Signal.computed() ersetzen einander nicht. computed() berechnet den Wert bei jedem get() neu. cached() speichert das Ergebnis und berechnet nur neu, wenn sich mindestens eine Abhängigkeit geändert hat – semantisch ein @Cacheable mit automatischer Invalidierung.
Mit diesem System ist die Werkzeugkiste komplett: ValueSignal und ListSignal für den Session-Local-State, die shared*-Varianten für Cross-Session-Zustand mit Transaktionsunterstützung und die Signal-Utilitys für abgeleitete Werte, Caching und kontextbewusste Effekte. Kapitel 4 untersucht, wie die gemeinsamen Varianten unter gleichzeitiger Last korrekt funktionieren – insbesondere bei Threadsicherheit, wiederholbaren Lesearten und Transaktionen.
Im Teil II werden wir dann die weiteren Eigenschaften der neuen Signals beleuchten..
Happy Coding




