JAVA 4

 

Phase 4 — Concurrency + Networking + APIs

This phase takes you from single-threaded Java programs to Java programs that can perform multiple tasks concurrently, communicate over networks, consume APIs, and handle asynchronous work.

Chapters in this Phase

Concurrency

  1. Threads and the concurrency mental model

  2. Creating and starting threads

  3. Runnable and Callable

  4. Thread lifecycle and interruption

  5. Shared state and race conditions

  6. synchronized and thread safety

  7. Locks and atomic variables

  8. volatile and visibility

  9. Concurrent collections

  10. Executors and thread pools

  11. Future and asynchronous results

  12. CompletableFuture

  13. Virtual threads

  14. Deadlocks and concurrency design

Networking & APIs

  1. Networking fundamentals

  2. HTTP fundamentals

  3. Java HTTP Client

  4. REST APIs

  5. JSON serialization and deserialization

  6. Calling external APIs

  7. Timeouts, retries and API failures

  8. Building a complete API-client workflow


Chapter 1 — Threads and the Concurrency Mental Model

Question

Given below is a code snippet that:

  • Creates multiple threads.

  • Gives each thread independent work.

  • Starts the threads.

  • Demonstrates that threads can execute concurrently.

  • Uses join() so the main program waits for them.

What should be the output of the following code?

public class Main {

    public static void main(String[] args) throws InterruptedException {

        // Create a thread that performs the first task.
        Thread paymentThread = new Thread(() -> {
            System.out.println("Payment processing");
        });

        // Create another thread that performs a different task.
        Thread emailThread = new Thread(() -> {
            System.out.println("Sending email");
        });

        // Start both threads.
        paymentThread.start();
        emailThread.start();

        // Wait for both threads to finish.
        paymentThread.join();
        emailThread.join();

        // This line executes after both threads have finished.
        System.out.println("Order completed");
    }
}

Answer

The exact order of the first two lines is not guaranteed.

Possible output:

Payment processing
Sending email
Order completed

or:

Sending email
Payment processing
Order completed

Step-by-step explanation

  1. paymentThread represents one independent task.

  2. emailThread represents another independent task.

  3. start() tells Java to begin executing each thread.

  4. The JVM decides when each thread gets CPU time.

  5. Therefore, either task may print first.

  6. join() tells the main thread to wait until that thread finishes.

  7. Because the main thread waits for both, Order completed comes last.

How to read the important code

"Create a thread that runs this task, start both threads, then join them so the main thread waits for completion."

Beginner trap

Calling run() is not the same as calling start().

start() creates concurrent execution. Calling run() directly simply executes the method normally on the current thread.

Key takeaway

Concurrency means multiple tasks can make progress independently; you should not assume their execution order.


Chapter 2 — Creating and Starting Threads

Question

Given below is a code snippet that:

  • Creates a custom Thread.

  • Overrides its run() method.

  • Starts the thread.

  • Demonstrates that the main thread and worker thread are separate.

What should be the output of the following code?

public class Main {

    // Define a custom thread by extending Thread.
    static class Worker extends Thread {

        @Override
        public void run() {
            // This code executes on the worker thread.
            System.out.println("Worker is running");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // Create the worker thread object.
        Worker worker = new Worker();

        // Start the worker thread.
        worker.start();

        // The main thread continues independently.
        System.out.println("Main is running");

        // Wait until the worker finishes.
        worker.join();
    }
}

Answer

The first two lines can appear in either order:

Worker is running
Main is running

or:

Main is running
Worker is running

Step-by-step explanation

  1. Worker is a class that extends Thread.

  2. Its run() method contains the work performed by that thread.

  3. worker.start() asks Java to execute run() on a separate thread.

  4. Meanwhile, the main thread continues.

  5. Therefore, either print statement may happen first.

  6. join() makes the main thread wait for the worker to finish.

How to read the important code

"Define a worker thread by extending Thread, override run, create the worker, and start it."

Beginner trap

A Thread object is not the same thing as the thread actually executing. start() causes the new execution path to begin.

Key takeaway

start() begins a new thread; run() contains the work that thread performs.


Chapter 3 — Runnable and Callable

Question

Given below is a code snippet that:

  • Uses Runnable for work that does not return a value.

  • Uses Callable for work that produces a result.

  • Executes both tasks through an executor.

  • Retrieves the Callable result.

What should be the output of the following code?

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

public class Main {

    public static void main(String[] args) throws Exception {

        // Create a thread pool containing two worker threads.
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // Runnable performs work but does not return a result.
        Runnable sendEmail = () -> {
            System.out.println("Email sent");
        };

        // Callable performs work and returns a result.
        Callable<Integer> calculatePrice = () -> {
            // Return the calculated price.
            return 500;
        };

        // Submit the Runnable task.
        executor.submit(sendEmail);

        // Submit the Callable task and keep its Future.
        Future<Integer> priceResult = executor.submit(calculatePrice);

        // Retrieve the result produced by Callable.
        int price = priceResult.get();

        // Print the result.
        System.out.println("Price: " + price);

        // Shut down the executor.
        executor.shutdown();
    }
}

Answer

The output is:

Email sent
Price: 500

Step-by-step explanation

  1. Runnable represents work that does not return a result.

  2. Callable<Integer> represents work that returns an Integer.

