The Illusion of Micro Services — What Actually Changes
The Expectation: Breaking the System Will Make It Simpler
When teams decide to move away from a monolith, the decision rarely comes from curiosity. It usually comes from pressure.
The system has already started showing signs of strain. Things are slowing down, deployments feel risky, and teams are stepping on each other. At that point, microservices begin to look like a clean solution.
The idea feels logical. If one large system is hard to manage, then breaking it into smaller systems should make things easier. Each part can evolve independently. Failures should stay isolated. Scaling should become more controlled.
On paper, everything looks cleaner, but what’s often missed is that microservices don’t actually reduce the complexity of the system, they simply move that complexity into different layers.

1st Shift: From In-Memory Calls to Network Communication
Inside a monolith, when one part of the system needs something from another, it’s just a method call. It happens instantly, within the same memory space, and almost never fails in unexpected ways.
When the system is split into services, that same interaction becomes a network call.
That change seems small, but it changes the nature of the system – A method call is predictable. A network call is not.
Now every interaction carries uncertainty. It might succeed, it might fail, it might be slow, or it might not return at all. Even when everything is working correctly, latency becomes part of the normal behavior.
So something that used to be guaranteed is now something you have to design around.
2nd Shift: Losing a Single Transaction Boundary
In a monolith, complex operations feel safe because they run inside a single transaction. If a flow involves multiple steps, they either all succeed or all fail together. That gives a strong sense of control over system behaviour.
Once you move to microservices, that safety net disappears. Each service manages its own data and commits its own changes. There is no shared transaction that connects them. So now, a flow can partially succeed.
An order might be created. A payment might fail. Inventory might already be reserved. Nothing is technically broken, but the system is now in a state that didn’t exist before.
This is where the thinking has to change. Instead of relying on rollback, you now have to design how the system recovers.
3rd Shift: Failure Is No Longer Simple
In a monolith, failure is easy to reason about. Either the request completes successfully, or it fails and rolls back.
In a micro services system, failure becomes layered. A request might succeed in one service, fail in another, and timeout in between. It might even be retried automatically, creating multiple attempts for the same operation.
This means the system can exist in intermediate states that were never visible before.
Handling these states is not optional. It becomes part of the system design. You are no longer just writing business logic. You are defining how the system behaves when things don’t go as expected.
4th Shift: Latency Becomes a System Property
In a monolith, latency is mostly tied to how long the code takes to execute and how fast the database responds.
In microservices, latency becomes something that flows through the entire system. A single request may pass through multiple services. Each one adds its own delay. Even if each step is fast, the total time increases. More importantly, latency becomes uneven. If one service slows down, everything behind it slows down. If one dependency is unstable, it affects the entire request flow.
The system starts behaving like a chain, where the weakest link(service) determines the overall performance.
5th Shift: Scaling Requires Coordination
One of the strongest arguments for microservices is independent scaling.
Athough it is true that individual services can scale separately. But real systems are not isolated pieces. They are connected.
If one service handles more traffic, it pushes more load onto the services it depends on. If those services are not prepared, they become bottlenecks. So scaling is no longer just about increasing capacity. It becomes about understanding how load moves through the system.
The system must scale as a whole, not just in parts.
6th Shift: Data Stops Being Centralized
In a monolith, data lives in one place. If you want to understand the system, you query the database and get a consistent answer.
In microservices, data is distributed. Each service owns its own data, and there is no single place where everything comes together in real time.
To understand the full state, you often need to combine information from multiple services. This introduces a new challenge. The system’s state is no longer immediately consistent everywhere. It is eventually consistent, and sometimes temporarily incomplete.
This makes reporting, debugging, and auditing more complex.
7th Shift: Debugging Becomes a System Problem
In a monolith, debugging is local. You look at logs, follow the execution path, and find the issue.
In microservices, a single request moves across multiple services. If something goes wrong, the information is spread across different systems. You have to trace the request across boundaries, correlate logs, and reconstruct what happened. Without proper tracing and observability, this becomes very difficult.
At this point, debugging is no longer about code. It is about understanding the system as a whole.
From Production Perspective
In real production environments, the move to microservices rarely feels like an immediate improvement.
- Systems often become harder to manage at first.
- Latency increases because of network communication.
- Failures become less predictable.
- Issues take longer to diagnose because they span multiple services.
The system is not worse, but it is more complex. Stability comes back only after introducing things that were not needed before—clear retry strategies, proper timeouts, circuit breakers, and strong observability.
Microservices don’t simplify the system, they require a higher level of discipline to manage it.
Summary
Moving to microservices changes how a system behaves at every level. What used to be simple and predictable becomes distributed and uncertain.
You move from:
- direct calls to network communication
- single transactions to distributed workflows
- clear failures to partial outcomes
- centralized data to distributed state
The system becomes more flexible, but also harder to reason about.
Understanding this shift is critical.
Because microservices are not just an architectural change—they are a change in how you think about building and operating systems.
The Network Call That Has No Timeout by Default
The most consequential difference between a method call and a network call is not latency — it is failure mode. A method call either returns or throws. A network call can hang indefinitely unless a timeout is explicitly set. Java 11+ HttpClient has no default timeout. Without one, a thread blocks until the OS closes the connection, which can take minutes.
This code shows the minimum-safe pattern for synchronous service-to-service calls. The connect timeout and request timeout are separate concerns — both must be set.
// Java 11+ -- synchronous service call with explicit connect and request timeouts
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InventoryClient {
private static final Logger log = LoggerFactory.getLogger(InventoryClient.class);
// connectTimeout: how long to wait for TCP handshake.
// Per-request timeout: how long to wait for the full response.
// Both must be set — neither has a default.
private final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
public String fetchStock(String productId) throws Exception {
long start = System.nanoTime();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://inventory-service/api/stock/" + productId))
.timeout(Duration.ofSeconds(5)) // request-level timeout
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
long ms = (System.nanoTime() - start) / 1_000_000;
log.info("inventory productId={} status={} latencyMs={}", productId, response.statusCode(), ms);
if (response.statusCode() != 200) {
log.warn("inventory-service non-200 productId={} status={}", productId, response.statusCode());
throw new RuntimeException("inventory unavailable: " + response.statusCode());
}
return response.body();
}
}Code language: Java (java)
In a monolith, the equivalent is a method call that either returns synchronously or throws a runtime exception. The timeout concern does not exist. In a microservices architecture, every inter-service call introduces this failure mode, and the thread that called it blocks for the duration of the wait.
Interview Questions
What happens if a synchronous HTTP call between two microservices has no timeout configured and the downstream service stops responding?
Root Cause: Java HttpClient has no default timeout. A call with no .timeout() set on the request will block the calling thread indefinitely until the OS-level TCP keepalive mechanism closes the connection — typically after several minutes. Internal Behaviour: The thread is in BLOCKED state, holding no locks, consuming a slot in the executor pool. Each new request that calls the same downstream service occupies another thread. Production Impact: The thread pool exhausts silently. The upstream service stops processing requests without throwing exceptions. Monitoring shows request rate dropping to zero with no error spike. Fix: Always set both connectTimeout on the HttpClient builder and .timeout() on each HttpRequest. Handle HttpTimeoutException explicitly and decide whether to retry, fall back, or propagate.
How does the thread blocking model change when a monolith method call is replaced with a synchronous HTTP call in a microservices system?
Root Cause: A monolith method call executes on the calling thread and returns immediately — no I/O wait, no network round-trip. A synchronous HTTP call blocks the calling thread for the full duration of network transit, remote service processing, and response transmission. Internal Behaviour: Under high concurrency, the number of threads blocked on remote calls equals the number of concurrent in-flight requests. If the downstream service slows by 200ms, every thread in the pool waits an extra 200ms per request, reducing throughput proportionally. Production Impact: At 100 concurrent requests with a 5-second timeout, the upstream service needs 100 threads available. A monolith serving the same volume through in-memory calls needs far fewer, because threads are released immediately after local execution. Fix: Use virtual threads (Java 21+) for I/O-bound service calls to avoid exhausting the platform thread pool, or switch to reactive/non-blocking HTTP clients where the architecture permits.
The Partial Failure Mode That Does Not Exist in a Monolith
In a monolith, a failure is total or it is not a failure. A method either returns or throws. If it throws, the transaction rolls back. The system does not end up in a state where some parts of a request completed and others did not, because everything runs inside the same transaction boundary and the same JVM. The rollback guarantee means partial state is not observable externally.
In a microservices architecture, this guarantee disappears. A request that calls three downstream services can succeed in two and fail in one. The two that succeeded have already committed their state — there is no distributed rollback to undo them. The calling service must decide what to do with a partial result, and that decision is a design choice that does not exist in a monolith.
The code below shows how a service assembles a response from multiple downstream calls and must explicitly handle the case where some succeed and others do not, rather than relying on a rollback.
// Java 11+ -- assembling a response from multiple services with explicit partial failure handling
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProductPageAssembler {
private static final Logger log = LoggerFactory.getLogger(ProductPageAssembler.class);
private final CatalogClient catalog;
private final InventoryClient inventory;
private final ReviewClient reviews;
public ProductPageAssembler(CatalogClient catalog, InventoryClient inventory, ReviewClient reviews) {
this.catalog = catalog;
this.inventory = inventory;
this.reviews = reviews;
}
// Partial failure is a first-class outcome here, not an exception.
// The page renders with whatever data is available -- degraded but not broken.
// A monolith does not need this logic because one failure rolls back everything.
public ProductPageResponse assemble(String productId) {
String details = fetchOrDefault(() -> catalog.getDetails(productId),
"details unavailable", "catalog", productId);
String stock = fetchOrDefault(() -> inventory.getStock(productId),
"stock unknown", "inventory", productId);
String rating = fetchOrDefault(() -> reviews.getRating(productId),
"no rating", "reviews", productId);
return new ProductPageResponse(details, stock, rating);
}
private String fetchOrDefault(SupplierWithException<String> call, String fallback,
String service, String productId) {
try {
return call.get();
} catch (Exception e) {
log.warn("service={} unavailable for productId={} -- using fallback", service, productId);
return fallback;
}
}
@FunctionalInterface
interface SupplierWithException<T> { T get() throws Exception; }
record ProductPageResponse(String details, String stock, String rating) {}
}Code language: Java (java)
This pattern — returning a degraded response rather than failing entirely — is called graceful degradation. It is a design decision that is forced on you the moment you distribute a previously monolithic operation across multiple services. In a monolith, the equivalent is handled by the database transaction. In a microservices system, it is handled by explicit fallback logic in application code. This is one of the complexity transfers described at the start of this article: the complexity did not go away, it moved from the platform into the code.



