Java: Threads - The Basics Before Going Reactive

Understanding how Java handles multiple things at once — and how Quarkus changes the game.

In my Back to Java journey, I’ve been rediscovering parts of the language that I used years ago but never really understood deeply.
Today’s topic is one of the most fundamental — and honestly, one of the most misunderstood: Threads.

Threads are the foundation of concurrency in Java.
And before I dive deeper into Quarkus’ reactive and non-blocking world, I want to understand what happens underneath.


What Are Threads (in general)?

A thread is like a worker inside your program — an independent flow of execution that can run code in parallel with others.

When you run a Java program, it always starts with one main thread:

public class MainExample {
    public static void main(String[] args) {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}

Output:

Running in: main

That’s your main worker.
Every additional thread you create runs alongside it.

Creating Threads in Java

There are two common ways to create threads:

1. Extend the Thread class

public class MyThread extends Thread {
    public void run() {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // Start a new thread
        System.out.println("Main: " + Thread.currentThread().getName());
    }
}
  • start() creates a new thread and calls run() asynchronously.
  • If you call run() directly, it just runs on the same thread — no concurrency.

2. Use a Runnable (preferred approach)

public class RunnableExample {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println("Running in: " + Thread.currentThread().getName());
        Thread thread = new Thread(task);
        thread.start();
    }
}

Runnable separates the work (the code you want to run) from the worker (the Thread).

Threads in Practice: A Quick Hands-On

Let’s simulate doing two things at once — something slow, like making coffee ☕ and toasting bread 🍞.

public class Breakfast {
    public static void main(String[] args) {
        Thread coffee = new Thread(() -> makeCoffee());
        Thread toast = new Thread(() -> toastBread());

        coffee.start();
        toast.start();
    }

