diff --git a/src/main/java/dev/kske/eventbus/Event.java b/src/main/java/dev/kske/eventbus/Event.java
new file mode 100644
index 0000000..b67fdc0
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/Event.java
@@ -0,0 +1,34 @@
+package dev.kske.eventbus;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.*;
+
+/**
+ * Indicates that a method is an event handler. To be successfully used as such, the method has to
+ * comply with the following specifications:
+ *
+ * - Declared inside a class that implements {@link EventListener}
+ * - One parameter of a type that implements {@link IEvent}
+ * - Return type of {@code void}
+ *
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ */
+@Documented
+@Retention(RUNTIME)
+@Target(METHOD)
+public @interface Event {
+
+ /**
+ * Defines the priority of the event handler. Handlers are executed in descending order of their
+ * priority.
+ *
+ * The execution order of handlers with the same priority is undefined.
+ *
+ * @since 0.0.1
+ */
+ int priority() default 100;
+}
diff --git a/src/main/java/dev/kske/eventbus/EventBus.java b/src/main/java/dev/kske/eventbus/EventBus.java
new file mode 100644
index 0000000..c44cb8d
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/EventBus.java
@@ -0,0 +1,120 @@
+package dev.kske.eventbus;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Event listeners can be registered at an event bus to be notified when an event is dispatched.
+ *
+ * This is a thread-safe implementation.
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ * @see Event
+ */
+public final class EventBus {
+
+ private final Map, Collection> bindings
+ = new ConcurrentHashMap<>();
+ private final Set registeredListeners = ConcurrentHashMap.newKeySet();
+
+ /**
+ * Dispatches an event to all event handlers registered for it in descending order of their
+ * priority.
+ *
+ * @param event the event to dispatch
+ * @since 0.0.1
+ */
+ public void dispatch(IEvent event) {
+ getHandlersFor(event.getClass()).forEach(handler -> handler.execute(event));
+ }
+
+ /**
+ * Searches for the event handlers bound to an event class.
+ *
+ * @param eventClass the event class to use for the search
+ * @return all event handlers registered for the event class
+ * @since 0.0.1
+ */
+ private List getHandlersFor(Class extends IEvent> eventClass) {
+ return bindings.containsKey(eventClass) ? new ArrayList<>(bindings.get(eventClass))
+ : new ArrayList<>();
+ }
+
+ /**
+ * Registers an event listener at this event bus.
+ *
+ * @param listener the listener to register
+ * @throws EventBusException if the listener is already registered or a declared event handler
+ * does not comply to the specification
+ * @since 0.0.1
+ * @see Event
+ */
+ public void registerListener(EventListener listener) throws EventBusException {
+ if (registeredListeners.contains(listener))
+ throw new EventBusException(listener + " already registered!");
+
+ registeredListeners.add(listener);
+ for (var method : listener.getClass().getDeclaredMethods()) {
+ Event annotation = method.getAnnotation(Event.class);
+
+ // Skip methods without annotations
+ if (annotation == null)
+ continue;
+
+ // Check for correct method signature and return type
+ if (method.getParameterCount() != 1)
+ throw new EventBusException(method + " does not have an argument count of 1!");
+
+ if (!method.getReturnType().equals(void.class))
+ throw new EventBusException(method + " does not have a return type of void!");
+
+ var param = method.getParameterTypes()[0];
+ if (!IEvent.class.isAssignableFrom(param))
+ throw new EventBusException(param + " is not of type IEvent!");
+
+ @SuppressWarnings("unchecked")
+ var realParam = (Class extends IEvent>) param;
+ if (!bindings.containsKey(realParam))
+ bindings.put(realParam, new HashSet<>());
+
+ bindings.get(realParam).add(new EventHandler(listener, method, annotation));
+ }
+ }
+
+ /**
+ * Removes a specific listener from this event bus.
+ *
+ * @param listener the listener to remove
+ * @since 0.0.1
+ */
+ public void removeListener(EventListener listener) {
+ for (var binding : bindings.values()) {
+ var it = binding.iterator();
+ while (it.hasNext())
+ if (it.next().getListener() == listener)
+ it.remove();
+ }
+ registeredListeners.remove(listener);
+ }
+
+ /**
+ * Removes all event listeners from this event bus.
+ *
+ * @since 0.0.1
+ */
+ public void clearListeners() {
+ bindings.clear();
+ registeredListeners.clear();
+ }
+
+ /**
+ * Provides an unmodifiable view of the event listeners registered at this event bus.
+ *
+ * @return all registered event listeners
+ * @since 0.0.1
+ */
+ public Set getRegisteredListeners() {
+ return Collections.unmodifiableSet(registeredListeners);
+ }
+}
diff --git a/src/main/java/dev/kske/eventbus/EventBusException.java b/src/main/java/dev/kske/eventbus/EventBusException.java
new file mode 100644
index 0000000..1c46c80
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/EventBusException.java
@@ -0,0 +1,21 @@
+package dev.kske.eventbus;
+
+/**
+ * This runtime exception is thrown when an event bus error occurs. This can either occur while
+ * registering event listeners with invalid handlers, or when an event handler throws an exception.
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ */
+public class EventBusException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public EventBusException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public EventBusException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/dev/kske/eventbus/EventHandler.java b/src/main/java/dev/kske/eventbus/EventHandler.java
new file mode 100644
index 0000000..7619d72
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/EventHandler.java
@@ -0,0 +1,82 @@
+package dev.kske.eventbus;
+
+import java.lang.reflect.*;
+
+/**
+ * Internal representation of an event handling method.
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ * @see EventBus
+ */
+final class EventHandler implements Comparable {
+
+ private final EventListener listener;
+ private final Method method;
+ private final Event annotation;
+
+ /**
+ * Constructs an event handler.
+ *
+ * @param listener the listener containing the handler
+ * @param method the handler method
+ * @param annotation the event annotation
+ * @since 0.0.1
+ */
+ EventHandler(EventListener listener, Method method, Event annotation) {
+ this.listener = listener;
+ this.method = method;
+ this.annotation = annotation;
+ }
+
+ /**
+ * Compares this to another event handler based on {@link Event#priority()}. In case of equal
+ * priority a non-zero value based on hash codes is returned.
+ *
+ * @since 0.0.1
+ */
+ @Override
+ public int compareTo(EventHandler other) {
+ int priority = annotation.priority() - other.annotation.priority();
+ if (priority == 0)
+ priority = listener.hashCode() - other.listener.hashCode();
+ return priority == 0 ? hashCode() - other.hashCode() : priority;
+ }
+
+ /**
+ * Executes the event handler.
+ *
+ * @param event the event used as the method parameter
+ * @throws EventBusException if the handler throws an exception
+ * @since 0.0.1
+ */
+ void execute(IEvent event) throws EventBusException {
+ try {
+ method.invoke(listener, event);
+ } catch (
+ IllegalAccessException
+ | IllegalArgumentException
+ | InvocationTargetException e
+ ) {
+ throw new EventBusException("Failed to invoke event handler!", e);
+ }
+ }
+
+ /**
+ * @return the listener containing this handler
+ * @since 0.0.1
+ */
+ EventListener getListener() { return listener; }
+
+ /**
+ * @return the event annotation
+ * @since 0.0.1
+ */
+ Event getAnnotation() { return annotation; }
+
+ /**
+ * @return the priority of the event annotation
+ * @since 0.0.1
+ */
+ int getPriority() { return annotation.priority(); }
+}
diff --git a/src/main/java/dev/kske/eventbus/EventListener.java b/src/main/java/dev/kske/eventbus/EventListener.java
new file mode 100644
index 0000000..bc48ec7
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/EventListener.java
@@ -0,0 +1,12 @@
+package dev.kske.eventbus;
+
+/**
+ * Marker interface for event listeners. Event listeners can contain event handling methods to which
+ * events can be dispatched.
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ * @see Event
+ * @see EventBus
+ */
+public interface EventListener {}
diff --git a/src/main/java/dev/kske/eventbus/IEvent.java b/src/main/java/dev/kske/eventbus/IEvent.java
new file mode 100644
index 0000000..fe9e843
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/IEvent.java
@@ -0,0 +1,12 @@
+package dev.kske.eventbus;
+
+/**
+ * Marker interface for event objects. Event objects can be used as event handler parameters and
+ * thus can be dispatched to the event bus.
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ * @see Event
+ * @see EventBus
+ */
+public interface IEvent {}
diff --git a/src/main/java/dev/kske/eventbus/package-info.java b/src/main/java/dev/kske/eventbus/package-info.java
new file mode 100644
index 0000000..99c8ccf
--- /dev/null
+++ b/src/main/java/dev/kske/eventbus/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Contains the public API and implementation of the event bus library.
+ *
+ * @author Kai S. K. Engelbart
+ * @since 0.0.1
+ * @see dev.kske.eventbus.Event
+ * @see dev.kske.eventbus.EventBus
+ */
+package dev.kske.eventbus;