Post

Creating a Lightweight EventBus in Java

Creating a Lightweight EventBus in Java

In many applications, components often need to communicate with each other in a decoupled way. This is where an event bus pattern comes in. In this post, we’ll build a lightweight EventBus system in Java that allows components to register as listeners and publish events to topics without needing to know who is listening.

We’ll walk through a simple implementation and explain how and why each piece works. By the end, you’ll understand how to integrate this pattern into your own applications.


🧠 What is an EventBus?

An EventBus is a messaging system within an application that lets objects communicate indirectly through events. It’s like a bulletin board — components post messages (events), and other components listen for specific types of messages.

Benefits of using an EventBus:

  • 📦 Loose coupling between components
  • 🔁 Reusability of components without tight dependencies
  • 🧪 Easier testing of individual parts

🏗️ Overview of the EventBus Implementation

Let’s break down the core components of the EventBus:

  • Topics are string identifiers (e.g., "movies", "tvShows")
  • Events contain metadata (sender, eventType)
  • Listeners implement the IEventListener interface
  • A single-threaded executor batches event delivery

This implementation supports:

  • ✅ Registering and removing listeners
  • ✅ Publishing events asynchronously with slight delay
  • ✅ Thread-safe operations using ReentrantReadWriteLock

📦 The Event Class

The Event is a simple record that encapsulates information about an action.

1
public record Event(Object sender, String eventType)

🔹 Usage Examples

1
2
Event saveEvent = Event.createSaveEvent(this);
Event addEvent = Event.createAddEvent(myService);

✨ Features

  • Static factory methods for common event types: save, add, remove
  • Overrides equals() and hashCode() so they can be used in sets
  • Compact thanks to Java’s record syntax

🧩 The Listener Interface

To respond to events, your component just needs to implement the IEventListener interface:

1
2
3
public interface IEventListener {
  void processEvent(Event event);
}

It’s a simple contract, but it gives flexibility. Any class implementing this can act as a subscriber for any topic.


⚙️ The EventBus Class

Here’s where the magic happens. This singleton class acts as the message hub.

🔐 Thread Safety

We use ReentrantReadWriteLock to manage concurrent access to listeners and events. This ensures safe modifications across threads without sacrificing performance.


🔌 Registering Listeners

1
EventBus.registerListener("movies", myListener);
1
2
3
4
public static void registerListener(String topic, IEventListener listener) {
  INSTANCE.readWriteLock.writeLock().lock();
  ...
}

🔑 We use a writeLock here to protect shared state during modifications.


❌ Removing Listeners

1
EventBus.removeListener("movies", myListener);
1
2
3
4
public synchronized static void removeListener(String topic, IEventListener listener) {
  INSTANCE.readWriteLock.writeLock().lock();
  ...
}

This helps avoid memory leaks or unwanted notifications when components are destroyed or no longer interested.


📤 Publishing Events

1
EventBus.publishEvent("movies", Event.createAddEvent(this));
1
public static void publishEvent(String topic, Event event)

🕒 Events are scheduled to be dispatched after 250ms using a ScheduledExecutorService. This slight delay helps batch rapid-fire events and ensures smoother performance in GUI or service-heavy apps.


⚙️ The EventBus Class (Full Implementation)

Here is the complete implementation of the EventBus class, as used in this article:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class EventBus {
  private static final EventBus INSTANCE = new EventBus();

  private final ReentrantReadWriteLock readWriteLock;
  private final Map<String, Set<IEventListener>> listeners;
  private final Map<String, Set<Event>> events;
  private final ScheduledExecutorService executor;

  private EventBus() {
    readWriteLock = new ReentrantReadWriteLock();
    listeners = new HashMap<>();
    events = new HashMap<>();
    executor = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "event-bus"));
  }

  public static void registerListener(String topic, IEventListener listener) {
    try {
      INSTANCE.readWriteLock.writeLock().lock();
      Set<IEventListener> listeners = INSTANCE.listeners.computeIfAbsent(topic, k -> new HashSet<>());
      listeners.add(listener);
    } finally {
      INSTANCE.readWriteLock.writeLock().unlock();
    }
  }

  public synchronized static void removeListener(String topic, IEventListener listener) {
    try {
      INSTANCE.readWriteLock.writeLock().lock();
      Set<IEventListener> listeners = INSTANCE.listeners.get(topic);
      if (listeners != null) {
        listeners.remove(listener);
      }
    } finally {
      INSTANCE.readWriteLock.writeLock().unlock();
    }
  }

  public static void publishEvent(String topic, Event event) {
    try {
      INSTANCE.readWriteLock.writeLock().lock();
      INSTANCE.events.computeIfAbsent(topic, k -> new LinkedHashSet<>()).add(event);

      Runnable runnable = () -> {
        Set<Event> events = new LinkedHashSet<>();
        Set<IEventListener> listeners = new HashSet<>();

        try {
          INSTANCE.readWriteLock.writeLock().lock();
          Set<Event> eventsForTopic = Objects.requireNonNullElse(INSTANCE.events.get(topic), Collections.emptySet());
          if (!eventsForTopic.isEmpty()) {
            events.addAll(eventsForTopic);
            eventsForTopic.clear();
          }
          listeners.addAll(Objects.requireNonNullElse(INSTANCE.listeners.get(topic), Collections.emptySet()));
        } finally {
          INSTANCE.readWriteLock.writeLock().unlock();
        }

        for (Event e : events) {
          for (IEventListener listener : listeners) {
            listener.processEvent(e);
          }
        }
      };

      INSTANCE.executor.schedule(runnable, 250, TimeUnit.MILLISECONDS);
    } finally {
      INSTANCE.readWriteLock.writeLock().unlock();
    }
  }
}

🧪 How It All Works Together

Let’s say you have a movie editor UI and a background service:

  1. The UI publishes a "save" event to "movies" when a movie is saved.
  2. A logging component and a database service have both registered as listeners for "movies".
  3. They each receive the event and perform their tasks independently.

No hard dependencies. No spaghetti code. Just clean, event-driven design.


🧠 Final Thoughts

Building your own EventBus can be a powerful way to learn about concurrency, asynchronous programming, and design patterns. While full-featured solutions like Guava’s EventBus exist, this DIY version is perfect for projects where you want:

  • Minimal dependencies
  • Full control over behavior
  • A better understanding of the inner workings

Hope this helps you on your journey toward cleaner, more modular Java applications!

If you’re interested in extending this, you could:

  • Add support for wildcard topics
  • Include listener priorities
  • Use weak references to prevent memory leaks
This post is licensed under CC BY 4.0 by the author.