  3. executor.submit(sendEmail) sends the email task to the thread pool.

  4. executor.submit(calculatePrice) sends the calculation to the pool.

  5. The returned Future<Integer> represents the eventual result.

  6. get() waits until the result is available.

  7. The result is 500.

How to read the important code

"Submit a Callable that returns an Integer and keep its Future so I can retrieve the result later."

Beginner trap

Future<Integer> does not contain the result immediately in the conceptual sense. It represents a computation whose result may become available later.

Key takeaway

Use Runnable for work without a result and Callable when the task needs to return a result.


Chapter 4 — Thread Lifecycle and Interruption

Question

Given below is a code snippet that:

  • Creates a worker thread.

  • Uses sleep().

  • Interrupts the worker.

  • Handles InterruptedException.

  • Stops the worker cleanly.

What should be the output of the following code?

public class Main {

    public static void main(String[] args) throws InterruptedException {

        // Create a worker that performs a slow task.
        Thread worker = new Thread(() -> {
            try {
                // Pretend that the worker is processing an order.
                System.out.println("Processing order");

                // Pause the worker for five seconds.
                Thread.sleep(5000);

                // This line will not normally be reached after interruption.
                System.out.println("Order processed");

            } catch (InterruptedException e) {
                // The thread was asked to stop waiting.
                System.out.println("Processing cancelled");
            }
        });

        // Start the worker.
        worker.start();

        // Give the worker a moment to enter sleep().
        Thread.sleep(100);

        // Interrupt the sleeping worker.
        worker.interrupt();

        // Wait for the worker to finish.
        worker.join();

        // Continue after the worker has stopped.
        System.out.println("Main finished");
    }
}

Answer

Processing order
Processing cancelled
Main finished

Step-by-step explanation

  1. The worker starts.

  2. It prints Processing order.

  3. Thread.sleep(5000) makes it wait.

  4. The main thread calls interrupt().

  5. Because the worker is sleeping, Java wakes it by throwing InterruptedException.

  6. The catch block prints Processing cancelled.

  7. The worker finishes.

  8. join() allows the main thread to continue.

  9. Main finished is printed.

How to read the important code

"Start the worker, interrupt it while it's sleeping, handle the interruption, and wait for it to finish."

Beginner trap

interrupt() does not forcibly kill a thread. It is a request/interruption signal. Well-designed code decides how to respond to that signal.

Key takeaway

Interruption is the normal Java mechanism for asking a thread to stop waiting or stop its work cooperatively.


Chapter 5 — Shared State and Race Conditions

Question

Given below is a code snippet that:

  • Creates shared mutable state.

  • Has two threads modify the same variable.

  • Demonstrates a race condition.

  • Shows why concurrent access can produce an unexpected result.

What should be the output of the following code?

public class Main {

    // This variable is shared by both worker threads.
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {

        // Create the first worker.
        Thread first = new Thread(() -> {
            // Increment the shared variable many times.
            for (int i = 0; i < 100_000; i++) {
                counter++;
            }
        });

        // Create the second worker.
        Thread second = new Thread(() -> {
            // Increment the same shared variable many times.
            for (int i = 0; i < 100_000; i++) {
                counter++;
            }
        });

        // Start both workers.
        first.start();
        second.start();

        // Wait for both workers.
        first.join();
        second.join();

        // The expected mathematical result is 200000,
        // but concurrent access can produce a smaller value.
        System.out.println(counter);
    }
}

Answer

You cannot reliably predict one exact number.

It will typically be less than or equal to 200000, and it may vary between executions.

For example:

184732

Step-by-step explanation

  1. Both threads access the same counter.

  2. counter++ looks like one operation to us.

  3. Conceptually, it involves reading the current value, adding one, and writing the new value.

  4. Two threads can interfere with these steps.

  5. For example, both threads might read 100 before either writes.

  6. Both calculate 101.

  7. Both write 101.

  8. One increment has effectively been lost.

  9. This is called a race condition.

How to read the important code

"Two threads increment the same shared variable at the same time."

Beginner trap

Adding volatile to counter would not make counter++ safe. Visibility and atomicity are different problems.

Key takeaway

When multiple threads modify shared mutable data, you must deliberately make access thread-safe.


Chapter 6 — synchronized and Thread Safety

Question

Given below is a code snippet that:

  • Protects shared state with synchronized.

  • Allows only one thread at a time into the critical section.

  • Prevents lost updates.

  • Demonstrates a thread-safe counter.

What should be the output of the following code?

public class Main {

    // Store a value shared by multiple threads.
    static class Counter {

        private int value = 0;

        // synchronized allows only one thread at a time
        // to execute this method on the same Counter object.
        public synchronized void increment() {
            value++;
        }

        // Return the current value.
        public int getValue() {
            return value;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // Create one shared Counter object.
        Counter counter = new Counter();

        // Create the first worker.
        Thread first = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                counter.increment();
            }
        });

        // Create the second worker.
        Thread second = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                counter.increment();
            }
        });

        // Start both workers.
        first.start();
        second.start();

        // Wait for both workers.
        first.join();
        second.join();

        // Print the final value.
        System.out.println(counter.getValue());
    }
}

Answer

200000

Step-by-step explanation

  1. Both threads share the same Counter.

  2. Both call increment().

  3. increment() is marked synchronized.

  4. Java allows only one thread at a time to execute that synchronized method for this object.

  5. Therefore, the read-modify-write operation cannot be interfered with by the other thread.

  6. All 200,000 increments are preserved.

