Networking & HTTP

Microservices spend most wall-clock time on the network—TCP connections, TLS handshakes, HTTP retries, and backpressure. Java gives you blocking sockets for learning and custom protocols, NIO channels for scalable servers, and java.net.http.HttpClient (Java 11+) for production HTTP and WebSocket clients. This chapter walks each layer and steers you away from legacy APIs that still compile but hurt operability.

mid senior

Choosing a networking API

Match transport to requirements: reliable ordered bytes (TCP), fire-and-forget datagrams (UDP), or application-layer HTTP when interoperability matters more than raw latency.

APITransportTypical use
ServerSocket / SocketTCPCustom protocols, learning, legacy services
DatagramSocketUDPDNS-like queries, gaming, metrics, multicast
SocketChannel + SelectorTCP (NIO)Many concurrent connections, proxies
HttpClientTCP + HTTP/1.1 or HTTP/2REST, downloads, webhooks, WebSocket client

For REST microservices, default to HttpClient (or framework clients built on it). Drop to sockets when you own the wire format end-to-end.

Socket programming

Sockets are the BSD-style endpoint abstraction: IP address + port, byte streams (TCP) or datagrams (UDP). All higher protocols—including HTTP—eventually sit on these primitives.

TCP — ServerSocket and Socket

TCP provides reliable, ordered, connection-oriented delivery. The server binds a port with ServerSocket, calls accept() to obtain a connected Socket per client, then reads/writes via input/output streams. The client creates a Socket(host, port) and uses the same stream API.

Each accepted socket is usually handled on its own thread in simple servers—or registered with a Selector for NIO. Always set read timeouts (socket.setSoTimeout) and close sockets in try-with-resources. InetAddress resolves hostnames to IPs—cache carefully in hot paths; prefer explicit connection timeouts over infinite blocking on misconfigured firewalls.

Java
// Server — echo one line per connection
try (var server = new ServerSocket(9000)) {
    server.setSoTimeout(30_000);
    while (true) {
        try (Socket client = server.accept();
             var in = new BufferedReader(new InputStreamReader(
                     client.getInputStream(), StandardCharsets.UTF_8));
             var out = new PrintWriter(client.getOutputStream(), true, StandardCharsets.UTF_8)) {
            String line = in.readLine();
            if (line != null) out.println("echo: " + line);
        }
    }
}

// Client
try (var socket = new Socket("localhost", 9000);
     var out = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8);
     var in = new BufferedReader(new InputStreamReader(
             socket.getInputStream(), StandardCharsets.UTF_8))) {
    out.println("hello");
    System.out.println(in.readLine());
}

SO_REUSEADDR, backlog queue sizing, and TLS (SSLSocket) matter in production servers—frameworks like Netty and embedded Tomcat wrap these details.

UDP — DatagramSocket and DatagramPacket

UDP sends self-contained datagrams without connection setup—no guarantee of delivery, order, or duplicate suppression. Lower latency and simpler fan-out; you implement reliability in the app layer if needed.

DatagramSocket sends/receives DatagramPacket objects wrapping byte arrays and SocketAddress endpoints.

Java
// UDP server
byte[] buf = new byte[512];
try (var socket = new DatagramSocket(9001)) {
    var packet = new DatagramPacket(buf, buf.length);
    socket.receive(packet);
    String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
    byte[] reply = ("ack: " + msg).getBytes(StandardCharsets.UTF_8);
    socket.send(new DatagramPacket(reply, reply.length, packet.getSocketAddress()));
}

// UDP client
try (var socket = new DatagramSocket()) {
    byte[] data = "ping".getBytes(StandardCharsets.UTF_8);
    socket.send(new DatagramPacket(data, data.length,
        InetAddress.getByName("localhost"), 9001));
}

Non-blocking TCP with NIO SocketChannel

