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
Threads and the concurrency mental model
Creating and starting threads
RunnableandCallableThread lifecycle and interruption
Shared state and race conditions
synchronizedand thread safetyLocks and atomic variables
volatileand visibilityConcurrent collections
Executors and thread pools
Futureand asynchronous resultsCompletableFutureVirtual threads
Deadlocks and concurrency design
Networking & APIs
Networking fundamentals
HTTP fundamentals
Java HTTP Client
REST APIs
JSON serialization and deserialization
Calling external APIs
Timeouts, retries and API failures
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 completedor:
Sending email
Payment processing
Order completedStep-by-step explanation
paymentThreadrepresents one independent task.emailThreadrepresents another independent task.start()tells Java to begin executing each thread.The JVM decides when each thread gets CPU time.
Therefore, either task may print first.
join()tells the main thread to wait until that thread finishes.Because the main thread waits for both,
Order completedcomes 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 runningor:
Main is running
Worker is runningStep-by-step explanation
Workeris a class that extendsThread.Its
run()method contains the work performed by that thread.worker.start()asks Java to executerun()on a separate thread.Meanwhile, the main thread continues.
Therefore, either print statement may happen first.
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
Runnablefor work that does not return a value.Uses
Callablefor work that produces a result.Executes both tasks through an executor.
Retrieves the
Callableresult.
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: 500Step-by-step explanation
Runnablerepresents work that does not return a result.Callable<Integer>represents work that returns anInteger.executor.submit(sendEmail)sends the email task to the thread pool.executor.submit(calculatePrice)sends the calculation to the pool.The returned
Future<Integer>represents the eventual result.get()waits until the result is available.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
Runnablefor work without a result andCallablewhen 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 finishedStep-by-step explanation
The worker starts.
It prints
Processing order.Thread.sleep(5000)makes it wait.The main thread calls
interrupt().Because the worker is sleeping, Java wakes it by throwing
InterruptedException.The
catchblock printsProcessing cancelled.The worker finishes.
join()allows the main thread to continue.Main finishedis 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:
184732Step-by-step explanation
Both threads access the same
counter.counter++looks like one operation to us.Conceptually, it involves reading the current value, adding one, and writing the new value.
Two threads can interfere with these steps.
For example, both threads might read
100before either writes.Both calculate
101.Both write
101.One increment has effectively been lost.
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
200000Step-by-step explanation
Both threads share the same
Counter.Both call
increment().increment()is markedsynchronized.Java allows only one thread at a time to execute that synchronized method for this object.
Therefore, the read-modify-write operation cannot be interfered with by the other thread.
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
synchronizedprotects 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
200000Step-by-step explanation
AtomicIntegerstores an integer that supports thread-safe atomic operations.incrementAndGet()increments and returns the new value as one atomic operation.Both threads can safely use it.
No increment is lost.
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
volatileflag.Demonstrates visibility between threads.
Stops a worker when another thread changes the flag.
Shows a situation where
volatileis 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 finishedStep-by-step explanation
runninginitially containstrue.The worker repeatedly checks it.
The main thread changes it to
false.Because the variable is
volatile, the worker can observe the updated value.The loop ends.
The worker prints
Worker stopped.The main thread waits with
join().Main finishedprints 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
volatileis 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
2Step-by-step explanation
ConcurrentHashMapis designed for concurrent access.Both threads update the same keys.
merge()performs the update safely.Each thread adds one to
books.Therefore
booksbecomes2.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 4But their order is not guaranteed.
For example:
Task 2
Task 1
Task 3
Task 4Step-by-step explanation
The executor has two worker threads.
Four tasks are submitted.
At most two tasks can execute simultaneously in this pool.
Once a worker finishes one task, it can execute another.
Therefore all four tasks eventually execute.
The order of execution is not guaranteed.
shutdown()prevents new tasks from being submitted.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: 42Step-by-step explanation
The calculation is submitted to another thread.
futurerepresents its future result.The main thread prints
Doing other work.The worker waits for 500 milliseconds.
future.get()waits for the result if it isn't ready.The result becomes
42.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
Futurerepresents 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
550Step-by-step explanation
supplyAsync()starts work asynchronously.The first operation produces
500.thenApply()receives that result.It adds
50.The resulting value becomes
550.join()waits for the completed result.550is 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
CompletableFuturelets 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 finishedor:
Second request finished
First request finished
All requests finishedStep-by-step explanation
Thread.startVirtualThread()starts a virtual thread.Virtual threads are lightweight threads provided by modern Java.
They are especially useful when applications have many tasks that spend significant time waiting, such as network requests.
Both virtual threads sleep independently.
Both eventually finish.
join()waits for them.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 lockand then it may remain stuck.
Step-by-step explanation
Thread one acquires
accountLock.Thread two acquires
paymentLock.Thread one tries to acquire
paymentLock.But thread two already owns it.
Thread two tries to acquire
accountLock.But thread one already owns it.
Each thread waits for the other.
Neither can continue.
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/42Step-by-step explanation
A URI such as:
https://api.example.com:8443/users/42can be mentally broken down as:
https → scheme/protocol
api.example.com → host
8443 → port
/users/42 → pathgetScheme()returnshttps.getHost()returns the server name.getPort()returns8443.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/jsonStep-by-step explanation
HttpRequest.newBuilder()begins constructing an HTTP request..uri(...)specifies the target..header(...)adds an HTTP header..GET()specifies the HTTP method..build()creates the finished request.No network request has actually been sent yet.
The program simply examines the request object.
Important HTTP methods
| Method | Typical purpose |
|---|---|
| GET | Retrieve data |
| POST | Create/submit data |
| PUT | Replace/update a resource |
| PATCH | Partially update a resource |
| DELETE | Delete 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 serverStep-by-step explanation
HttpClientis Java's built-in HTTP client.HttpRequestdescribes what we want to send.client.send()actually sends the request.BodyHandlers.ofString()tells Java to represent the response body as aString.statusCode()returns200.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
HttpClientsends the request;HttpResponsecontains 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/42Step-by-step explanation
Think about /users as a collection and /users/42 as one particular resource.
GET /users/42asks for user 42.POST /userssubmits data to create a new user.PUT /users/42replaces the user representation.PATCH /users/42partially modifies the user.DELETE /users/42removes 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
JSON is a text format commonly used to exchange structured data.
The API sends:
{"name":"Alice","age":30}A Java application needs to convert that text into Java objects/data structures.
That process is called deserialization.
Converting a Java object back into JSON is called serialization.
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
The client creates an HTTP request.
The URI identifies the external API endpoint.
Accept: application/jsontells the server that JSON is preferred.send()sends the request.The server responds with status
200.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 failedStep-by-step explanation
Network operations can take an unpredictable amount of time.
A timeout prevents your application from waiting indefinitely.
The request has a one-second timeout.
If the server does not respond in time, Java throws an exception.
The
catchblock handles the failure.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 helpYou 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: AliceStep-by-step explanation
This example represents the kind of workflow you will repeatedly encounter in backend development.
HttpClientrepresents the application's HTTP client.HttpRequestdescribes what we want to retrieve.The URI identifies
/users/42.The
Acceptheader indicates that JSON is expected.sendAsync()sends the request without blocking the calling thread while waiting for the network response.A
CompletableFuturerepresents the eventual HTTP response.thenApply()receives the response when it arrives.The status code is checked.
If the status is not
200, an exception is created.If successful, the response body is passed to the next stage.
The JSON would normally be deserialized by a JSON library such as Jackson.
The resulting application object is represented here as:
new User(42, "Alice")
thenAccept()receives thatUser.It prints:
User: Aliceexceptionally()provides a failure path.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 objectThey 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 WorkflowsThe 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 userAnd concurrency determines how efficiently your application handles many such operations at the same time.
The critical distinctions to remember
| Concept | Think of it as |
|---|---|
| Thread | A path of execution |
| Runnable | Work with no returned result |
| Callable | Work that returns a result |
start() | Start separate thread execution |
join() | Wait for a thread |
| Race condition | Threads interfere with shared state |
synchronized | One thread at a time in protected code |
| Atomic | Individual operation performed safely |
volatile | Visibility between threads |
| Concurrent collection | Collection designed for concurrent access |
| Executor | Manages worker threads |
| Future | Result available later |
| CompletableFuture | Chain asynchronous operations |
| Virtual thread | Lightweight Java thread |
| Deadlock | Threads permanently waiting on each other |
| URI | Identifies a network resource |
| HTTP | Communication protocol |
| REST | Resource-oriented API style |
| JSON | Common data-exchange format |
| Timeout | Maximum time you're willing to wait |
| Retry | Attempt an operation again |
| API client | Code 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.