Skip to main content
  1. Posts/

How and why to use the classic Observer pattern in Vaadin Flow

Sven Ruppert
Author
Sven Ruppert
20+ years of Java, specialised in Security, Vaadin and Developer Relations. When not coding, you’ll find me in the woods with an axe.
Table of Contents

1. Introduction and motivation
#

The observer pattern is one of the basic design patterns of software development and is traditionally used to decouple state changes and process them. Its origins lie in the development of graphical user interfaces, where a shift in the data model required synchronising several views immediately, without a direct link between these views. This pattern quickly established itself as the standard solution to promote loosely coupled architectures.

  1. 1. Introduction and motivation
  2. 2. The classic observer pattern
  3. 3. Observer Pattern in Vaadin Flow
  4. 4. Differences between classic Observer and Vaadin Flow
  5. 5. When does the classic observer pattern still make sense?
  6. 6. Example: Combination of Observer Pattern and Vaadin Flow (with external producer)
  7. 7. Best Practices and Pitfalls
  8. 8. Conclusion

At its core, the Observer Pattern addresses the challenge that events or state changes do not remain isolated; instead, they must be processed by different components. This creates an asynchronous notification model: The subject informs all registered observers as soon as its state changes. The observers react to this individually, without the subject having to know their concrete implementations.

The source code with the examples can be found in the following git-repo:https://github.com/Java-Publications/Blog—Vaadin—How-to-use-the-Observer-Pattern-and-why** __**

In modern software architectures, characterised by interactivity, parallelism, and distributed systems, this basic principle remains highly relevant. It continues to be used in the field of UI development, as user interactions and data flows must be handled dynamically and in a reactive manner. Frameworks such as Vaadin Flow take up this idea, but go beyond the classic implementation by providing mechanisms for state matching, server-side synchronisation and client-side updates.

The motivation to apply the Observer Pattern in the context of Vaadin Flow lies in comparing proven yet simple patterns and their further development within a modern UI framework. This not only sharpens the understanding of loose coupling and event handling, but also creates a practical reference to today’s web applications.

2. The classic observer pattern
#

The classical observer pattern describes the relationship between a subject and a set of observers. The subject maintains the current state and automatically informs all registered observers of any relevant change. This achieves a loose coupling: the subject only knows the interface of its observers, but not their concrete implementation or functionality.

In its original form, according to the “Gang of Four” definition, the pattern consists of two central roles. The subject manages the list of its observers and provides methods of logging in and out. As soon as the internal state changes, it calls a notification method for all observers. The observers implement an interface through which they are informed and can define their own reaction. This allows state changes to be propagated without creating complex coupling or cyclic dependencies.

To avoid outdated dependencies and to maintain control over the API, an independent, generic variant with its own interfaces is shown below. The aim is to demonstrate how it works in a JUnit 5 test without the main method. The output is a logger via HasLogger from the package com.svenruppert.dependencies.core.logger.

package com.svenruppert.demo;

import com.svenruppert.dependencies.core.logger.HasLogger;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ObserverDemoTest {

  @Test
  void beobachter_erhaelt_updates_und_letzten_wert() {
    var sensor = new TemperatureSensor();
    var anzeigeA = new TemperaturAnzeige();
    var displayB = new Temperature display();
    sensor.addObserver(displayA);
    sensor.addObserver(displayB);
    sensor.setTemperature(22);
    assertEquals(22, displayA.lastObservedTemp);
    assertEquals(22, sensor.getTemperature());
    sensor.setTemperature(25);
    assertEquals(25, sensor.getTemperature());
    assertEquals(25, displayA.lastObservedTemp);
    assertEquals(25, displayB.lastObservedTemp);
    sensor.removeObserver(displayA);
    sensor.setTemperature(21);
    assertEquals(21, sensor.getTemperature());
    assertEquals(25, displayA.lastObservedTemp);
    assertEquals(21, displayB.lastObservedTemp);
  }

  /**
   * Minimalist, own Observer API (generic, thread-safe implemented in the subject).
   */
  interface Observer<T> {
    void onUpdate(T value);
  }

  interface Subject<T> {
    void addObserver(Observer<T> o);
    void removeObserver(Observer<T> o);
    void notifyObservers(T value);
  }

  /**
   * Concrete subject: maintains a temperature and informs observers of changes.
   */
  static class Temperature Sensor
      implements Subject<Integer> {

    private final List<Observer<Integer>> observers = new CopyOnWriteArrayList<>();

    Private int temperature;

    @Override
    public void addObserver(Observer<Integer> o) { observers.add(o); }

    @Override
    public void removeObserver(Observer<Integer> o) { observers.remove(o); }

    @Override
    public void notifyObservers(Integer value) { observers.forEach(o -> o.onUpdate(value)); }

    public int getTemperatur() { return temperatur; }

    public void setTemperature(int newtemperature) {
      this.temperature = newtemperature;
      notifyObservers(temperature);
    }
  }

  /**
   * Concrete Observer: remembers the last value and logs it.
   */
  static class temperature display
      implements Observer<Integer>, HasLogger {
    Integer lastObservedTemp;
    @Override
    public void onUpdate(Integer value) {
      lastObservedTemp = value;
      logger().info("New temperature: {}°C", value);
    }
  }
}