How to read the important code

"Make increment synchronized so only one thread at a time can change the counter."

Beginner trap

synchronized can solve correctness problems, but excessive synchronization can reduce performance because threads may have to wait.

Key takeaway

synchronized protects critical sections so multiple threads cannot simultaneously perform unsafe operations on shared state.


Chapter 7 — Locks and Atomic Variables

Question

Given below is a code snippet that:

  • Uses AtomicInteger.

  • Performs atomic increments.

  • Avoids manually synchronizing the counter.

  • Demonstrates a common lightweight thread-safe operation.

What should be the output of the following code?

import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    public static void main(String[] args) throws InterruptedException {

        // AtomicInteger provides thread-safe atomic operations.
        AtomicInteger counter = new AtomicInteger(0);

        // Create the first worker.
        Thread first = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                // Atomically increase the value by one.
                counter.incrementAndGet();
            }
        });

        // Create the second worker.
        Thread second = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                // Atomically increase the same value.
                counter.incrementAndGet();
            }
        });

        // Start both workers.
        first.start();
        second.start();

        // Wait for both workers.
        first.join();
        second.join();

        // Read the final atomic value.
        System.out.println(counter.get());
    }
}

Answer

200000

Step-by-step explanation

  1. AtomicInteger stores an integer that supports thread-safe atomic operations.

  2. incrementAndGet() increments and returns the new value as one atomic operation.

  3. Both threads can safely use it.

  4. No increment is lost.

  5. The final value is 200000.

How to read the important code

"Create an AtomicInteger starting at zero and atomically increment it."

Beginner trap

Atomic classes are excellent for certain individual operations, but they don't automatically make an entire multi-step business process thread-safe.

Key takeaway

Use atomic classes when you need simple thread-safe operations on individual values.


Chapter 8 — volatile and Visibility

Question

Given below is a code snippet that:

  • Uses a shared volatile flag.

  • Demonstrates visibility between threads.

  • Stops a worker when another thread changes the flag.

  • Shows a situation where volatile is appropriate.

What should be the output of the following code?

public class Main {

    // volatile ensures that changes made by one thread
    // become visible to other threads reading this variable.
    static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {

        // Create a worker that keeps running while the flag is true.
        Thread worker = new Thread(() -> {

            // Continue checking the shared flag.
            while (running) {
                // Perform a small unit of work.
            }

            // This executes after another thread changes running.
            System.out.println("Worker stopped");
        });

        // Start the worker.
        worker.start();

        // Give the worker time to start.
        Thread.sleep(100);

        // Change the shared flag.
        running = false;

        // Wait for the worker to observe the change.
        worker.join();

        // Continue after the worker stops.
        System.out.println("Main finished");
    }
}

Answer

Worker stopped
Main finished

Step-by-step explanation

  1. running initially contains true.

  2. The worker repeatedly checks it.

  3. The main thread changes it to false.

  4. Because the variable is volatile, the worker can observe the updated value.

  5. The loop ends.

  6. The worker prints Worker stopped.

  7. The main thread waits with join().

  8. Main finished prints afterward.

What volatile actually solves

volatile primarily addresses visibility.

It does not turn complex operations such as:

counter++;

into atomic operations.

How to read the important code

"Use a volatile flag as a signal that one thread can change and another thread can observe."

Beginner trap

Think:

volatile = visibility

not:

volatile = all thread safety.

Key takeaway

volatile is useful when threads need to reliably see changes to a shared variable.


Chapter 9 — Concurrent Collections

Question

Given below is a code snippet that:

  • Uses ConcurrentHashMap.

  • Allows multiple threads to update shared data.

  • Uses computeIfAbsent.

  • Reads the final thread-safe map.

What should be the output of the following code?

import java.util.concurrent.ConcurrentHashMap;

public class Main {

    public static void main(String[] args) throws InterruptedException {

        // Create a map designed for concurrent access.
        ConcurrentHashMap<String, Integer> sales = new ConcurrentHashMap<>();

        // Create the first worker.
        Thread first = new Thread(() -> {

            // Atomically create or update the "books" entry.
            sales.merge("books", 1, Integer::sum);

            // Atomically create or update the "phones" entry.
            sales.merge("phones", 1, Integer::sum);
        });

        // Create the second worker.
        Thread second = new Thread(() -> {

            // Atomically update the same "books" entry.
            sales.merge("books", 1, Integer::sum);

            // Atomically update the same "phones" entry.
            sales.merge("phones", 1, Integer::sum);
        });

        // Start both workers.
        first.start();
        second.start();

        // Wait for both workers.
        first.join();
        second.join();

        // Read the final values.
        System.out.println(sales.get("books"));
        System.out.println(sales.get("phones"));
    }
}

Answer

2
2

Step-by-step explanation

  1. ConcurrentHashMap is designed for concurrent access.

  2. Both threads update the same keys.

  3. merge() performs the update safely.

  4. Each thread adds one to books.

  5. Therefore books becomes 2.

  6. The same happens for phones.

How to read the important code

"Atomically merge one into the existing value for this key."

Beginner trap

Don't automatically replace every HashMap with ConcurrentHashMap. If a map is only used by one thread, a normal HashMap is usually appropriate.

