Why a Microservice Call Is Different
In a traditional monolith, calling another module is an in-memory method invocation:paymentService.charge(order). The CPU jumps to a memory address, executes instructions in nanoseconds, and returns.
“In microservices, there are no in-memory guarantees. When Service A calls Service B, your request passes through the operating system network stack and network infrastructure.”
To build reliable backend services, a microservice call cannot be treated like an ordinary in-memory function call. Understanding what the operating system, network stack, and infrastructure do helps in writing resilient systems.
// Service A (Order Service) Calling Service B (Payment Service)
package com.example.orderservice.client;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@Service
public class PaymentClient {
private final RestClient restClient;
public PaymentClient(RestClient.Builder builder) {
// "http://payment-service" is resolved by Kubernetes DNS or Eureka!
this.restClient = builder
.baseUrl("http://payment-service")
.build();
}
// Circuit breaker stops requests if Payment Service fails repeatedly
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public PaymentResponse processPayment(PaymentRequest request) {
// Behind this single line, 8 distinct network & OS steps happen:
return restClient.post()
.uri("/api/v1/payments")
.header("X-Request-Source", "order-service")
.body(request) // Jackson serializes Java object -> JSON bytes
.retrieve()
.body(PaymentResponse.class);
}
public PaymentResponse fallbackPayment(PaymentRequest request, Throwable ex) {
// Graceful degradation when network or target service is down
return new PaymentResponse("FAILED", "Payment service unavailable, retrying in background");
}
}The 8-Step Request Flow
│
├── Step 1: Service Discovery & DNS Resolution (Find IP of 'payment-service')
├── Step 2: Client-Side Load Balancing (Pick Pod 2 over Pod 1 & 3)
├── Step 3: Distributed Tracing Injection (Add TraceId, SpanId headers)
├── Step 4: Object Serialization (Java DTO ──▶ JSON String ──▶ Raw Bytes)
├── Step 5: OS Kernel, TCP 3-Way Handshake & TLS (SYN ──▶ SYN-ACK ──▶ ACK)
│
▼ (Ethernet / VPC Network Transit)
│
[Payment Service (Pod 2)]
├── Step 6: Socket Listen & Tomcat Thread Allocation (Accept byte stream)
├── Step 7: JSON Deserialization (Bytes ──▶ JSON ──▶ PaymentRequest DTO)
└── Step 8: Business Logic & HTTP Response Trip (200 OK sent back)
Here is how each step works in detail:
Detailed Walkthrough of Each Step
Step 1: Service Discovery & DNS Resolution
In your code, you specify http://payment-service. Network interfaces do not route by hostnames; they require IP addresses.
10.96.0.45).• In Spring Cloud or Consul: The client retrieves a list of healthy registered instances from service discovery.
Step 2: Client-Side Load Balancing
If multiple instances of payment-service are running, the client needs to pick one. Depending on the setup, client libraries (like Spring Cloud LoadBalancer) or a service mesh proxy (like Envoy) select a target instance using algorithms such as Round Robin or Least Active Requests.
Step 3: Distributed Tracing Context Injection
To trace requests across distributed services, libraries like Micrometer Tracing or OpenTelemetry inject tracing headers into the HTTP request before transmission:
// HTTP Request Headers sent over the wire:
POST /api/v1/payments HTTP/1.1
Host: payment-service:8080
Content-Type: application/json
Content-Length: 128
User-Agent: Spring-RestClient/6.1
# Distributed Tracing Headers (W3C TraceContext):
# traceparent: version(00) - trace_id(4bf92f3577b34da6a3ce929d0e0e4736) - parent_span_id(00f067aa0ba902b7) - sampled(01)
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
X-B3-TraceId: 4bf92f3577b34da6a3ce929d0e0e4736
X-B3-SpanId: 00f067aa0ba902b7
{
"orderId": "ORD-9842",
"customerId": 10452,
"amount": 249.99
}The traceparent header ensures that logs across OrderService, PaymentService, and downstream services share the same Trace ID in monitoring platforms like Zipkin or Grafana.
Step 4: Object Serialization (Java DTO → JSON Bytes)
Your Java object PaymentRequest(orderId, amount) exists on the JVM heap. Jackson ObjectMapper converts this object into a UTF-8 encoded JSON byte stream (byte[]) ready for transmission over a network socket.
Step 5: OS Kernel, TCP Handshake & TLS (mTLS)
The runtime passes the bytes to the operating system kernel. If an existing pooled HTTP connection is not available:
Step 6 & 7: Target Service Processing (Tomcat Thread + Deserialization)
On the target host, the Payment Service's web server (e.g. Apache Tomcat):
http-nio-8080-exec-12).PaymentRequest object.Step 8: The Return Trip
The response (PaymentResponse) undergoes the reverse process: Jackson serializes it to JSON, passes it back through the TCP socket across the network, and the calling thread in OrderService unblocks to receive the result.
What Happens When the Call Fails?
In a monolith, code rarely fails due to network transport. In microservices, network and service failures must be accounted for:
1. Network Socket Timeout
If PaymentService is paused during garbage collection or overloaded, OrderService's thread will wait until its connect or read timeout expires. Without configured timeouts, threads remain blocked, potentially causing cascading failures across upstream services.
2. Target Service Crash (HTTP 503 / Connection Refused)
If the target instance crashed or is terminating, the connection attempt is rejected. Without a Circuit Breaker (Resilience4j), the caller continues sending requests to an unhealthy service instead of failing fast.
3. Partial Success & Duplicate Requests
PaymentService might process the transaction successfully, but the network connection drops before the response reaches OrderService. If OrderService retries the call without an Idempotency Key, the operation may execute twice. This is why idempotency keys are standard in financial and transactional endpoints.
The 5 Production Rules for Microservice Calls
Summary
A microservice call is different from an in-process function call. It is a distributed operation involving DNS resolution, load balancers, TCP sockets, TLS encryption, and serialization. Understanding these layers helps you diagnose latency issues, identify cascading failures, and build more reliable backend services.
Did you find this breakdown helpful?
Share it with backend developers and engineers designing distributed systems!