Course contents

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.

PremiumGood to knowLesson14 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: leaving your number with the tailor
  2. Callbacks under the hood
  3. Real world: the listener that kept a whole session alive
  4. Advanced: futures, threads and errors
  5. How to say it in the interview

📞 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 CompletableFuture turns nested callbacks into a flat chain with thenApply, thenAccept and exceptionally, 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
  • CompletableFuture in 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.

Fig. 1

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

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

Practise this topicFunctional Java basics: interview questions & scenarios
Further reading (8)