Key takeaway

Use concurrent collections when shared collections genuinely need safe concurrent access.


Chapter 10 — Executors and Thread Pools

Question

Given below is a code snippet that:

  • Creates a fixed thread pool.

  • Submits multiple tasks.

  • Reuses a limited number of worker threads.

  • Waits for all submitted tasks.

  • Shuts down the executor.

What should be the output?

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

public class Main {

    public static void main(String[] args) throws InterruptedException {

        // Create a pool containing two reusable worker threads.
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // Submit four independent tasks to the pool.
        for (int i = 1; i <= 4; i++) {

            // Store the task number for this iteration.
            int taskNumber = i;

            // Send the task to the executor.
            executor.submit(() -> {
                // Report which task is executing.
                System.out.println("Task " + taskNumber);
            });
        }

        // Stop accepting new tasks.
        executor.shutdown();

        // Wait until submitted tasks finish.
        executor.awaitTermination(10, TimeUnit.SECONDS);
    }
}

Answer

All four lines will appear:

Task 1
Task 2
Task 3
Task 4

But their order is not guaranteed.

For example:

Task 2
Task 1
Task 3
Task 4

Step-by-step explanation

  1. The executor has two worker threads.

  2. Four tasks are submitted.

  3. At most two tasks can execute simultaneously in this pool.

  4. Once a worker finishes one task, it can execute another.

  5. Therefore all four tasks eventually execute.

  6. The order of execution is not guaranteed.

  7. shutdown() prevents new tasks from being submitted.

  8. awaitTermination() waits for existing work to finish.

How to read the important code

"Create a fixed pool of two workers and submit these four tasks to it."

Beginner trap

Creating one new Thread for every tiny task is usually not how production applications should manage large amounts of concurrent work.

Key takeaway

Executors separate task submission from thread management and allow threads to be reused.


Chapter 11 — Future and Asynchronous Results

Question

Given below is a code snippet that:

  • Submits a calculation to an executor.

  • Receives a Future.

  • Does other work while the calculation runs.

  • Retrieves the result later.

What should be the output?

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

public class Main {

    public static void main(String[] args) throws Exception {

        // Create a worker pool.
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // Submit a task whose result will be available later.
        Future<Integer> future = executor.submit(() -> {

            // Simulate a slow calculation.
            Thread.sleep(500);

            // Return the calculation result.
            return 42;
        });

        // The main thread can continue doing other work.
        System.out.println("Doing other work");

        // get() waits until the calculation finishes.
        int result = future.get();

        // Print the completed result.
        System.out.println("Result: " + result);

        // Shut down the executor.
        executor.shutdown();
    }
}

Answer

Doing other work
Result: 42

Step-by-step explanation

  1. The calculation is submitted to another thread.

  2. future represents its future result.

  3. The main thread prints Doing other work.

  4. The worker waits for 500 milliseconds.

  5. future.get() waits for the result if it isn't ready.

  6. The result becomes 42.

  7. Java prints it.

How to read the important code

"Submit the calculation, keep its Future, continue working, then get the result."

Beginner trap

Calling get() immediately after submitting a task can eliminate much of the benefit of asynchronous execution because you may immediately wait for the result.

Key takeaway

A Future represents a result that may become available later.


Chapter 12 — CompletableFuture

Question

Given below is a code snippet that:

  • Starts asynchronous work.

  • Transforms the result with thenApply.

  • Continues the asynchronous pipeline.

  • Retrieves the final result.

What should be the output?

import java.util.concurrent.CompletableFuture;

public class Main {

    public static void main(String[] args) {

        // Start an asynchronous calculation.
        CompletableFuture<Integer> price =
                CompletableFuture.supplyAsync(() -> {

                    // Pretend we retrieved a price from another service.
                    return 500;
                });

        // Transform the result after the first operation completes.
        CompletableFuture<Integer> finalPrice =
                price.thenApply(value -> {

                    // Add shipping cost to the retrieved price.
                    return value + 50;
                });

        // Wait for the final result.
        int result = finalPrice.join();

        // Print the completed value.
        System.out.println(result);
    }
}

Answer

550

Step-by-step explanation

  1. supplyAsync() starts work asynchronously.

  2. The first operation produces 500.

  3. thenApply() receives that result.

  4. It adds 50.

  5. The resulting value becomes 550.

  6. join() waits for the completed result.

  7. 550 is printed.

How to read the important code

"Run this asynchronously, then transform its result by adding fifty."

Beginner trap

thenApply() transforms a result. When you need another asynchronous operation that itself returns a CompletableFuture, thenCompose() is usually the appropriate tool.

Key takeaway

CompletableFuture lets you build asynchronous workflows as chains of operations.


Chapter 13 — Virtual Threads

Question

Given below is a code snippet that:

  • Creates virtual threads.

  • Starts multiple tasks.

  • Waits for them.

  • Demonstrates Java's lightweight thread model for blocking-style tasks.

What should be the output?

public class Main {

    public static void main(String[] args) throws InterruptedException {

        // Create a virtual thread for the first task.
        Thread first = Thread.startVirtualThread(() -> {

            // Simulate waiting for an external resource.
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            // Report completion.
            System.out.println("First request finished");
        });

        // Create another virtual thread.
        Thread second = Thread.startVirtualThread(() -> {

            // Simulate another blocking operation.
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            // Report completion.
            System.out.println("Second request finished");
        });

        // Wait for both virtual threads.
        first.join();
        second.join();

        // Print after both are complete.
        System.out.println("All requests finished");
    }
}