SocketChannel.open() and ServerSocketChannel.open() integrate with Selector— configure configureBlocking(false), register for OP_ACCEPT / OP_READ, and multiplex many connections on few threads. Same model as the echo server in I/O: Selector & NIO server.

Java
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(9002));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);

// On accept:
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
// read/write via ByteBuffer on client.read(buf)
📦 Real World

With virtual threads, blocking Socket code scales further without selectors—still cap connection counts and use timeouts. Selectors remain relevant for custom gateways and Netty-style event loops.

HTTP Client (Java 11+)

java.net.http.HttpClient replaces Apache HttpClient and HttpURLConnection for new code: HTTP/2, async API, WebSocket, and immutable request/response types. Create one shared instance per application (connection pooling inside).

HttpClient — builder, executor, redirects, authenticator

HttpClient.newBuilder() configures defaults for all requests: connect timeout, redirect policy, proxy, SSL context, HTTP version preference, and the Executor used by sendAsync.

Builder methodEffect
connectTimeoutMax wait to establish TCP connection
followRedirectsALWAYS, NEVER, or NORMAL (no https→http downgrade)
versionHTTP_2 preferred with HTTP/1.1 fallback
proxyProxySelector for corporate egress
authenticatorSupply credentials when server returns 401
executorThread pool for async callbacks; default is executor service
cookieHandlerOptional CookieManager for session cookies
Java
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .version(HttpClient.Version.HTTP_2)
    .executor(Executors.newFixedThreadPool(8))
    .authenticator(new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            if (getRequestorType() == RequestorType.SERVER) {
                return new PasswordAuthentication("user", "secret".toCharArray());
            }
            return null;
        }
    })
    .build();

HttpRequest — GET, POST, headers, body

HttpRequest.newBuilder(URI) builds immutable requests. Set method (default GET), headers, timeout, and body publisher for POST/PUT/PATCH. BodyPublishers.ofString, ofByteArray, ofFile, and ofInputStream supply bodies; set Content-Type explicitly.

Java
HttpRequest get = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Accept", "application/json")
    .GET()
    .build();

String json = "{\"name\":\"Ada\"}";
HttpRequest post = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .timeout(Duration.ofSeconds(30))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse — BodyHandlers

client.send(request, responseBodyHandler) pairs the response with a typed body. Common handlers: BodyHandlers.ofString(), ofByteArray(), ofFile(path), ofInputStream() (stream must be consumed or closed), ofLines() for line-delimited text, discarding() when you only care about status headers.

Java
HttpResponse<String> response = client.send(get, HttpResponse.BodyHandlers.ofString());

int status = response.statusCode();
HttpHeaders headers = response.headers();
String body = response.body();

// Stream large download — consumer must read and close
HttpResponse<InputStream> fileResp = client.send(get,
    HttpResponse.BodyHandlers.ofInputStream());
try (InputStream in = fileResp.body()) {
    in.transferTo(Files.newOutputStream(Path.of("out.bin")));
}

Check statusCode() and handle 4xx/5xx—HttpClient does not throw on HTTP error status by default (unlike some REST clients).

Custom types use HttpResponse.BodyHandlers.mapping(handler, mapper)—e.g. map JSON string to a record with Jackson in the mapper after ofString().

Java
HttpRequest upload = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/upload"))
    .header("Content-Type", "application/octet-stream")
    .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("report.pdf")))
    .build();

Synchronous vs asynchronous — sendAsync and CompletableFuture

send blocks the calling thread until the response is complete—fine for CLI tools and request-per-thread servers. sendAsync returns CompletableFuture<HttpResponse<T>>—compose non-blocking pipelines, fan out parallel requests, or bridge to reactive stacks. Uses the client’s executor; avoid blocking inside thenApply on a small pool.

Java
CompletableFuture<HttpResponse<String>> future =
    client.sendAsync(get, HttpResponse.BodyHandlers.ofString());

HttpResponse<String> response = future.join();  // or get with timeout