In the example, the company’s own API defines two lean interfaces: Subject manages observers and propagates changes, while Observer reacts to updates. The implementation of the Subject uses a CopyOnWriteArrayList to handle concurrent logins and logoffs during notification robustly. The JUnit5Test demonstrates the expected behaviour: By setting the temperature twice, notifications are triggered twice. The temperature display logs the values via HasLogger and retains the last observed value for assertion. This demonstrates the basic semantics of the Observer Pattern – loose coupling, clear responsibilities and event-driven updating – in a modern, framework-free Java variant.

3. Observer Pattern in Vaadin Flow
#

While the classic Observer Pattern has its origins in desktop programming, Vaadin Flow elevates the concept to a higher level of abstraction. Since Vaadin operates on the server side and manages the UI state centrally, synchronisation between the data model, user interface, and user interactions is an integral part of the framework. Events and listeners take on the role of the observer mechanism.

A central element is the ValueChangeListener , which is automatically triggered when changes are made to input components such as text fields or checkboxes. Here, the UI component acts as a subject, while listeners represent the observer. As soon as the user makes an input, an event is generated, and all registered listeners are informed. All the developer has to do is implement the desired response without worrying about chaining event flows.

Additionally, Vaadin provides a powerful tool with the Binder , which enables bidirectional data binding between the data model and UI components. Changes to a field in the form are automatically propagated to the bound object, while changes in the object are immediately visible in the UI elements. The binder thus adopts a bidirectional observation that goes beyond the possibilities of the classic observer pattern.

Another example of the observer pattern’s implementation in Vaadin Flow is the EventBus mechanism , which enables components or services to exchange information about events in a loosely coupled manner. Here, it becomes clear that Vaadin adopts the basic idea of the pattern, but expands it in the context of a web application to include aspects such as thread security, lifecycle management, and client-server synchronisation.

The strength of Vaadin Flow lies in the fact that developers no longer have to implement observer interfaces explicitly; instead, they can establish observation relationships via declarative APIs, such as addValueChangeListener, addClickListener, or via binder bindings. This does not abolish the pattern, but integrates it deeply into the architecture and adapts it to the special requirements of a server-side web UI.

Vaadin Flow implements the basic idea of the Observer Pattern along two levels. At the UI level, component-specific events (e.g., ValueChangeEvent, ClickEvent) exist that are registered via listeners and trigger changes to the representation or state model. This mechanism corresponds to a finely granulated, component-internal observer and is conveyed via an internal EventBus for each component. At the application level, a domain-level observation structure is also recommended to model state changes outside the UI and to mirror them in the UI in a decoupled manner. This allows long-lived domain objects and short-lived UII punches to be cleanly separated.

A minimal example exclusively with Vaadin on-board tools shows the observation of UIC conditions as well as the binding to a model via binder:

import com.svenruppert.flow.MainLayout;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.binder;
import com.vaadin.flow.router.Route;