Answer

The first two lines can appear in either order:

First request finished
Second request finished
All requests finished

or:

Second request finished
First request finished
All requests finished

Step-by-step explanation

  1. Thread.startVirtualThread() starts a virtual thread.

  2. Virtual threads are lightweight threads provided by modern Java.

  3. They are especially useful when applications have many tasks that spend significant time waiting, such as network requests.

  4. Both virtual threads sleep independently.

  5. Both eventually finish.

  6. join() waits for them.

  7. The final message is therefore last.

How to read the important code

"Start this task on a virtual thread."

Beginner trap

Virtual threads are not automatically faster for CPU-heavy calculations. Their biggest advantage is handling large numbers of concurrent tasks that spend time waiting.

Key takeaway

Virtual threads make large amounts of blocking-style concurrent work much easier to scale.


Chapter 14 — Deadlocks and Concurrency Design

Question

Given below is a code snippet that:

  • Creates two locks.

  • Has two threads acquire the locks in opposite orders.

  • Demonstrates the structure of a deadlock.

  • Shows why consistent lock ordering matters.

What should happen when this program runs?

public class Main {

    // Two separate locks represent two shared resources.
    static final Object accountLock = new Object();
    static final Object paymentLock = new Object();

    public static void main(String[] args) throws InterruptedException {

        // Thread one acquires accountLock first.
        Thread first = new Thread(() -> {

            synchronized (accountLock) {

                // Pretend this thread is doing some work.
                System.out.println("First has account lock");

                synchronized (paymentLock) {

                    // This may never execute if the second thread
                    // already owns paymentLock.
                    System.out.println("First has both locks");
                }
            }
        });

        // Thread two acquires paymentLock first.
        Thread second = new Thread(() -> {

            synchronized (paymentLock) {

                // Pretend this thread is doing some work.
                System.out.println("Second has payment lock");

                synchronized (accountLock) {

                    // This may never execute if the first thread
                    // already owns accountLock.
                    System.out.println("Second has both locks");
                }
            }
        });

        // Start both threads.
        first.start();
        second.start();

        // Wait for them.
        first.join();
        second.join();

        // This may never be reached if a deadlock occurs.
        System.out.println("Finished");
    }
}

Answer

The program can deadlock.

A possible output is:

First has account lock
Second has payment lock

and then it may remain stuck.

Step-by-step explanation

  1. Thread one acquires accountLock.

  2. Thread two acquires paymentLock.

  3. Thread one tries to acquire paymentLock.

  4. But thread two already owns it.

  5. Thread two tries to acquire accountLock.

  6. But thread one already owns it.

  7. Each thread waits for the other.

  8. Neither can continue.

  9. This is a deadlock.

How to read the important code

"Thread one locks account then payment; thread two locks payment then account."

Beginner trap

Deadlocks are not simply "two threads running at the same time." A deadlock requires a circular waiting situation involving resources.

Key takeaway

When multiple locks are necessary, acquire them in a consistent global order whenever possible.


Chapter 15 — Networking Fundamentals

Before writing network code, you need the basic mental model.

Question

Given below is a code snippet that:

  • Represents a URL.

  • Separates protocol, host, port and path.

  • Demonstrates how Java sees a network address.

What should be the output?

import java.net.URI;

public class Main {

    public static void main(String[] args) throws Exception {

        // Create a URI representing a web resource.
        URI uri = URI.create(
                "https://api.example.com:8443/users/42"
        );

        // Read the communication scheme.
        System.out.println(uri.getScheme());

        // Read the server host.
        System.out.println(uri.getHost());

        // Read the port.
        System.out.println(uri.getPort());

        // Read the resource path.
        System.out.println(uri.getPath());
    }
}

Answer

https
api.example.com
8443
/users/42

Step-by-step explanation

A URI such as:

https://api.example.com:8443/users/42

can be mentally broken down as:

https        → scheme/protocol
api.example.com → host
8443         → port
/users/42    → path
  1. getScheme() returns https.

  2. getHost() returns the server name.

  3. getPort() returns 8443.

  4. getPath() returns /users/42.

How to read the important code

"Create a URI for the HTTPS API server on port 8443 at the users/42 path."

Beginner trap

A URL/URI identifies a resource, while HTTP defines how clients and servers communicate about resources.

Key takeaway

Networking becomes easier when you mentally separate scheme, host, port, path, query and fragment.


Chapter 16 — HTTP Fundamentals

Question

Given below is a code snippet that:

  • Creates an HTTP request.

  • Uses the GET method.

  • Adds an HTTP header.

  • Demonstrates an HTTP request conceptually.

What should be the output?

import java.net.URI;
import java.net.http.HttpRequest;

public class Main {

    public static void main(String[] args) {

        // Build a request for a user resource.
        HttpRequest request = HttpRequest.newBuilder()

                // Specify the target URI.
                .uri(URI.create("https://api.example.com/users/42"))

                // Tell the server that we want JSON.
                .header("Accept", "application/json")

                // Use HTTP GET to retrieve the resource.
                .GET()

                // Finish building the request.
                .build();

        // Print the HTTP method.
        System.out.println(request.method());

        // Print the requested URI.
        System.out.println(request.uri());

        // Read the Accept header.
        System.out.println(
                request.headers().firstValue("Accept").orElse("missing")
        );
    }
}