// Parallel fetches
List<CompletableFuture<HttpResponse<String>>> futures = ids.stream()
    .map(id -> client.sendAsync(
        HttpRequest.newBuilder(URI.create(base + "/items/" + id)).build(),
        HttpResponse.BodyHandlers.ofString()))
    .toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();

See Concurrency: CompletableFuture for composition, exception handling, and executor choice.

WebSocket support

The same HttpClient exposes newWebSocketBuilder() for RFC 6455 handshakes. Provide a WebSocket.Listener for open, text/binary messages, ping/pong, error, and close. Use wss:// in production; handle backpressure—sendText returns a completion stage.

Java
WebSocket.Listener listener = new WebSocket.Listener() {
    @Override
    public void onOpen(WebSocket webSocket) {
        webSocket.sendText("subscribe:metrics", true);
        WebSocket.Listener.super.onOpen(webSocket);
    }
    @Override
    public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
        process(data.toString());
        return WebSocket.Listener.super.onText(webSocket, data, last);
    }
};

WebSocket ws = client.newWebSocketBuilder()
    .header("Authorization", "Bearer " + token)
    .buildAsync(URI.create("wss://stream.example.com/v1"), listener)
    .join();
💡 Pro Tip

Reuse one HttpClient instance—construction is relatively expensive and pooling is per client. For Spring Boot 3+, RestClient and WebClient wrap similar ideas; know the JDK API underneath.

URL & URI

Addresses and resource identifiers appear throughout HTTP configuration, redirects, and classpath loading—use the right type and avoid the legacy connection API.

URI vs URL

URI (java.net.URI) is the general identifier: scheme, authority, path, query, fragment—parsing and resolution without requiring network access. Immutable and strict about encoding; preferred for HttpRequest and configuration strings.

URL (java.net.URL) is a legacy combination of identifier and legacy connection machinery—can open streams via handlers. URLs are tied to stream handler protocols registered on the JVM; parsing is lenient compared to URI. Convert with uri.toURL() only when an older API requires URL; prefer URI at boundaries.

ConcernURIURL
ImmutabilityImmutableMutable (hashCode cache quirk)
HttpClientNativeConvert first
EqualityStructure-basedOpens connection semantics historically
Java
URI base = URI.create("https://api.example.com/v1/");
URI item = base.resolve("users/42");           // https://api.example.com/v1/users/42
// Encode query values — never concatenate raw user input
String q = URLEncoder.encode("java networking", StandardCharsets.UTF_8);
URI withQuery = URI.create("https://api.example.com/search?q=" + q + "&page=2");

// Prefer URI for HttpClient
HttpRequest req = HttpRequest.newBuilder(item).GET().build();

URLConnection — legacy, why to avoid

URL.openConnection() returns HttpURLConnection (or HTTPS variant)—blocking, mutable, easy to misuse: must call setRequestMethod, handle redirects manually, read error streams on 4xx, and tune disconnect flags. No HTTP/2, awkward timeout API, and inconsistent behavior across JDK versions compared to HttpClient.

Third-party stacks (Apache HttpClient 5, OkHttp) remain valid; for JDK-only dependencies, standardize on HttpClient since Java 11.

Java
// Legacy — avoid for new code
URL url = URI.create("https://api.example.com/data").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(10_000);
int code = conn.getResponseCode();
// read conn.getInputStream() or getErrorStream()

// Modern replacement
HttpResponse<String> resp = HttpClient.newHttpClient()
    .send(HttpRequest.newBuilder(URI.create("https://api.example.com/data")).build(),
        HttpResponse.BodyHandlers.ofString());
⚠️ Pitfall

Building query strings with string concatenation breaks encoding—use URI builders, libraries, or URLEncoder.encode for form bodies vs path segments (different rules).

🎯 Interview Tip

Contrast TCP vs UDP. Sketch client-server flow with ServerSocket.accept. Name why HttpClient supersedes HttpURLConnection and how sendAsync fits CompletableFuture composition.