👨‍💻 Hazriq’s Dev Blog

Learning in public. Building and exploring tech world.

Containers: Why They Exist and Why I’m Learning Them

I’ve been seeing containers everywhere.

Job descriptions. Architecture diagrams.
AWS services like ECS, ECR, and Fargate.
Even simple tutorials casually say “just run it in a container”.

For a long time, containers felt like something I could delay learning.
But at some point, delaying fundamentals becomes technical debt — in your own understanding.

This post is the first in a series where I slow down and learn containers from the basics, before jumping into tools or AWS services.


The problem before containers

Before containers became common, deploying an application often meant dealing with questions like:

  • What OS is this running on?
  • Which runtime version is installed?
  • What dependencies already exist on this machine?
  • Why does it work on my machine but not somewhere else?

As systems grew more complex, these problems didn’t disappear — they multiplied.

Most of the time, the issue wasn’t the application logic itself.
It was the environment around the code.


Life before containers (from my own experience)

I started my development career around 2014 — before Docker was widely adopted.

Back then, running an application usually meant:

  • Spinning up a virtual machine
  • Making sure the VM was reachable and stable
  • Installing runtimes and system dependencies
  • Managing configuration using tools like Puppet or Ansible

Configuration management helped, but it didn’t remove friction.

If I wanted a clean environment for a new piece of work, I couldn’t simply reset things.

I often had to:

  • Tear down the VM
  • Create a new one
  • Reapply configuration
  • Redeploy the application

This took time — but more importantly, it created mental overhead.

Every new task came with a quiet doubt:

“Is this environment actually clean, or am I fighting leftovers from my previous work?”

The environment itself became part of the problem I had to manage.


“It works on my machine”

Another common issue was environment mismatch across stages.

Something would work perfectly on my local machine — but fail in QA.

After some digging, the causes were often familiar:

  • Different OS versions
  • Slightly different VM specs
  • Missing or extra system libraries
  • Configuration values that didn’t fully match

Each environment looked similar on paper, but behaved differently in practice.

Debugging became less about the code and more about guessing what was different this time.

This wasn’t anyone’s fault — it was simply hard to keep environments truly identical.


What is a container (conceptually)?

At a high level, a container is a way to package:

  • Application code
  • Runtime and dependencies
  • Basic environment assumptions

…into a single, predictable unit.

Instead of saying:

“This app needs Python 3.11, these libraries, and these system configs”

You’re effectively saying:

“Here is everything this app needs to run.”

That same package can then run consistently:

  • On a developer’s laptop
  • On a QA environment
  • In production
  • In the cloud

The key value of containers isn’t performance.
It’s consistency and repeatability.


What problem do containers actually solve?

Containers don’t eliminate all complexity — but they reduce several painful ones:

  • Environment drift over time
  • Differences between local, QA, and production
  • Lengthy setup instructions for new machines
  • Fear of hidden configuration changes

By making the runtime environment explicit and versioned, containers shrink the gap between: development → testing → production.

That’s why they became a foundation of modern software delivery.


A very light look at containers in AWS

In the cloud — especially AWS — containers are treated as first-class citizens.

AWS provides managed services that:

  • Store container images
  • Run containers
  • Scale them
  • Handle the infrastructure underneath

At this stage, the service names aren’t important yet.
What matters is understanding that containers are the unit of execution, and cloud platforms are built around running them reliably.

I’ll dive into AWS-specific services later in this series — once the fundamentals are solid.


What’s next

This post intentionally avoids tools and commands.

Before touching Docker or AWS services, I want to clearly understand:

  • Why containers exist
  • What problems they were designed to solve
  • How they changed the way applications are delivered

In the next post, I’ll walk through the full lifecycle of a containerised application — from code to production — still at a conceptual level.

Slow progress, but solid foundations.

January 31, 2026 · 4 min

2026: Levelling Up as Software Engineer

It’s the first day of 2026, and I’ve been thinking a lot about my journey as a software engineer. I’ve been lucky enough to work with some really strong colleagues; be it Engineers, Senior/ Staff Engineers, even my Engineering Manager — the kind who stay calm in complex situations, simplify chaos, and make technical decisions look effortless.

Every time I work with engineers like that, I feel two things at the same time:

  1. admiration, because their clarity and depth are inspiring, and
  2. hunger, because I want to reach that level one day.

I’m a generalist by nature — I’ve worked with JavaScript, TypeScript, Java, Python, backend, web, AI, cloud… a bit of everything. That helped me survive and adapt for nine years, but in 2026 I want to start sharpening that generalist skill into something more senior, more intentional, and more impactful.