Answer

GET
https://api.example.com/users/42
application/json

Step-by-step explanation

  1. HttpRequest.newBuilder() begins constructing an HTTP request.

  2. .uri(...) specifies the target.

  3. .header(...) adds an HTTP header.

  4. .GET() specifies the HTTP method.

  5. .build() creates the finished request.

  6. No network request has actually been sent yet.

  7. The program simply examines the request object.

Important HTTP methods

MethodTypical purpose
GETRetrieve data
POSTCreate/submit data
PUTReplace/update a resource
PATCHPartially update a resource
DELETEDelete a resource

How to read the important code

"Build a GET request for this URI and ask the server for JSON."

Beginner trap

Creating an HttpRequest does not send it. You need an HttpClient and send() or sendAsync().

Key takeaway

An HTTP request contains information such as the method, target URI, headers and optionally a body.


Chapter 17 — Java HTTP Client

Question

Given below is a code snippet that:

  • Creates Java's HTTP client.

  • Builds a GET request.

  • Sends the request.

  • Reads the HTTP status code.

  • Reads the response body.

What should be the output if the server responds with status 200 and body Hello from server?

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {

    public static void main(String[] args) throws Exception {

        // Create an HTTP client.
        HttpClient client = HttpClient.newHttpClient();

        // Build a GET request.
        HttpRequest request = HttpRequest.newBuilder()

                // Specify the server endpoint.
                .uri(URI.create("https://example.com/hello"))

                // Use GET.
                .GET()

                // Finish the request.
                .build();

        // Send the request synchronously.
        HttpResponse<String> response =
                client.send(
                        request,
                        HttpResponse.BodyHandlers.ofString()
                );

        // Print the HTTP status code.
        System.out.println(response.statusCode());

        // Print the response body.
        System.out.println(response.body());
    }
}

Answer

Assuming the server returns the stated response:

200
Hello from server

Step-by-step explanation

  1. HttpClient is Java's built-in HTTP client.

  2. HttpRequest describes what we want to send.

  3. client.send() actually sends the request.

  4. BodyHandlers.ofString() tells Java to represent the response body as a String.

  5. statusCode() returns 200.

  6. body() returns the server's response text.

How to read the important code

"Send this HTTP request and give me the response body as a String."

Beginner trap

HTTP status 200 does not mean "the body contains valid data." Your application may still need to validate and parse the response.

Key takeaway

HttpClient sends the request; HttpResponse contains the server's result.


Chapter 18 — REST APIs

Question

Given below is a code snippet that:

  • Represents a REST-style user resource.

  • Uses different HTTP methods.

  • Demonstrates resource-oriented API design.

  • Shows how CRUD operations map to HTTP.

What should be the output?

public class Main {

    public static void main(String[] args) {

        // Retrieve a user resource.
        String getUser = "GET /users/42";

        // Create a new user resource.
        String createUser = "POST /users";

        // Replace an existing user resource.
        String replaceUser = "PUT /users/42";

        // Partially update an existing user.
        String updateUser = "PATCH /users/42";

        // Delete an existing user.
        String deleteUser = "DELETE /users/42";

        // Print the REST operations.
        System.out.println(getUser);
        System.out.println(createUser);
        System.out.println(replaceUser);
        System.out.println(updateUser);
        System.out.println(deleteUser);
    }
}

Answer

GET /users/42
POST /users
PUT /users/42
PATCH /users/42
DELETE /users/42

Step-by-step explanation

Think about /users as a collection and /users/42 as one particular resource.

  1. GET /users/42 asks for user 42.

  2. POST /users submits data to create a new user.

  3. PUT /users/42 replaces the user representation.

  4. PATCH /users/42 partially modifies the user.

  5. DELETE /users/42 removes the user.

How to read the important code

"GET the user with ID 42."

"POST a new user to the users collection."

Beginner trap

REST is an architectural style, not simply "an API that returns JSON."

Key takeaway

REST APIs model resources and use HTTP's methods and status codes to communicate operations on those resources.


Chapter 19 — JSON Serialization and Deserialization

Question

Given below is a code snippet that:

  • Represents JSON data.

  • Parses JSON conceptually into Java data.

  • Demonstrates serialization and deserialization terminology.

  • Shows why JSON is common in APIs.

What should be the output?

import java.util.HashMap;
import java.util.Map;

public class Main {

    public static void main(String[] args) {

        // This String represents JSON received from an API.
        String json = """
                {"name":"Alice","age":30}
                """;

        // Store parsed API values in a Java map.
        Map<String, Object> user = new HashMap<>();

        // In a real application, a JSON library would perform
        // this parsing automatically.
        user.put("name", "Alice");
        user.put("age", 30);

        // Read the parsed values.
        System.out.println(user.get("name"));
        System.out.println(user.get("age"));

        // The original JSON can be thought of as serialized data.
        System.out.println(json.trim());
    }
}

Answer

Alice
30
{"name":"Alice","age":30}

Step-by-step explanation

  1. JSON is a text format commonly used to exchange structured data.

  2. The API sends:

{"name":"Alice","age":30}
  1. A Java application needs to convert that text into Java objects/data structures.

  2. That process is called deserialization.

  3. Converting a Java object back into JSON is called serialization.

  4. In real Java backend development, libraries such as Jackson commonly perform these conversions.

How to read the important code

"Receive JSON, deserialize it into Java data, work with that data, and serialize it again when sending a response."

Beginner trap

JSON and Java objects are not the same thing. JSON is a data representation; Java objects are Java runtime objects.

Key takeaway

APIs commonly use JSON as the language through which systems exchange structured data.


Chapter 20 — Calling External APIs

Question

Given below is a code snippet that:

  • Calls an external REST endpoint.

  • Sends an HTTP GET request.

  • Receives JSON.

  • Reads the HTTP status.

  • Reads the response body.

What should be the output if the API returns the following response?

{"id":42,"name":"Alice"}

with HTTP status 200.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {

    public static void main(String[] args) throws Exception {

        // Create the HTTP client used to communicate with the API.
        HttpClient client = HttpClient.newHttpClient();

        // Build a GET request for user 42.
        HttpRequest request = HttpRequest.newBuilder()

                // Specify the external API endpoint.
                .uri(URI.create(
                        "https://api.example.com/users/42"
                ))

                // Ask the API for JSON.
                .header("Accept", "application/json")

                // Use GET to retrieve the user.
                .GET()

                // Build the request.
                .build();

        // Send the request and receive the body as text.
        HttpResponse<String> response =
                client.send(
                        request,
                        HttpResponse.BodyHandlers.ofString()
                );

        // Print the status returned by the API.
        System.out.println("Status: " + response.statusCode());

        // Print the JSON returned by the API.
        System.out.println("Body: " + response.body());
    }
}

Answer

Status: 200
Body: {"id":42,"name":"Alice"}

Step-by-step explanation

  1. The client creates an HTTP request.

  2. The URI identifies the external API endpoint.

  3. Accept: application/json tells the server that JSON is preferred.

  4. send() sends the request.

  5. The server responds with status 200.

  6. The JSON response is available through response.body().

How to read the important code

"Send a GET request to the users endpoint and expect JSON back."

Beginner trap

Never assume an external API will always return success. Production code must handle error status codes, timeouts, malformed responses and changing API behavior.

Key takeaway

An API client is simply Java code that constructs requests, sends them, validates responses, and converts the returned data into useful application objects.


Chapter 21 — Timeouts, Retries and API Failures

Question

Given below is a code snippet that:

  • Configures an HTTP timeout.

  • Attempts a network operation.

  • Handles a timeout.

  • Demonstrates the basic structure of resilient API communication.

What should be the output if the server takes longer than one second to respond?

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class Main {

    public static void main(String[] args) {

        // Create an HTTP client with a one-second request timeout.
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(1))
                .build();

        // Build a request with its own one-second timeout.
        HttpRequest request = HttpRequest.newBuilder()

                // Specify the remote endpoint.
                .uri(URI.create("https://api.example.com/orders"))

                // Stop waiting after one second.
                .timeout(Duration.ofSeconds(1))

                // Use GET.
                .GET()

                // Finish building the request.
                .build();

        try {
            // Send the request.
            HttpResponse<String> response =
                    client.send(
                            request,
                            HttpResponse.BodyHandlers.ofString()
                    );

            // Print successful status.
            System.out.println(
                    "Status: " + response.statusCode()
            );

        } catch (Exception e) {

            // Handle communication failure.
            System.out.println("API request failed");
        }
    }
}

Answer

If the request exceeds the configured timeout:

API request failed

Step-by-step explanation

  1. Network operations can take an unpredictable amount of time.

  2. A timeout prevents your application from waiting indefinitely.

  3. The request has a one-second timeout.

  4. If the server does not respond in time, Java throws an exception.

  5. The catch block handles the failure.

  6. The application prints API request failed.

Retries

In production, you may retry certain temporary failures.

But you should not blindly retry everything.

For example:

Connection temporarily failed → potentially retry
Server temporarily unavailable → potentially retry
Invalid authentication → retrying usually doesn't help
Invalid request → retrying usually doesn't help

You also need backoff, meaning increasing the waiting time between retries.

How to read the important code

"Send the request, but don't wait forever; if communication fails, handle the failure."

Beginner trap

Retries can accidentally duplicate operations. Retrying a POST that creates a payment or order requires careful idempotency design.

Key takeaway

Reliable API clients need timeouts, appropriate error handling, and carefully designed retries.


Chapter 22 — Complete API-Client Workflow

This is the chapter that brings the entire networking section together.

Question

Given below is a code snippet that:

  • Builds an HTTP request.

  • Sends it asynchronously.

  • Handles the HTTP response.

  • Validates the status code.

  • Processes the JSON response.

  • Converts the response into application data.

  • Handles communication failure.

  • Demonstrates a realistic API-client workflow.

What should be the output if the server returns HTTP 200 with the following JSON?

