-
Notifications
You must be signed in to change notification settings - Fork 0
How to Implement an Application Event Bus for a Typical Vaadin Business App
This guide walks through a practical, production‑oriented way to build an application‑level event bus for a typical Vaadin business application:
- Hundreds to a few thousand users
- 1–4 application replicas
- Events are semantic signals, not data carriers ("Book X was saved"; the receiver loads data from the backend)
- The same event model works both within a single JVM and across a small cluster via Redis
The examples use Vaadin 8 style APIs (e.g. UI.access) and presenters like
vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java and
vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/stats/StatsPresenter.java,
but the EventBus and Redis pieces are generic and can be reused in any Java business application.
Before we look at code, clarify what we want from an application‑level event bus:
-
Semantic, atomic events
- Events are facts about things that happened, e.g.
Book 42 saved. - Receivers fetch the latest data from the backend instead of trusting data inside the event.
- Events are facts about things that happened, e.g.
-
Process‑local and cluster‑wide delivery
- Within a JVM, events should be simple method calls to listeners.
- Across nodes, the same events are broadcast via Redis pub/sub.
-
UI‑friendly for Vaadin
- Presenters can listen to events and update views.
- All UI changes happen via
UI.access(...), with Vaadin Push enabled.
-
Simple to reason about
- Single
EventBusAPI. - Typed events (Java records) instead of stringly‑typed messages.
- Single
Start from the domain: define a sealed interface for all application events and a few concrete records.
Backend module (e.g. vaadincreate-backend):
// AbstractEvent.java
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "eventType")
@JsonSubTypes({
@JsonSubTypes.Type(value = BooksChangedEvent.class, name = "BooksChangedEvent"),
@JsonSubTypes.Type(value = CategoriesUpdatedEvent.class, name = "CategoriesUpdatedEvent"),
// ... other events
})
public sealed interface AbstractEvent
permits BooksChangedEvent, CategoriesUpdatedEvent /* ... */ {
}Example events:
// BooksChangedEvent.java
public record BooksChangedEvent(Integer productId, BookChange change)
implements AbstractEvent {
public enum BookChange { SAVE, DELETE }
}
// CategoriesUpdatedEvent.java
public record CategoriesUpdatedEvent(Integer categoryId, CategoryChange change)
implements AbstractEvent {
public enum CategoryChange { SAVE, DELETE }
}Key ideas
- Events are small and semantic: an ID plus an enum for the type of change.
- Jackson annotations allow polymorphic JSON serialization (needed for Redis).
Next, define a small service that knows how to publish and subscribe to events using Redis.
// RedisPubSubService.java
public interface RedisPubSubService {
void publishEvent(String nodeId, AbstractEvent event);
void startSubscriber(Consumer<EventEnvelope> handler);
void stopSubscriber();
void closePublisher();
// Factory method for singleton instance
static RedisPubSubService get() {
return RedisPubSubServiceImpl.getInstance();
}
@JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "@class")
record EventEnvelope(String nodeId, AbstractEvent event) { }
}Minimal implementation outline (details like error handling omitted for brevity):
// RedisPubSubServiceImpl.java
public class RedisPubSubServiceImpl implements RedisPubSubService {
private final Jedis publisherJedis;
private final Jedis subscriberJedis;
private final ExecutorService executor;
private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
private final String channel = "eventbus_channel";
private boolean localMode = false;
public static synchronized RedisPubSubService getInstance() { /* ... */ }
@Override
public void publishEvent(String nodeId, AbstractEvent event) {
if (localMode) return; // fall back to local‑only mode if Redis is down
var envelope = new EventEnvelope(nodeId, event);
var json = mapper.writeValueAsString(envelope);
publisherJedis.publish(channel, json);
}
@Override
public void startSubscriber(Consumer<EventEnvelope> handler) {
executor.submit(() -> subscriberJedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String ch, String message) {
EventEnvelope env = mapper.readValue(message, EventEnvelope.class);
handler.accept(env);
}
}, channel));
}
// stopSubscriber / closePublisher close connections & shut down executor
}Responsibilities
- Serialize
AbstractEventinto JSON and send it to a single Redis pub/sub channel. - Receive JSON messages, deserialize into
EventEnvelope, and pass them to a handler. - Act as a cluster‑wide transport, unaware of Vaadin or UI.
The application talks to a small EventBus interface.
UI module (e.g. vaadincreate-ui):
// EventBus.java
@NullMarked
public interface EventBus {
interface EventBusListener {
void eventFired(AbstractEvent event);
}
void post(AbstractEvent event);
void registerEventBusListener(EventBusListener listener);
void unregisterEventBusListener(EventBusListener listener);
void shutdown();
static EventBus get() {
return EventBusImpl.getInstance();
}
}Implementation that bridges in‑JVM listeners and Redis:
// EventBusImpl.java
public class EventBusImpl implements EventBus {
private static EventBusImpl instance;
private final String nodeId = UUID.randomUUID().toString();
private final RedisPubSubService redisService;
// WeakHashMap avoids leaking Vaadin components / presenters
private final WeakHashMap<EventBusListener, Object> listeners = new WeakHashMap<>();
// Small pool of virtual threads for async dispatch
private final ExecutorService executor =
Executors.newFixedThreadPool(5, Thread.ofVirtual().name("eventbus").factory());
public static synchronized EventBus getInstance() {
if (instance == null) {
instance = new EventBusImpl(RedisPubSubService.get());
}
return instance;
}
protected EventBusImpl(RedisPubSubService redisService) {
this.redisService = redisService;
redisService.startSubscriber(env -> {
// Ignore events from this node
if (!nodeId.equals(env.nodeId())) {
postLocal(env.event());
}
});
}
@Override
public void post(AbstractEvent event) {
redisService.publishEvent(nodeId, event); // cluster broadcast
postLocal(event); // local dispatch
}
private void postLocal(AbstractEvent event) {
synchronized (listeners) {
listeners.forEach((listener, o) ->
executor.execute(() -> listener.eventFired(event)));
}
}
@Override
public void registerEventBusListener(EventBusListener listener) {
synchronized (listeners) {
listeners.put(listener, null);
}
}
@Override
public void unregisterEventBusListener(EventBusListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
@Override
public void shutdown() {
redisService.stopSubscriber();
redisService.closePublisher();
executor.shutdown();
}
}What this gives you
-
EventBus.post(event)immediately notifies all listeners in the same JVM. - The same event is published to Redis and delivered to other nodes.
- Other nodes unwrap the payload and call
postLocal(event)into their own listeners.
With the infrastructure in place, presenters treat the EventBus as a simple, strongly‑typed pub/sub.
BooksPresenter both publishes and listens to events.
- Publish a semantic event after saving a book
public class BooksPresenter implements EventBusListener, Serializable {
public BooksPresenter(BooksView view) {
this.view = view;
getEventBus().registerEventBusListener(this);
}
@Nullable
public Product saveProduct(Product product) {
// ... validate, call ProductDataService.updateProduct(product), update view ...
Integer id = product.getId();
getEventBus().post(new BooksChangedEvent(id, BookChange.SAVE));
return product;
}
public void deleteProduct(Product product) {
Integer id = product.getId();
// ... delete from backend, update view ...
getEventBus().post(new BooksChangedEvent(id, BookChange.DELETE));
}
private EventBus getEventBus() {
return EventBus.get();
}
}- React to events from anywhere in the cluster
@Override
public void eventFired(AbstractEvent event) {
switch (event) {
case LockingEvent locking ->
view.refreshProductAsync(locking.id());
case BooksChangedEvent(Integer productId, BookChange change) -> {
if (change == BookChange.SAVE) {
view.refreshProductAsync(productId);
}
}
default -> { /* ignore */ }
}
}refreshProductAsync is implemented on the view to load data from the backend and update the UI using UI.access, which we will cover shortly.
StatsPresenter listens to any relevant event and triggers a full stats recomputation.
public class StatsPresenter implements EventBusListener, Serializable {
public StatsPresenter(StatsView view) {
this.view = view;
getEventBus().registerEventBusListener(this);
}
public void requestUpdateStats() {
future = CompletableFuture
.supplyAsync(this::loadProductsAndCategories, getExecutor())
.thenAccept(this::calculateStatistics)
.whenComplete((r, t) -> future = null);
}
@Override
public void eventFired(AbstractEvent event) {
if (event instanceof BooksChangedEvent
|| event instanceof CategoriesUpdatedEvent) {
view.setLoadingAsync(); // show spinner
requestUpdateStats(); // recompute stats
}
}
private EventBus getEventBus() {
return EventBus.get();
}
}Again, the view methods (setLoadingAsync, updateStatsAsync) are responsible for doing the actual UI updates safely.
Because EventBusImpl dispatches events on executor threads, presenter callbacks do not run in the Vaadin UI thread. All changes to Vaadin components must be marshalled back using UI.access.
You also need Vaadin Push enabled so that the server can push updates to the browser.
On your main UI class:
// VaadinCreateUI.java
@Push
public class VaadinCreateUI extends UI {
// ...
}For example, the stats view can expose methods like setLoadingAsync and updateStatsAsync that presenters call from any thread.
// StatsView.java (simplified)
public class StatsView extends VerticalLayout {
public void setLoadingAsync() {
UI ui = getUI();
if (ui == null) return;
ui.access(() -> setLoading(true));
}
public void updateStatsAsync(ProductStatistics stats) {
UI ui = getUI();
if (ui == null) return;
ui.access(() -> {
updateCharts(stats);
setLoading(false);
});
}
private void setLoading(boolean loading) { /* show/hide spinner */ }
private void updateCharts(ProductStatistics stats) { /* update components */ }
}Books view can follow the same pattern:
// BooksView.java (simplified)
public class BooksView extends VerticalLayout {
public void refreshProductAsync(Integer productId) {
UI ui = getUI();
if (ui == null) return;
ui.access(() -> {
Product product = productService.getProductById(productId);
updateGridItem(product);
});
}
}Rules of thumb
- Presenters never touch Vaadin components directly from event callbacks.
- Views provide
...Asynchelpers that do theUI.accessand any backend calls. -
@Pushis required so that UI updates initiated from background threads reach the browser.
With 1–4 replicas behind a load balancer:
- Each node has its own
EventBusImplandRedisPubSubServiceImplinstance. - When any node calls
EventBus.post(event):- It dispatches to local listeners immediately.
- It publishes an
EventEnvelope(nodeId, event)to the Redis channel.
- Every other node receives the envelope via Redis, ignores events from itself (via
nodeId), and redispatches theAbstractEventto its own local listeners.
This gives you:
- Global, best‑effort broadcast of semantic events to all nodes.
- Simple scaling for a typical business app; all nodes receive all events, which is acceptable at this size and event volume.
-
Graceful degradation: if Redis is temporarily down, the implementation can switch to a
localModewhere events only reach listeners in the same JVM.
This EventBus pattern is a good fit when:
- You build a Vaadin business app with modest write traffic.
- Events are primarily used for UI updates, notifications, and lightweight coordination.
- You run a small cluster (1–4 nodes) and value simplicity over heavy infrastructure.
You probably want something more sophisticated (e.g. Kafka or Redis Streams, multiple topics, consumer groups) if:
- Events become central to business logic and must be durable and replayable.
- You need high throughput event processing with many different consumers.
- You require strict ordering or exactly‑once processing guarantees.
For many Vaadin‑based line‑of‑business applications, this simple, typed EventBus + Redis approach hits a sweet spot: easy to implement, easy to reason about, and scalable enough for hundreds to a few thousand users.