To stay relevant — and honestly, to move up — I need to level up in a few strategic areas.

🚀 AI — Becoming More Than Just an “API Consumer”

My team is venturing deeper into AI, and I don’t want to be the engineer who just calls /v1/chat/completions.
I want to understand how real AI systems are built.

My AI focus areas for 2026:

  • Making RAG production-grade (chunking strategies, embeddings, evaluation)
  • Understanding fine-tuning (LoRA basics, when it makes sense, data prep)
  • Learning agentic workflows (tools, memory, reasoning steps)
  • Becoming better at AI observability (prompt logs, latency, cost, failure patterns)

The industry is heading toward AI-heavy systems, and I want to be one of the engineers who understand the inner mechanics, not just the surface.

🟦 Quarkus — Strengthening My Cloud-Native Java Skills

Quarkus recently became part of my day-to-day work, and it’s something I want to get better at. Not just using it, but being confident designing and delivering backend services with it.

My Quarkus goals for 2026:

  • Understand Quarkus deeply (DI, RESTEasy, configuration, native builds)
  • Improve testing using Dev Mode and TestContainers
  • Deploy Quarkus microservices on AWS
  • Learn reactive patterns where needed
  • Build services with proper observability and performance considerations

Quarkus is modern, fast, and cloud-friendly — mastering it lines up perfectly with where my team is heading.

☁️ AWS — Becoming Comfortable, Not Just Familiar

I’m still fairly new to AWS, but in 2026 I want to reach the point where I can not only build but also architect.

The AWS areas that matter most to a backend + AI engineer:

  • Core compute: Lambda or ECS (choosing one to master deeply)
  • Messaging: SQS + SNS
  • Storage: S3, DynamoDB or RDS
  • API Gateway and EventBridge
  • Infrastructure as Code using CDK
  • Basic understanding of Bedrock and ML integrations

I don’t need to know every AWS service — just the ones required to build reliable and scalable systems.

🧠 Preparing for a Senior Role — Systems, Design, Leadership

Tech skills make you a strong mid-level engineer.
System thinking, decision-making, and leadership push you toward senior.

My senior-prep goals for 2026:

  • Deepen system design fundamentals: CQRS, event-driven design, sagas, caching, idempotency
  • Design with scale in mind: throughput, failure modes, SLAs and SLOs
  • Own services end-to-end: API → deployment → monitoring
  • Mentor juniors and help improve team engineering culture
  • Lead small projects independently

This is the layer I want to grow into more intentionally this year.

🔮 What Software Engineering Will Look Like in 2026 (and Why I Need to Prepare)

2026 is already showing signs of how fast the engineering world is changing.
More companies are integrating AI directly into their products, and AI tools are starting to take over the “easy” parts of software engineering:

  • boilerplate code
  • documentation
  • writing basic tests
  • generating CRUD services
  • initial architecture drafts

That means the value of a software engineer can no longer just be “I can code.”
AI can code too — very well, and very fast.

The real value now comes from:

  • understanding systems end-to-end
  • designing scalable architectures
  • knowing how to integrate AI responsibly
  • making good tradeoffs
  • writing code that works reliably in production
  • knowing AWS, cloud patterns, and distributed systems
  • collaborating across teams
  • leading projects and mentoring others

If I don’t prepare for this shift, the role will only get harder.

But if I grow in the right areas — AI, cloud, system design, backend architecture — I think 2026 can be the year I step closer to the engineer I want to be:

  • someone confident, impactful, and ready for the next level.

Here’s to a challenging but exciting year ahead.
Happy 2026.

H.

January 1, 2026 · 4 min

Databricks: What Is That?

For a long time, Python + pandas was more than enough for me.

Read some data, process it, send/ save the result somewhere.

Simple. Familiar. Productive.

Until one day… it wasn’t.

My Usual Flow: Python + Pandas

My default approach was always the same:

  1. Use pandas to read data
  2. Apply business logic
  3. Push results to another system

It worked well, especially for:

  • Small to medium datasets
  • One-off jobs
  • Quick experiments

If you’ve written backend scripts or data utilities in Python, this probably feels familiar.

When Pandas Starts to Hurt

As data grew, so did the problems:

  • 🚨 Data no longer fits nicely into memory
  • 🚨 Scripts become slower as data grows
  • 🚨 Running multiple jobs at the same time becomes painful
  • 🚨 “Which machine should this run on?” suddenly matters
  • 🚨 Sharing scripts with teammates isn’t straightforward