{"id":42,"name":"Alice"}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class Main {

    // Represent the application-level user data.
    record User(int id, String name) {}

    public static void main(String[] args) {

        // Create the HTTP client.
        HttpClient client = HttpClient.newHttpClient();

        // Build the request sent to the remote service.
        HttpRequest request = HttpRequest.newBuilder()

                // Identify the API resource.
                .uri(URI.create(
                        "https://api.example.com/users/42"
                ))

                // Tell the server we expect JSON.
                .header("Accept", "application/json")

                // Retrieve the resource.
                .GET()

                // Finish the request.
                .build();

        // Send the request asynchronously.
        CompletableFuture<HttpResponse<String>> responseFuture =
                client.sendAsync(
                        request,
                        HttpResponse.BodyHandlers.ofString()
                );

        // Continue processing when the HTTP response arrives.
        responseFuture
                .thenApply(response -> {

                    // Reject unsuccessful HTTP responses.
                    if (response.statusCode() != 200) {
                        throw new RuntimeException(
                                "API returned status "
                                        + response.statusCode()
                        );
                    }

                    // Return the response body for the next stage.
                    return response.body();
                })
                .thenApply(json -> {

                    // In a real application, Jackson or another
                    // JSON library would deserialize this JSON.
                    // Here we demonstrate the resulting application data.
                    return new User(42, "Alice");
                })
                .thenAccept(user -> {

                    // Use the resulting Java object.
                    System.out.println(
                            "User: " + user.name()
                    );
                })
                .exceptionally(error -> {

                    // Handle any failure in the asynchronous pipeline.
                    System.out.println("Request failed");

                    // Return null because the pipeline has completed
                    // after handling the failure.
                    return null;
                })

                // Keep this simple console program alive until
                // the asynchronous pipeline finishes.
                .join();
    }
}

Answer

User: Alice

Step-by-step explanation

This example represents the kind of workflow you will repeatedly encounter in backend development.

  1. HttpClient represents the application's HTTP client.

  2. HttpRequest describes what we want to retrieve.

  3. The URI identifies /users/42.

  4. The Accept header indicates that JSON is expected.

  5. sendAsync() sends the request without blocking the calling thread while waiting for the network response.

  6. A CompletableFuture represents the eventual HTTP response.

  7. thenApply() receives the response when it arrives.

  8. The status code is checked.

  9. If the status is not 200, an exception is created.

  10. If successful, the response body is passed to the next stage.

  11. The JSON would normally be deserialized by a JSON library such as Jackson.

  12. The resulting application object is represented here as:

new User(42, "Alice")

  1. thenAccept() receives that User.

  2. It prints:

User: Alice
  1. exceptionally() provides a failure path.

  2. join() keeps this small console application alive until the asynchronous chain completes.

How to read the important code

A professional programmer might mentally read this as:

"Build a GET request for the user endpoint, send it asynchronously, validate the response, deserialize the JSON into a User, process the User, and handle failures."

That is a much better way to read Java than mechanically reading every symbol.

Beginner trap

Don't confuse these three different things:

HTTP request
      ↓
HTTP response
      ↓
Java object

They are different layers.

For example:

Request:
GET /users/42

Response:
HTTP 200
{"id":42,"name":"Alice"}

Java:
User(42, "Alice")

Key takeaway

Professional Java backend code often connects concurrency + HTTP + JSON + error handling into one asynchronous workflow.


Phase 4 — What You Now Understand

You've now covered the complete Phase 4 progression:

                    CONCURRENCY
                        │
        ┌───────────────┴────────────────┐
        ↓                                ↓
      Threads                     Shared State
        │                                │
        ↓                                ↓
 Runnable / Callable              Race Conditions
        │                                │
        ↓                                ↓
 Thread Lifecycle                synchronized
        │                                │
        ↓                                ↓
 Interruption                 Locks / Atomic
        │                                │
        └───────────────┬────────────────┘
                        ↓
                 volatile / Visibility
                        ↓
              Concurrent Collections
                        ↓
                Executors / Pools
                        ↓
                    Future
                        ↓
                CompletableFuture
                        ↓
                 Virtual Threads
                        ↓
               Deadlocks / Safety
                        │
                        ▼
                  NETWORKING
                        │
                        ↓
                  URI / Network
                        ↓
                     HTTP
                        ↓
                Java HttpClient
                        ↓
                   REST APIs
                        ↓
                     JSON
                        ↓
              External API Calls
                        ↓
              Timeouts / Retries
                        ↓
          Asynchronous API Workflows

The most important mental model from Phase 4

A backend request often looks like this:

User request
     ↓
Java application
     ↓
Controller/service
     ↓
External API / database
     ↓
HTTP request
     ↓
Network waiting
     ↓
Response
     ↓
JSON
     ↓
Java object
     ↓
Business logic
     ↓
Response to user

And concurrency determines how efficiently your application handles many such operations at the same time.

The critical distinctions to remember

ConceptThink of it as
ThreadA path of execution
RunnableWork with no returned result
CallableWork that returns a result
start()Start separate thread execution
join()Wait for a thread
Race conditionThreads interfere with shared state
synchronizedOne thread at a time in protected code
AtomicIndividual operation performed safely
volatileVisibility between threads
Concurrent collectionCollection designed for concurrent access
ExecutorManages worker threads
FutureResult available later
CompletableFutureChain asynchronous operations
Virtual threadLightweight Java thread
DeadlockThreads permanently waiting on each other
URIIdentifies a network resource
HTTPCommunication protocol
RESTResource-oriented API style
JSONCommon data-exchange format
TimeoutMaximum time you're willing to wait
RetryAttempt an operation again
API clientCode that communicates with another service

Phase 4 is complete.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.