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()
andhashCode()
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:
- The UI publishes a
"save"
event to"movies"
when a movie is saved. - A logging component and a database service have both registered as listeners for
"movies"
. - 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