@Route(value = "observer-bordmittel", layout = MainLayout.class)
public class On-board ResourcesView
    extends VerticalLayout {

  public BordmittelView() {
    var name = new TextField("Name");
    var log = new Div();
    var save = new Button("Save");
    //(1) Component-internal observer: reacts to ValueChangeEvent
    name.addValueChangeListener(
        e -> log.setText("UI → UI: Name changed to '"
                         + e.getValue() + "'"));
    //(2) Binder as a bidirectional observation between UI and model
    var binder = new Binder<>(Person.class);
    var model = new Person();
    binder.forField(name).bind(Person::getName, Person::setName);
    binder.setBean(model);
   // (3) Action: reads current model state (powered by the binder)
    store.addClickListener(
        e -> log.setText("Model → Action: saved name = "
                         + model.getName()));
    add(name, store, log);
  }

  static class Person {
    private String name;

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }
  }
}

Here, addValueChangeListener(…), the role of the observer at the component level, while the binder synchronises the changes between the input field and the domain object. Without additional infrastructure, this creates a transparent, declarative observation chain from the UI to the model and back to the UI. It is important to note that the button does not trigger a new save, but reads the model state that has already been synchronised by the binder and makes it visible. It thus serves as an action-level observer that confirms and logs the current consistency between UI and model.

4. Differences between classic Observer and Vaadin Flow
#

The comparison between the classic Observer Pattern and its implementation in Vaadin Flow reveals that both approaches share the same basic idea, but operate at different levels of abstraction. While the classic pattern relies on purely object-oriented structures – subject and observer communicate directly via defined interfaces – Vaadin Flow shifts observation to a framework-supported environment with integrated event control.

A significant difference lies in life cycle management. In the classic observer, subjects and observers remain the responsibility of the developer; Registration and deregistration must be done manually, otherwise there is a risk of memory leaks. Vaadin Flow integrates observation into the lifecycle of UI components. Listeners are registered when a component is created and automatically resolved when the component is removed, reducing administrative overhead.

ThreadSecurity also differs significantly. The classic observer pattern works in a local context and has no UI thread binding. Vaadin Flow, on the other hand, must guarantee that UI updates are done exclusively in the UI thread. This forces the use of mechanisms such as UI.access(…)as soon as events from foreign threads are processed.

Another difference concerns the direction and range of the synchronisation. In the classic model, notifications are limited to the objects involved. Vaadin Flow extends this principle to client-server communication: changes to the UI are automatically synchronised to the server, while server-side state changes are, in turn, made visible in the UI on the client side.

Finally, the granularity of the observation varies. Classic observers usually react to gross changes in state. Vaadin Flow, on the other hand, offers a variety of specialised event types (e.g. ValueChange, Click, Attach/Detach) that allow for very finely tuned reactions. In combination with the binder , a bidirectional coupling between model and UI is created that goes far beyond the original idea of the pattern.

In summary, the classic observer pattern provides the theoretical basis. Still, Vaadin Flow abstracts and expands it by integrating lifecycle, thread security, and client-server synchronisation, thereby enabling a higher degree of robustness and productivity.

5. When does the classic observer pattern still make sense?
#

Although Vaadin Flow already integrates many observation mechanisms, there are scenarios in which the classic observer pattern can also be used sensibly in modern Vaadin applications. The crucial point here is the separation between UI logic and domain logic. While listeners and binders in Vaadin handle the synchronisation between user input and UI components, events often arise in the business layer that must be managed independently of the UI.

A typical field of application is internal domain events. For example, a background process can receive a new message, complete a calculation, or import data from an external system. These events should not be directly bound to UI components; instead, they should be distributed throughout the system. Here, the classic observer pattern is suitable for informing interested services or components of such changes without creating a direct link.

The pattern also plays a role in the context of integrations with external systems. When a REST service or messaging infrastructure feeds events into the system, the observer pattern can be used to propagate those events within the domain first. Only in the second step is it decided whether and how this information is passed on to a Vaadin UI. This keeps the dependency on the UI low, and the domain layer remains usable outside of a UI context.

Another reason for using the classic observer pattern is testability and reusability. Unit testing at the domain layer is easier to perform when observation mechanisms do not depend on Vaadin-specific APIs. This allows complex interactions to be tested in isolation before they are later wired to the UI.

