Callback functions in Java
What a callback is, synchronous versus asynchronous callbacks, the move from anonymous classes to lambdas, listeners and the observer pattern, CompletableFuture chains, which thread runs your callback, listener leaks and error handling.
On this page
📞 What is a callback function?
A callback is a piece of code you hand to another method so that method can call you back when something happens. In Java the "piece of code" is an object implementing an interface, today usually a lambda. For example, gateway.charge(order, receipt -> email.send(receipt)) says: charge this order, and when you have a receipt, run my code with it.
Callbacks are everywhere in Java, even when nobody uses the word. list.sort(comparator) calls your comparator back. executor.submit(task) calls your task back on another thread. Spring's @EventListener, a Kafka consumer's record handler, a servlet filter chain and a CompletableFuture stage are all callbacks with nicer names. Interviewers ask about them because the concept is easy and the consequences are not: which thread runs my code, what happens when it throws, and why is this screen still in memory after the user left?
What you’ll learn
- What a callback is, and the difference between a synchronous callback (runs before the call returns) and an asynchronous one (runs later, often on another thread)
- How callbacks evolved from anonymous classes to lambdas, and what each one captures from the enclosing object
- How to build a listener (observer) registry that is thread-safe and can be unsubscribed
- How
CompletableFutureturns nested callbacks into a flat chain withthenApply,thenAcceptandexceptionally, and which thread runs each stage - Why registered listeners are a classic memory leak, and how to handle errors so a failing callback doesn't vanish silently
For this lesson, we'll focus on what a callback is, synchronous versus asynchronous callbacks, how they moved from anonymous classes to lambdas, the listener (observer) pattern, a first look at CompletableFuture as the fix for nested callbacks, threading, leaks and error handling.
Below the line (out of scope)
- The functional interfaces callbacks are built on: Functional interfaces in Java
CompletableFuturein depth (composition, timeouts, custom executors) and thread pools: later in the course, in the concurrency module- Reactive streams (Project Reactor, RxJava) and back-pressure
The 30-second answer
A callback is code passed as an argument, usually a lambda implementing a functional interface, that the receiver invokes later. A synchronous callback runs on the caller's thread before the method returns, like a Comparator inside sort. An asynchronous callback runs after the method returns, usually on another thread, like a payment-result handler. Before Java 8 we wrote anonymous classes; lambdas made callbacks one line and, unlike anonymous classes, don't capture this unless they use it. Nested async callbacks become "callback hell", which CompletableFuture flattens into thenApply, thenAccept and exceptionally. The three things I always check: which thread runs it, where its exceptions go, and who unregisters it.
Interview radar
Asked very often: What is a callback in Java, and what's the difference between a synchronous and an asynchronous callback?
Senior rounds probe: Which thread runs an async callback, where its exceptions go, and how CompletableFuture flattens callback hell.
Production angle: A listener that was never unregistered keeps a whole user session reachable and slowly leaks memory.
01Picture it first: leaving your number with the tailor
You drop a suit at the tailor. You could stand at the counter until it's done (that's a normal blocking call). Instead you leave your phone number and walk away; when the suit is ready, the tailor calls you. Your phone number is the callback: you gave someone a way to reach you, and they decide when to use it.
Now the two flavours. If the tailor fixes a button while you watch and then hands the suit back, any "call me when the button is on" happened before you left the counter: a synchronous callback. If the alteration takes three days, the call comes later while you're doing something else: an asynchronous callback. Same phone number, very different consequences for whoever picks up.
Here are the two timelines side by side; watch which thread runs the callback in each.
Two timelines: in a synchronous callback the main thread calls sort, sort calls the comparator back on the same thread, and only then returns; in an asynchronous callback the main thread calls charge, which returns at once, and later a worker thread runs the callback while main has moved on
02Callbacks under the hood
Try it yourself first
Before reading on, predict the order of the four lines this prints. Which of them could change between runs?
List<Order> orders = new ArrayList<>(List.of(new Order("B", 300), new Order("A", 900)));
System.out.println("1. before sort");
orders.sort((x, y) -> { // synchronous callback
System.out.println("2. comparing on " + Thread.currentThread().getName());
return Long.compare(x.cents(), y.cents());
});
System.out.println("3. after sort");
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(() -> System.out.println("4. async on " + Thread.currentThread().getName()));
System.out.println("5. submitted");
pool.shutdown();
Lines 1, 2 and 3 always appear in that order, and line 2 says main. sort doesn't return until it has finished calling your comparator. Lines 4 and 5 can appear in either order: submit returns as soon as the task is queued, and the callback runs on pool-1-thread-1 whenever that thread gets scheduled.
Notice three things:
- The mechanism is the same. Both callbacks are objects implementing an interface (
Comparator,Runnable), and the receiver calls an interface method on them. There's no special "callback" feature in the language. - What differs is the contract. A synchronous callback is part of the call: its exceptions propagate to you, it sees your locks, and it can safely touch your local state. An asynchronous callback lives on its own: different thread, different stack, and its exceptions go wherever the receiver decides.
- The name of the thread tells you everything. When debugging callback code, logging
Thread.currentThread().getName()answers half the questions.
From anonymous classes to lambdas
Try it yourself first
Before reading on: the two callbacks below do the same thing. What does each one hold a reference to?
public class CheckoutScreen {
private final byte[] productImages = new byte[50 * 1024 * 1024]; // 50 MB
// Java 7 style
PaymentCallback oldStyle() {
return new PaymentCallback() {
@Override public void onSuccess(Receipt r) { System.out.println("Paid " + r.id()); }
@Override public void onFailure(Exception e) { System.out.println("Failed: " + e.getMessage()); }
};
}
// Java 8+ style, for a single-method callback
Consumer<Receipt> newStyle() {
return r -> System.out.println("Paid " + r.id());
}
}
interface PaymentCallback {
void onSuccess(Receipt receipt);
void onFailure(Exception error);
}
The anonymous class compiles to its own file, CheckoutScreen$1.class. Compiled for Java 17 or earlier, it has a hidden field final CheckoutScreen this$0, so the callback keeps the whole screen, and its 50 MB of images, reachable. Since Java 18, javac drops this$0 when the anonymous class never uses it, but the moment its body reads a field, the reference is back.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Real world: the listener that kept a whole session alive
- Advanced: futures, threads and errors
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal
Further reading (8)
- JLS §15.27 Lambda Expressions
- CompletableFuture (Java 21 API)
- The Java Tutorials: Writing Event Listeners
- The Java Tutorials: Lambda Expressions
- Effective Java, 3rd ed., Item 79: Avoid excessive synchronization (alien methods and listeners)
- Baeldung: Callback Functions in Java
- Baeldung: The Observer Pattern in Java
- Baeldung: Guide to CompletableFuture