At some point, you realize:

Pandas is great — but it’s still single-machine thinking.

This is where Databricks entered my life.

Enter Databricks (Without the Hype)

I didn’t move to Databricks because I wanted to “do big data”.

I moved because I wanted:

  • To keep writing Python
  • To stop worrying about machine size
  • To process larger datasets safely
  • To schedule jobs without duct tape

Databricks is essentially:

  • A managed platform for Apache Spark
  • With notebooks
  • With scheduling
  • With collaboration built in

You write code, Databricks worries about the rest.

A Typical Flow (From My Actual Work)

Here’s what my day-to-day flow looks like now:

  1. 📚 Data already exists in the Databricks Catalog
  2. 📓 I create a Python notebook
  3. 🔄 Read data using Spark
  4. 🧠 Apply business logic
  5. 📤 Publish results to SQS
  6. ⏰ Schedule it to run every 3 hours

No servers to provision. No EC2 sizing debates.

Reading Data with Spark (Feels Familiar)

Instead of Pandas, I now use PySpark:

df = spark.read.table("catalog_name.schema_name.source_table")

Under the hood, Spark:

  • Splits data
  • Processes it in parallel
  • Handles datasets much larger than memory

Doing Business Logic (Still Just Python)

from pyspark.sql.functions import col

processed_df = (
    df
    .filter(col("status") == "ACTIVE")
    .withColumn("total_amount", col("price") * col("quantity"))
)

The rest

After processing, I can:

  • Write results back to tables
  • Send data to external systems (like SQS)
  • Schedule notebooks to run automatically

Why Spark Beats Pandas Here (Simply Put)

Pandas Spark (via Databricks)
Single-machine Distributed across many machines
Limited by RAM Scales with cluster size
Manual scheduling Built-in job scheduling
Great for small data Handles big data seamlessly

I:

  • Still write Python
  • Still think in business logic

Just stopped worrying about scale and infrastructure

Databricks + Spark let me grow without rewriting how I think.

If you’re comfortable with Python and Pandas, this transition is far less scary than it sounds.

December 25, 2025 · 3 min

Java: Record on Quarkus/ Newer Java

As I continue re-learning Java after my time away since Java 8, one of the most pleasant surprises has been the record keyword.

If you remember the pain of creating POJOs (Plain Old Java Objects) in the past—private fields, getters, setters, equals(), hashCode(), toString()—you know it was mostly noise. Lombok helped, but now, this capability is baked right into the language.

In this post, we’ll look at what Records are, why they make perfect Data Transfer Objects (DTOs), and how to use them in a Quarkus REST API.

What is a Record?

Introduced officially in Java 16, a Record is a special kind of class that acts as a transparent carrier for immutable data.

The Old Way (Java 8)

To create a simple object to hold a user’s data, we had to write this:

public class UserDTO {
    private final String name;
    private final String email;

    public UserDTO(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }

    // ... plus equals(), hashCode(), and toString()
}

The New Way (Java 16+)

With Records, the same can be achieved with much less boilerplate:

public record UserDTO(String name, String email) {}

The compiler automatically provides the constructor, getters (called accessors), and implementations for equals(), hashCode(), and toString().

That’s it. It is immutable by default, meaning once it’s created, the data cannot change. This makes it thread-safe and perfect for passing data around.

DTOs and Records: A Perfect Match

DTO (Data Transfer Object) is a design pattern. It’s an object used to transport data between processes or layers (e.g., from your API to your UI).

Why we use DTOs:

  • Decoupling: They separate the internal data representation from the external API.
  • Validation: They can enforce data constraints.
  • Clarity: They make it clear what data is being transferred.

Because DTOs are primarily about carrying data, Records are an excellent fit. They provide a concise way to define these data carriers without unnecessary boilerplate. So when you need a DTO, consider using a Record. Record in Java is created specifically for this purpose.

Using Records in a Quarkus REST API

Let’s see how to use Records in a Quarkus REST API.

Typical structure of the project:

src/main/java/com/example/
    ├── dto/
    │   └── UserDTO.java
    └── controllers/
        └── UserController.java
  1. Define the Record:
public record UserDTO(String name, String email) {}
  1. Create a REST Endpoint:
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import com.example.dto.UserDTO;