In summary, the classic observer pattern complements the mechanisms available in Vaadin Flow for UI-independent, domain-internal events or integrations with external sources. It enables loose coupling, increases testability and ensures clear demarcations between layers – a principle that is a decisive advantage, especially in more complex applications.

6. Example: Combination of Observer Pattern and Vaadin Flow (with external producer)
#

An efficient variant arises when messages are not generated by the UI itself, but come from an external producer outside the UI life cycle. Typical sources are background processes, REST integrations or message queues. The Observer Pattern ensures that these events can be distributed to all interested components, regardless of the UI.

The following example extends the previous construction: A MessageProducer periodically generates messages in a separate thread and passes them to the MessageService. The Vaadin view remains a pure observer, reflecting only the messages in the UI.

//Bootstrap: starts the producer on startup
final class ApplicationBootstrap {

  private static final MessageService SERVICE = new MessageService();

  static {
    MessageProducer.start(SERVICE);
  }

  private ApplicationBootstrap() {
  }

  static MessageService messageService() {
    return SERVICE;
  }
}

//External producer that periodically generates news

public class MessageProducer {
  private static ScheduledExecutorService scheduler;
  private MessageProducer() { }
  static void start(MessageService service) {
    scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.scheduleAtFixedRate(
        new CronJob(service),
        0, 2, SECONDS);
  }

  static void stop() {
    if (scheduler != null) {
      scheduler.shutdownNow();
    }
  }

  public record CronJob(MessageService service)
      implements Runnable, HasLogger {

    @Override
    public void run() {
      logger().info("Next message will be sent..");
      service.newMessage("Tick @ " + Instant.now());
    }
  }
}

//Domain Service
public class MessageService
    implements Subject<String> , HasLogger {
  private final List<Observer<String>> observers = new CopyOnWriteArrayList<>();

  @Override
  public void addObserver(Observer<String> o) {
    logger().info("New Observer added: {}", o);
    observers.add(o);
  }

  @Override
  public void removeObserver(Observer<String> o) {
    logger().info("Observer removed: {}", o);
    observers.remove(o);
  }

  @Override
  public void notifyObservers(String value) {
    logger().info("Notifying {} observers with the value {} ", observers.size(), value);
    observers.forEach(o -> o.onUpdate(value));
  }

  public void newMessage(String msg) {
    notifyObservers(msg);
  }
}

public interface Observer<T> {
  void onUpdate(T value);
}

public interface Subject<T> {
  void addObserver(Observer<T> o);
  void removeObserver(Observer<T> o);
  void notifyObservers(T value);
}

This structure creates a complete message flow producer → domain → UI. The domain layer is independent of Vaadin and can be tested in isolation. The UI remains limited to the display and updates itself exclusively in the UI thread.