    static void makeCoffee() {
        try {
            Thread.sleep(2000);
            System.out.println("☕ Coffee is ready!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static void toastBread() {
        try {
            Thread.sleep(3000);
            System.out.println("🍞 Bread is toasted!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output (order may vary):

☕ Coffee is ready!
🍞 Bread is toasted!

Both tasks run at the same time — this is concurrency in action.
Each task gets its own thread, and the program doesn’t wait for one to finish before starting the other.

Thread Pools: Managing Many Tasks

Creating new threads every time can be expensive.
A better way is to reuse threads using an ExecutorService (a thread pool).

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            pool.submit(() ->
                System.out.println("Running task " + taskId + " on " + Thread.currentThread().getName())
            );
        }

        pool.shutdown();
    }
}

This allows multiple tasks to share a limited number of threads efficiently — something frameworks like Quarkus also do under the hood.

Java Threads vs JavaScript Async (Same Goal, Different Path)

Java uses multiple threads to handle concurrency.

JavaScript (and TypeScript) runs on a single thread but uses an event loop to handle async operations (like fetch, setTimeout, etc.).

Feature Java JavaScript / TypeScript
Execution Model Multi-threaded Single-threaded with event loop
Parallel Execution Yes (real threads) No (cooperative async via callbacks/promises)
Example new Thread() setTimeout(), fetch(), Promise
Blocking Behavior Can block (Thread.sleep) Never blocks (async callback queued)

💡 So both aim to “do many things at once,” but JavaScript uses asynchronous scheduling, while Java can literally run multiple threads in parallel.

Threads and Quarkus: Same Idea, Smarter Execution

Quarkus applications still use threads — but they’re used more efficiently.

Traditional Java apps (like Spring or Servlet-based apps) use a thread per request model:

  • Every incoming HTTP request gets its own thread.
  • If a thread waits for a DB or network call, it’s blocked.

Quarkus, on the other hand, is built for non-blocking I/O:

  • Threads don’t sit idle waiting for I/O.
  • A small pool of threads can handle thousands of concurrent requests.
  • Reactive frameworks (like Mutiny and Vert.x) schedule work when data arrives — instead of holding a thread hostage.
Concept Traditional Java Quarkus Reactive
Model Thread per request Event-driven, non-blocking
Threads used Many Few (efficient reuse)
Behavior Blocking Reactive
Library Thread, ExecutorService Uni, Multi (Mutiny)

👉 So, threads are still there — Quarkus just uses them smarter.

Real-Life Scenario: 1000 Requests Hitting the Same Endpoint

Imagine this common situation:
You’ve built an endpoint /users/profile that calls an external user service (maybe a third-party authentication API).
Sometimes it’s fast (50 ms), sometimes slow (2 s).
Now — 1000 users open the app at once.

Traditional Java (Blocking I/O)

@Path("/users")
public class UserResource {

    @GET
    @Path("/profile")
    public String getProfile() {
        // Blocking call to external API
        String user = externalService.getUserData(); 
        return "Profile: " + user;
    }
}
  • Each request is assigned its own thread from a pool (say, 200 threads).
  • The thread makes the external call and then waits.
  • Once all 200 threads are busy, new requests are queued until one is free.

If 1000 requests come in:

  • 200 active threads, 800 waiting.
  • Threads consume memory even while idle.
  • Response time grows as requests queue up.
  • CPU usage stays low (most threads are sleeping).

Quarkus (Reactive / Non-Blocking I/O)

@Path("/users")
public class ReactiveUserResource {

    @GET
    @Path("/profile")
    public Uni<String> getProfile() {
        return externalService.getUserDataAsync() // returns Uni<String>
            .onItem().transform(user -> "Profile: " + user)
            .onFailure().recoverWithItem("Fallback profile");
    }
}
  • The call is non-blocking — it sends the HTTP request and releases the thread.
  • The same few event-loop threads handle other requests while waiting for responses.
  • When the API responds, the result is processed asynchronously.

If 1000 requests come in:

  • The same 8–16 event-loop threads can manage them all.
  • No threads sit idle; they’re constantly reused.
  • Memory footprint stays low, and response times remain stable even if some APIs are slow.
Step Traditional Java (Blocking) Quarkus Reactive (Non-Blocking)
Request arrives Takes a thread from pool Uses small event-loop thread
External call Thread waits Thread released immediately
While waiting Idle Reused for other requests
Response arrives Thread resumes work Async callback resumes pipeline
1000 concurrent requests Needs ~1000 threads (or queues) Same small pool handles all

💬 In short:

Traditional Java: many threads that spend most time waiting.
Quarkus: fewer threads that never wait — they react when data is ready.

What This Means in Real Life

  • Predictable performance: Even if a third-party API slows down, Quarkus keeps serving others.
  • Lower cost: Fewer threads → lower memory, better CPU utilization.
  • Massive scalability: A single service can handle thousands of concurrent users.
  • Still Java: It’s still built on threads — just orchestrated smarter.

☕ Analogy

Think of a coffee shop:

  • Traditional Java: one barista per customer — efficient until the queue grows.
  • Quarkus: a few baristas multitasking, preparing drinks while others brew — no one stands idle.

Scenarios Where Threads Are Useful

  1. Parallel processing: Doing multiple independent computations (e.g., processing files).
  2. Background tasks: Logging, cleanup, or asynchronous notifications.
  3. Simulations or CPU-bound tasks: Heavy calculations that benefit from multiple cores.
  4. Learning async behavior: Understanding how frameworks like Quarkus optimize concurrency.

Bringing It All Together

  • Threads are workers that help Java do multiple things at once.
  • Quarkus still relies on them — but in a reactive, efficient way.
  • JavaScript handles concurrency differently — single-threaded but event-driven.
  • And understanding these fundamentals helps make sense of why reactive programming feels so different (yet familiar).

Closing

Before learning Quarkus’ reactive style, I used to think “threads” were just a low-level concept from the old Java days.
But understanding how they work — and how Quarkus builds on top of them — makes the reactive magic feel more logical than mysterious.

October 20, 2025 · 7 min

Java: Reactive Programming in Quarkus with Mutiny

In my previous post, I introduced Quarkus and touched briefly on reactive programming. We saw how non-blocking code allows threads to do more work instead of waiting around.

This time, let’s go deeper — into Mutiny, Quarkus’ reactive programming library — and see how its chaining style makes reactive code both powerful and readable.

Traditional Java frameworks are blocking: one request = one thread. If the thread waits on I/O (DB, API, file), it’s stuck.

Reactive programming is non-blocking: the thread is freed while waiting, and picks up the result asynchronously. This makes applications more scalable and efficient, especially in the cloud.

That’s the foundation. Now let’s talk about Mutiny.

What is Mutiny?

Mutiny is the reactive programming API in Quarkus. It’s built on top of Vert.x but designed to be developer-friendly.

Its two core types are:

  • Uni<T> → a promise of one item in the future (like JavaScript’s Promise<T>).
  • Multi<T> → a stream of many items over time.

But the real magic of Mutiny is in its fluent chaining style.

Example: Calling an External API with Mutiny

Imagine you’re building a service that fetches user info from an external API (which might be slow). Instead of blocking while waiting, we’ll use Mutiny + chaining.

import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RestClient;

@Path("/users")
public class UserResource {

    @Inject
    @RestClient
    ExternalUserService externalUserService; // REST client interface

    @GET
    public Uni<String> getUserData() {
        return externalUserService.fetchUser()  // Uni<User>
            .onItem().transform(user -> {
                // Step 1: uppercase the name
                String upperName = user.getName().toUpperCase();

                // Step 2: return formatted string
                return "Hello " + upperName + " with id " + user.getId();
            })
            .onFailure().recoverWithItem("Fallback user");
    }
}

Alternative: Split Into Multiple Steps

Some developers prefer breaking the chain into smaller, clearer transformations:

return externalUserService.fetchUser()
    // Step 1: uppercase the name
    .onItem().transform(user -> new User(user.getId(), user.getName().toUpperCase()))
    // Step 2: format the message
    .onItem().transform(user -> "Hello " + user.getName() + " with id " + user.getId())
    // Step 3: fallback if something fails
    .onFailure().recoverWithItem("Fallback user");

Both versions are valid — it depends if you want compact or step-by-step clarity.

Without Mutiny: Blocking Example

If we weren’t using Mutiny, the call to fetchUser() would be blocking:

@Path("/users-blocking")
public class BlockingUserResource {

    @GET
    public String getUserData() {
        // Simulate a slow external API call
        User user = externalUserService.fetchUserBlocking(); // blocks thread
        String upperName = user.getName().toUpperCase();
        return "Hello " + upperName + " with id " + user.getId();
    }
}

In this case:

  • The thread waits until fetchUserBlocking() returns.
  • While waiting, the thread does nothing else.
  • If 100 requests arrive at once → you need 100 threads just sitting idle, each waiting for its response.
  • This quickly becomes heavy, especially in microservices where memory and threads are limited.

With Mutiny, the call returns immediately as a Uni<User>:

  • The thread is released right away and can handle another request.
  • When the external API responds, Quarkus resumes the pipeline and finishes processing.
  • If 100 requests arrive at once → you still only need a small pool of threads, since none of them sit idle waiting.
  • This means the service can scale much more efficiently with the same resources.

Common Mutiny Operators (Beyond transform)

Mutiny has a rich set of operators to handle different scenarios. Some useful ones:

  • onItem() – work with the item if it arrives.
    • .transform(x -> ...) → transform the result.
    • .invoke(x -> ...) → side-effect (like logging) without changing the result.
  • onFailure() – handle errors.
    • .recoverWithItem("fallback") → return a default value.
    • .retry().atMost(3) → retry the operation up to 3 times.
  • onCompletion() – run something once the pipeline is finished (success or failure).
  • Multi operators – streaming equivalents, e.g. .map(), .filter(), .select().first(n).
  • combine() – merge results from multiple Unis.
Uni.combine().all().unis(api1.call(), api2.call())
    .asTuple()
    .onItem().transform(tuple -> tuple.getItem1() + " & " + tuple.getItem2());

Why Mutiny’s Chaining Matters

  • Readable → async pipelines look like synchronous code.
  • Composable → add/remove steps easily without rewriting everything.
  • Declarative → you describe what should happen, not how.
  • Error handling inline.onFailure().recoverWithItem() instead of try/catch gymnastics.

Compared to raw Java CompletableFuture or even RxJava/Reactor, Mutiny feels lighter and easier to follow.

Where to Use Reactive + Mutiny

Reactive code shines in:

  • High-concurrency APIs → e.g., chat apps, booking systems, trading platforms.
  • Streaming/event-driven systems → Kafka, AMQP, live data.
  • Serverless apps → quick startup, minimal resource use.
  • Cloud-native microservices → scaling up/down efficiently.

But if you’re writing a small monolithic app, blocking may still be simpler and good enough.

Trade-offs to Keep in Mind

  • Learning curve → async code requires a shift in thinking.
  • Debugging → stack traces are harder to follow.
  • Overhead → reactive isn’t “free”; don’t use it unless concurrency/scalability matter.

Quarkus + Mutiny turns reactive programming from a “scary async monster” into something that feels natural and even elegant.

For me, the fluent chaining style is the deal-breaker — it makes reactive code look like a narrative, not a puzzle.

October 3, 2025 · 4 min

Java: And Quarkus

Back to Java, Now with Quarkus

After years of writing mostly in JavaScript and Python, I recently joined a company that relies on Java with Quarkus. Coming back to Java, I quickly realized Quarkus isn’t just “another framework”—it’s Java re-imagined for today’s cloud-native world.

What is Quarkus?

Quarkus is a Kubernetes-native Java framework built for modern apps. It’s optimized for:

  • Cloud (runs smoothly on Kubernetes, serverless, containers)
  • Performance (fast boot time, low memory)
  • Developer experience (hot reload, unified config, reactive support)

It’s often described as “Supersonic Subatomic Java.”

What’s the Difference?

Compared to traditional Java frameworks (like Spring Boot or Jakarta EE):

  • Startup time: Quarkus apps start in milliseconds, not seconds.
  • Memory footprint: Uses less RAM—great for microservices in containers.
  • Native compilation: Works with GraalVM to compile Java into native binaries.
  • Reactive by design: Built to handle modern async workloads.

Reactive Programming in Quarkus

One thing you’ll hear often in the Quarkus world is reactive programming.

At a high level:

  • Traditional Java apps are usually blocking → one request = one thread. If that thread is waiting for a database or network response, it just sits idle until the result comes back.
  • Reactive apps are non-blocking → threads don’t get stuck. Instead, when an I/O call is made (like fetching from a DB or API), the thread is freed to do other work. When the result is ready, the app picks it back up asynchronously.

Think of it like this:

  • Blocking (restaurant analogy): A waiter takes your order, then just stands at the kitchen until your food is ready. They can’t serve anyone else.
  • Non-blocking (reactive): The waiter takes your order, gives it to the kitchen, and immediately goes to serve another table. When your food is ready, they bring it over. Same waiter, more customers served.

Blocking vs Non-blocking in Quarkus

Blocking Example:

@Path("/blocking")
public class BlockingResource {

    @GET
    public String getData() throws InterruptedException {
        // Simulate slow service
        Thread.sleep(2000);
        return "Blocking response after 2s";
    }
}
  • Each request holds a thread for 2 seconds.
  • If 100 users hit this at once, you need 100 threads just waiting.

Non-blocking Example with Mutiny:

import io.smallrye.mutiny.Uni;
import java.time.Duration;

@Path("/non-blocking")
public class NonBlockingResource {

    @GET
    public Uni<String> getData() {
        // Simulate async response
        return Uni.createFrom()
            .item("Non-blocking response after 2s")
            .onItem().delayIt().by(Duration.ofSeconds(2));
    }
}
  • The thread is released immediately.
  • Quarkus will resume the request once the result is ready, without hogging threads.
  • Much more scalable in high-concurrency environments.

👉 In short: Reactive = Non-blocking = More scalable and efficient in modern distributed systems.

💡 Note on Mutiny Quarkus doesn’t invent its own reactive system from scratch. Instead, it builds on Vert.x (a popular reactive toolkit for the JVM) and introduces Mutiny as a friendly API for developers.

  • Uni<T> → like a Promise of a single item in the future.
  • Multi<T> → like a stream of multiple items over time.

So when you see Uni or Multi in Quarkus code, that’s Mutiny helping you handle non-blocking results in a clean, developer-friendly way.

When Should Developers Consider Quarkus?

You don’t always need Quarkus. Here are scenarios where it makes sense:

  • ✅ Microservices – You’re building many small services that need to be fast, lightweight, and cloud-friendly.
  • ✅ Containers & Kubernetes – Your apps are deployed in Docker/K8s and you want to reduce memory costs.
  • ✅ Serverless – Functions that need to start fast and consume minimal resources.
  • ✅ Event-driven / Reactive systems – You’re working with Kafka, messaging, or need to handle high concurrency.
  • ✅ Cloud cost optimization – Running many services at scale and every MB of memory counts.

On the other hand:

  • If you’re running a monolithic enterprise app on a stable server, traditional Java frameworks may be simpler.
  • If your team is heavily invested in another ecosystem (e.g., Spring), migration cost could outweigh the benefit.

Benefits at a Glance:

  • 🚀 Fast: Startup in milliseconds.
  • 🐇 Lightweight: Minimal memory usage.
  • 🐳 Container-native: Tailored for Docker/Kubernetes.
  • 🔌 Reactive-ready: Async handling out of the box.
  • 🔥 Fun to dev: Hot reload + clear config = better DX.

Java vs Quarkus: A Quick Comparison

Feature Traditional Java (e.g., Spring Boot) Quarkus
Startup Time Seconds (2–5s or more) Milliseconds (<1s possible)
Memory Usage Higher (hundreds MB) Lower (tens of MB)
Deployment Style Typically fat JARs JVM mode or Native binary
Container/Cloud Ready Works but heavy Built for it
Dev Experience Restart for changes Live reload (quarkus:dev)
Reactive Support Add-on via frameworks Built-in (Mutiny, Vert.x)

For me, Quarkus feels like Java reborn for the cloud era. It keeps the strengths of Java (ecosystem, type safety, mature libraries) but strips away the heavyweight feel.

October 1, 2025 · 4 min