@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
    @POST
    public Response createUser(UserDTO user) {
        // Process the user data (e.g., save to database)
        System.out.println("Creating user: " + user.name() + " with email: " + user.email());
        return Response.status(Response.Status.CREATED).entity(user).build();
    }

    @GET
    @Path("/{id}")
    public Response getUser(@PathParam("id") Long id) {
        // Simulate fetching user from database
        UserDTO user = new UserDTO("John Doe", "john@example.com");
        return Response.ok(user).build();
    }
}

Sample Request and Response Payloads

Creating a User (POST Request)

Request:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice Johnson",
    "email": "alice@example.com"
  }'

Response:

{
  "name": "Alice Johnson",
  "email": "alice@example.com"
}

Retrieving a User (GET Request)

Request:

curl -X GET http://localhost:8080/users/1

Response:

{
  "name": "John Doe",
  "email": "john@example.com"
}

How Records Help with JSON Serialization

One of the biggest advantages of using Records as DTOs is how seamlessly they work with JSON serialization/deserialization:

  1. Automatic Field Mapping: The record components automatically map to JSON fields
  2. Clean JSON Output: No extra boilerplate fields or methods are serialized
  3. Type Safety: The compiler ensures all required fields are present
  4. Immutability: Once created, the data cannot be accidentally modified

Compare this to traditional POJOs where you might accidentally serialize internal state or have inconsistent field naming.

Adding Validation with Records

Records work excellently with Bean Validation (JSR-303). Let’s enhance our UserDTO with validation.

Before adding validation, our record looks like this:

public record UserDTO(String name, String email) {}

Now, we improve it by adding validation annotations:

import jakarta.validation.constraints.*;

public record UserDTO(
    @NotBlank(message = "Name cannot be blank")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    String name,

    @NotBlank(message = "Email cannot be blank")
    @Email(message = "Email should be valid")
    String email
) {}

PS. It’s the same record definition, but with validation annotations. So, this solves the problem of validating incoming data in a clean way.

Update your controller to handle validation:

import jakarta.validation.Valid;

@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {

    @POST
    public Response createUser(@Valid UserDTO user) {
        // Validation happens automatically before this method is called
        System.out.println("Creating user: " + user.name() + " with email: " + user.email());
        return Response.status(Response.Status.CREATED).entity(user).build();
    }

    // ... rest of the methods
}

The @Valid annotation ensures that the incoming UserDTO is validated according to the constraints defined in the record. If validation fails, Quarkus will automatically return a 400 Bad Request response with details about the violations.

Validation in Action

Invalid Request:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "",
    "email": "invalid-email"
  }'

Error Response:

{
  "title": "Constraint Violation",
  "status": 400,
  "violations": [
    {
      "field": "name",
      "message": "Name cannot be blank"
    },
    {
      "field": "email",
      "message": "Email should be valid"
    }
  ]
}

Advanced Record Features

Custom Methods in Records

While Records are primarily for data, you can add custom methods:

public record UserDTO(
    @NotBlank String name,
    @NotBlank @Email String email
) {
    // Custom method to get display name
    public String displayName() {
        return name.toUpperCase();
    }

    // Validation in constructor
    public UserDTO {
        if (name != null && name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
    }
}

Nested Records for Complex Data

public record Address(String street, String city, String zipCode) {}

public record UserProfileDTO(
    @NotBlank String name,
    @NotBlank @Email String email,
    @Valid Address address
) {}

Why Records are Perfect for Modern Java Development

  1. Less Boilerplate: Focus on business logic, not getter/setter noise
  2. Immutability by Default: Thread-safe and predictable
  3. Pattern Matching Ready: Works great with newer Java features like pattern matching (Java 17+)
  4. Validation Friendly: Integrates seamlessly with Bean Validation
  5. JSON Serialization: Works out-of-the-box with Jackson and other JSON libraries
  6. Performance: Often more memory-efficient than traditional classes

Conclusion

Records represent a significant step forward in Java’s evolution. They eliminate much of the boilerplate code that made Java verbose while providing type safety and immutability.

For REST APIs in Quarkus, Records make excellent DTOs. They’re concise, safe, and integrate well with validation and serialization frameworks. If you’re working with Java 16+ (and you should be!), start using Records for your data transfer objects.

Next time you need to create a DTO, skip the traditional class and reach for a Record. Your future self will thank you for the cleaner, more maintainable code.

December 14, 2025 · 6 min

AWS: EC2 vs Lambda

If you’ve read my previous post on AWS Lambda, you already know how magical it feels to just upload your code and let AWS handle the rest.

But here’s a question most beginners eventually ask:

“If Lambda can already run my code, why do people still use EC2?”

Good question. Let’s break it down in plain English.

EC2 — The Always-On Office PC

EC2 is like renting your own computer in the cloud.
You control everything — OS, software, uptime — but you also pay even when you’re not using it.

Perfect for:

  • Apps that need to run 24/7.
  • Consistent traffic or background services.
  • Workloads needing custom setups (special libraries, daemons, etc.).

⚠️ Downside: You manage scaling, patching, and cost.

Lambda — The Cloud Vending Machine

Lambda, on the other hand, is event-driven magic.
You just drop in your code, and it runs only when something triggers it.

If you want to understand Lambda in detail (handlers, events, roles, limits, etc.),
check out my earlier post AWS Lambda Basics.

Here, let’s keep things simple.

You pay only when it runs, it scales automatically, and it shuts down when done.
But you give up some control — you can’t run long-lived processes or manage your own environment.


Real-Life Example: SQS → DynamoDB Forwarder

Imagine you have a small data forwarder:
it listens to messages in SQS, processes them, and stores results in DynamoDB.

Let’s see what happens with both EC2 and Lambda

EC2 Version

You set up an EC2 instance, install your app, and keep it running 24/7, polling SQS every few seconds.

Pros

  • Full control and visibility.
  • Works great when messages keep coming in all day.
  • You can fine-tune performance (threads, caching, retries).

Cons

  • Still running (and billing) even when the queue is empty.
  • You manage scaling, logs, and health checks.

Billing vibe: Pay per hour. Idle? Still billed.

Lambda Version

You configure SQS as a trigger for your Lambda.
When a message arrives, AWS spins up your function, processes it, and shuts it down.

Pros

  • Pay only when messages arrive.
  • No servers, no scaling worries.
  • Handles bursty traffic automatically.

Cons

  • Time-limited execution (max 15 min).
  • Cold starts add slight delay.
  • Harder to debug long or stateful logic.

Billing vibe: No messages = no cost.


Which One Fits You?

Situation What You’d Pick
Constant message flow 🖥️ EC2 (or Fargate later)
Occasional bursts ⚡ Lambda
Need to install custom packages EC2
Want zero maintenance Lambda

Simple analogy:

  • EC2 = rent a car → you maintain it.
  • Lambda = GrabCar → you just ride when needed.

In real projects, both often coexist:
EC2 runs the main services, while Lambda handles small, event-based tasks.

Start simple — use Lambda for event-driven bits, and bring EC2 in when you need always-on power.
AWS gives you both tools so you can pick what fits the moment.

October 21, 2025 · 3 min

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: Decimals

Revisiting the basics — understanding floating-point numbers in Java.

In my ongoing Back to Java series, I’ve been rediscovering parts of the language that I used years ago but never really thought deeply about.
Today’s topic: double vs float — two simple types that can quietly cause big differences in performance and precision.


What Are float and double?

Both are floating-point data types used for storing decimal numbers.

The key differences are in their size, precision, and use cases.

Type Size Approx. Range Decimal Precision Default Literal Suffix
float 32-bit ±3.4 × 10³⁸ ~6–7 digits f or F
double 64-bit ±1.7 × 10³⁰⁸ ~15–16 digits none or d

So, double literally means double-precision floating point — twice the size and precision of float.


Double Is the Default for Decimals in Java

One thing that often surprises new Java developers:

In Java, all decimal literals are treated as double by default.

That’s why you can write this:

double pi = 3.14159; // ✅ works fine

But this won’t compile:

float price = 19.99; // ❌ error: possible lossy conversion from double to float

You have to explicitly mark it as a float:

float price = 19.99f; // ✅ correct

Java does this to favor precision over size — it assumes you want a double unless you tell it otherwise.

When to Use Which

✅ Use float when:

  • Memory is limited (e.g., large arrays, 3D graphics, sensor data).
  • You don’t need extreme precision (6–7 digits is enough).

✅ Use double when:

  • Precision matters (scientific calculations, analytics, or financial models).
  • You want the default — most math operations and decimal constants in Java use double.

💡 Rule of thumb:

  • Use double by default. Use float only when you know you need to save space.

Precision Pitfalls

Floating-point numbers can’t always represent decimals exactly.

double a = 0.1;
double b = 0.2;
System.out.println(a + b); // prints 0.30000000000000004

Why? Binary floating-point can’t represent 0.1 precisely — it’s a repeating fraction in base 2. This tiny difference often doesn’t matter, but it can accumulate in calculations.

A Quick Look at BigDecimal

When you need exact decimal precision, especially in financial or accounting systems, you should use BigDecimal — a class from the java.math package.

import java.math.BigDecimal;

BigDecimal price = new BigDecimal("19.99");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity);