The new messages appear every two seconds, as the MessageProducer is configured in the background with scheduleAtFixedRate. When it starts, it immediately generates a message and then periodically generates a new one every two seconds. These are distributed to the registered observers via the MessageService. Important: For server-side changes to be reflected in the browser without user interaction, ServerPush or Polling must be enabled. In this example, @Push on the View ensures that ui.access(…) actively sends the changes to the client. Alternatively, polling (e.g. @Poll(2000) or UI#setPollInterval). The view receives the events and sets the text in the div using UI.access(…) in UI Thread, so that the most recent message is visible.

This uses the Observer Pattern to integrate external events into a Vaadin Flow application reliably.

7. Best Practices and Pitfalls
#

The integration of the classic Observer Pattern into a Vaadin Flow application brings both advantages and specific challenges. To ensure a robust and maintainable architecture, several best practices should be considered.

Observer lifecycle management. A common mistake is to leave Observer permanently registered in the service, even if the associated UI component has already been destroyed. This leads to memory leaks and unexpected calls to views that no longer exist. Therefore, observers in Vaadin views should always be registered inonAttach and removed in onDetach. In this way, the observation remains clearly linked to the life cycle of the UI.

Use of suitable data structures
Instead of simple lists, it is recommended to use concurrent, duplicate-free data structures, such as ConcurrentHashMap or CopyOnWriteArraySet, for managing observers. This avoids duplicate registrations and ensures thread safety, even if multiple threads add or remove observers at the same time.

Thread security and UI updates. Since external events are often generated in separate threads, it is imperative to install UI updates in Vaadin viaUI.access(…) be performed. Otherwise, there is a risk of race conditions and exceptions, because the UI components may only be changed from within the UI thread. Additionally, server push or polling should be enabled to ensure that changes are sent to the client without user interaction.

Domain and UI demarcation. The domain layer should remain completely free of Vaadin-specific dependencies. This ensures that business logic can be tested and reused outside of the UI context. The UI only registers itself as an observer and takes over the representation of the events delivered by the domain.

Error handling and logging
Events should be handled robustly to prevent individual errors from interrupting the entire notification flow. Clean logging (e.g. via a common HasLogger interface) facilitates the analysis and traceability of the event flow.

Summary. The main pitfalls lie in improper lifecycle management, a lack of thread security, and overly tight coupling between the UI and domain. When these aspects are taken into account, the combination of the Observer Pattern and Vaadin Flow enables a clear separation of responsibilities, testable business logic, and a reactive, user-friendly interface.

8. Conclusion
#

A look at the Observer Pattern in combination with Vaadin Flow reveals that it remains a relevant and compelling design pattern, whose benefits extend beyond classic desktop or console applications. In its original form, it provides apparent decoupling of state changes and their processing, thus creating a clean basis for loosely coupled architectures.

Vaadin Flow abstracts and extends this approach by integrating observation mechanisms directly into the lifecycle of UI components, providing a variety of specialised events, and automating synchronisation between server and client. This relieves the developer of many tasks that could still be solved manually in the classic observer pattern, such as memory management or thread security.

Nevertheless, the classic observer pattern remains essential in areas where UI-independent events occur or external systems are connected. The combination of both approaches – a clearly defined domain layer with observers and a Vaadin UI based on it – creates an architecture that is both loosely coupled, testable, and extensible.

Overall, it can be said that the Observer Pattern forms the theoretical basis, while Vaadin Flow provides the practical implementation for modern web applications. Those who consciously combine both approaches benefit from robust and flexible systems that are prepared for future requirements.

Related

What makes Vaadin components special?

·6 mins
From my experience, Vaadin has always stood out from other Java frameworks. Of course, it enables the creation of modern web UIs, but the real difference lies in its component architecture. This is not conceived as a short-term aid, but is consistently designed for maintainability and flexibility. It creates the possibility of running applications stably for many years while still being able to extend them step by step.

Part III - WebUI with Vaadin Flow for the URL Shortener

·19 mins
1. Introduction and objectives # The first two parts of this series established the theoretical and practical foundations of a URL shortener in pure Java. We discussed the semantic classification of short URLs, the architecture of a robust mapping system, and the implementation of a REST-based service based on the JDK HTTP server. These efforts resulted in a functional, modularly extensible backend that creates, manages, and efficiently resolves short links. However, a crucial component was missing-a visual interface for direct user interaction with the system. This interface is essential for tasks such as manual link creation, viewing existing mappings, and analysing individual redirects.

Connecting REST Services with Vaadin Flow in Core Java

1. Introduction # Why REST integration in Vaadin applications should not be an afterthought # In modern web applications, communication with external services is no longer a special function, but an integral part of a service-oriented architecture. Even if Vaadin Flow, as a UI framework, relies on server-side Java logic to achieve a high degree of coherence between view and data models, the need to communicate with systems outside the application quickly arises. These can be simple public APIs—for example, for displaying weather data or currency conversions—as well as internal company services, such as license verification, user management, or connecting to a central ERP system.

Creating a simple file upload/download application with Vaadin Flow

Vaadin Flow is a robust framework for building modern web applications in Java, where all UI logic is implemented on the server side. In this blog post, we’ll make a simple file management application step by step that allows users to upload files, save them to the server, and download them again when needed. This is a great way to demonstrate how to build protection against CWE-22, CWE-377, and CWE-778 step by step.