Lambdas & Functional Interfaces

Project: A Mini Callback System

15 min Lesson 10 of 13

Project: A Mini Callback System

Throughout this tutorial you have learned about lambda syntax, functional interfaces, Predicate, Function, Consumer, Supplier, method references, and function composition. In this final lesson you pull all of those ideas together into a small but realistic project: a mini callback / event-bus utility built entirely on functional interfaces — no third-party libraries, no heavyweight frameworks.

The goal is not to build a production event system. The goal is to see how lambdas replace boilerplate, how you can store and compose behaviour as data, and why this design is so flexible.

What We Are Building

We will build an EventBus class that lets you:

  • Register multiple Consumer<T> listeners for a named event topic.
  • Fire (publish) an event, which calls every registered listener in order.
  • Register one-time listeners that automatically unsubscribe after their first invocation.
  • Add a Predicate<T> filter so a listener only fires when a condition is met.

The entire implementation is under 80 lines of Java and uses nothing but java.util.function and standard collections.

Step 1 — The Core EventBus

Start with the simplest version: a map from topic string to a list of Consumer callbacks.

import java.util.*; import java.util.function.Consumer; public class EventBus<T> { // Each topic holds an ordered list of consumers private final Map<String, List<Consumer<T>>> listeners = new HashMap<>(); /** Register a listener for a topic. */ public void on(String topic, Consumer<T> listener) { listeners .computeIfAbsent(topic, k -> new ArrayList<>()) .add(listener); } /** Fire an event, invoking every listener registered for that topic. */ public void emit(String topic, T event) { List<Consumer<T>> handlers = listeners.getOrDefault(topic, List.of()); handlers.forEach(h -> h.accept(event)); } }

Usage is already clean:

EventBus<String> bus = new EventBus<>(); bus.on("login", user -> System.out.println("Welcome, " + user)); bus.on("login", user -> System.out.println("Audit log: " + user + " logged in")); bus.emit("login", "Alice"); // Welcome, Alice // Audit log: Alice logged in
Why Consumer<T>? A callback that reacts to an event but returns nothing is the exact contract of Consumer. There is no need to invent an interface — the JDK already has the right shape.

Step 2 — One-Time Listeners

A once listener fires on the first matching event and then removes itself. The trick: wrap the caller-supplied consumer in another consumer that unregisters itself before delegating.

public void once(String topic, Consumer<T> listener) { // We need a reference to the wrapper so it can remove itself Consumer<T>[] wrapperHolder = new Consumer[1]; wrapperHolder[0] = event -> { listeners.getOrDefault(topic, List.of()).remove(wrapperHolder[0]); listener.accept(event); }; on(topic, wrapperHolder[0]); }
The single-element array trick is a common workaround for the "effectively final" rule inside lambdas. The array reference itself is final; only its contents change. An alternative is a private inner class field — both are valid.

Step 3 — Filtered Listeners

Add an onIf method that accepts a Predicate<T>. The listener runs only when the predicate returns true. This keeps business logic out of individual handlers.

import java.util.function.Predicate; public void onIf(String topic, Predicate<T> filter, Consumer<T> listener) { on(topic, event -> { if (filter.test(event)) { listener.accept(event); } }); }

Now you can write expressive subscriber registrations:

EventBus<Order> orderBus = new EventBus<>(); // Only notify the fraud-check service for large orders orderBus.onIf("order.placed", order -> order.total() > 500.0, order -> FraudService.check(order)); // Always log every order orderBus.on("order.placed", order -> Logger.log(order));

Step 4 — Putting It All Together

Here is a runnable demo using a simple record as the event payload:

record UserEvent(String name, String action) {} public class Main { public static void main(String[] args) { EventBus<UserEvent> bus = new EventBus<>(); // Always log every event bus.on("user", e -> System.out.println("[LOG] " + e.name() + " → " + e.action())); // Only greet users who just signed up (filtered) bus.onIf("user", e -> e.action().equals("signup"), e -> System.out.println("Welcome aboard, " + e.name() + "!")); // One-time alert for the very first login of the session bus.once("user", e -> System.out.println("First event received: " + e)); bus.emit("user", new UserEvent("Alice", "signup")); bus.emit("user", new UserEvent("Bob", "login")); bus.emit("user", new UserEvent("Alice", "login")); } } // [LOG] Alice → signup // Welcome aboard, Alice! // First event received: UserEvent[name=Alice, action=signup] // [LOG] Bob → login // [LOG] Alice → login
Thread safety: this implementation is single-threaded. If you emit events from multiple threads, wrap the list mutations in synchronized blocks or replace ArrayList with CopyOnWriteArrayList. Always identify concurrency requirements before reaching for the simplest data structure.

Design Takeaways

  • Behaviour as data: lambdas stored in a List<Consumer<T>> are first-class values you can add, remove, and iterate — exactly like any other object.
  • Composition without inheritance: onIf combines a Predicate and a Consumer into a new Consumer inline, no subclass required.
  • Open/Closed: to add a new kind of listener (throttled, async, logged) you wrap the existing consumer — you never modify the bus itself.
  • Testability: because every handler is just a function, you can pass test doubles (assertions as lambdas) with zero mocking framework overhead.

Summary

You have built a fully functional, composable callback system in under 80 lines by applying every concept from this tutorial: lambdas as listener implementations, Consumer for event handling, Predicate for filtering, variable capture for the once-wrapper, and inline function composition for onIf. This is the essence of functional-style Java — small, well-named pieces of behaviour wired together rather than deep class hierarchies.