System.out.println(total); // prints 59.97 exactly
  • BigDecimal stores numbers as unscaled integers + scale (not binary floating point).
  • It avoids rounding errors that can happen with float or double.
  • Downside: it’s slower and uses more memory, but you get perfect accuracy.

👉 That’s why it’s commonly used in banking, billing, and currency calculations.

Use Case Recommended Type
General math or analytics double
High-performance graphics / sensors float
Exact financial or monetary values BigDecimal
Integer-only math int or

For years, I used double by habit — it worked, so I never questioned it. But learning about precision again reminded me that choosing the right type is part of writing reliable code.

Sometimes you need speed (float), sometimes you need safety (BigDecimal), and most of the time, double is that balanced middle ground — it’s even the default for decimals in Java.

October 19, 2025 · 3 min

Testing: Load with Latency

When I was asked to test the performance of a new API endpoint, I quickly realized: I had never done load or latency testing before. I knew performance mattered, but I didn’t know how to measure it, what numbers to care about, or how to even start.

This post is my attempt to write down what I learned. I’ll explain the terms in plain English, the different types of tests, and show a simple example of how to run your first load test. If you’ve ever seen words like p95 latency or throughput and felt lost—this one is for you.

Key Terms Explained

  • Latency: simply how long a request takes. If a user clicks “Save Preference” and the response comes back in 180 ms, that’s the latency for that request.

Why not average?

Let’s say 9 requests are really fast (~100 ms), but 1 request is very slow (~900 ms).

  • Average latency looks like ~200 ms (which hides the slow request).
  • p50 (median) means “50% of requests are faster than this” (maybe ~120 ms).
  • p95 means “95% of requests are faster than this” (maybe ~200 ms).
  • p99 means “99% of requests are faster than this” (maybe ~900 ms).

Users notice the slow ones (the “tail”), so we measure p95 and p99 in addition to averages.

  • Throughput (RPS/QPS): Requests per second. How many requests your service can process in a given time.

  • Concurrency: How many requests are being processed at once.

  • Errors: Non-2xx responses (500, 502) or timeouts. Even a 1% error rate is a red flag in production.

Types of Performance Tests

  • Smoke Test – A tiny test (1 request per second for 1 min) just to check if the endpoint works.
  • Baseline Test – Light load to capture normal latency under “calm” conditions.
  • Load Test – Run the system at expected traffic (say 100 RPS) for 10–15 minutes. Does it still meet your latency/error targets?
  • Stress Test – Push past expected traffic until it breaks. This tells you where the limits are.
  • Spike Test – Jump suddenly from low → high traffic. Can the system autoscale?
  • Soak Test – Run for hours at moderate load. Useful to find memory leaks or slow drifts.

Each one answers a different question.

Strategy: How to Run Your First Test

  1. Define success first.
  • Example: “At 100 RPS, p95 ≤ 300 ms, error rate ≤ 0.1%.”
  1. Start small.
  • Run a smoke test: 1 VU (virtual user), 1 request per second.
  1. Ramp up gradually.
  • Increase RPS step by step until you reach your target.
  1. Measure carefully.
  • Look at p50/p95/p99 latency, errors, throughput.
  1. Observe your system.
  • Is CPU near 100%?
  • Are DB connections maxed?
  • Is an external service slow?
  1. Document the results.
  • Write down what you tested, the numbers, and what you learned.

What to Look For in Results

  • Good signs
  • p95 stable across the run
  • Errors < 0.1%
  • CPU and DB usage below ~70%
  • Warning signs
  • p95/p99 climbing while p50 stays flat → system under strain
  • Errors/timeouts creeping in
  • DB or external services throttling

Wrap-Up

When I started, terms like p95 and throughput felt intimidating. But once I ran my first smoke test, it clicked: latency is just “how long it takes,” and load testing is just “seeing if it still works when many requests come in.”

The important part is to:

  • Learn the basic terms (p95, RPS, errors).
  • Run small tests first.
  • Build up to realistic load.
  • Watch how your system behaves, not just the test numbers.

If you’ve never done load testing before, I encourage you to try a 5-minute k6 script on your own API. It’s eye-opening to see how your service behaves under pressure.

October 4, 2025 · 3 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