Microservices I — Concepts, Gateway, IPC & Discovery
A monolith is simple until it isn't. This chapter builds the vocabulary for why large teams eventually break an application into independently deployable services — the Scale Cube, database-per-service, the API Gateway — and then tackles the two hardest new problems microservices create: how services talk to each other across a network instead of a function call (inter-process communication), and how they find each other when instances are constantly starting, stopping and moving (service discovery). One running example carries the whole chapter: an online shopping application with Product, Order, Customer, Shipping and Notification services.
Microservices is arguably the most interview-tested, most misunderstood architecture topic in the module. Almost every "drawback" here is a direct trade for a benefit somewhere else — nothing is free. If you can explain what you gain and what you pay for every decision on this page, you understand microservices properly.
From monolith to microservices
The monolith
A monolithic application is one that, however modular its internal code structure, is built, packaged and deployed as a single unit. Our shopping application — product catalog, shopping cart, checkout, order history, customer accounts, shipping tracking, email notifications — all lives in one codebase, compiles to one deployable artifact (a WAR file on Tomcat, a single ASP.NET Core project publishing one binary, whatever your stack is), and runs as one process. Scaling it means running several identical copies of that one process behind a load balancer.
A monolith is a single, huge Swiss-army knife. Everything is in your pocket, always available, nothing to coordinate between tools — but it's heavy, and sharpening the scissors means recalling the whole knife.
Monoliths aren't a mistake — they're the right starting point for most projects. They're simple to build (your IDE, debugger and build tooling are all designed around "one project"), simple to test end-to-end (spin up the one app, drive the UI with Selenium), simple to deploy (copy one artifact to a server), and you can even scale them horizontally by running clones behind a load balancer. Most successful microservices systems — including the companies that popularised the pattern — started as monoliths.
The trouble starts as the shopping application grows. That's when the following drawbacks, which are invisible at small scale, start to dominate:
| Drawback of the monolith | What it looks like in the shopping app |
|---|---|
| Overwhelming complexity | No single developer understands checkout, pricing, inventory, shipping rules and notifications all at once. Every change requires understanding code you didn't write. |
| Slow development | The whole application must compile and start before you can test a one-line change to the shipping-label template. Start-up time grows with the codebase. |
| Obstacle to continuous deployment | One team's half-finished promotions feature, or a failing test in an unrelated module, blocks the release train for everyone — including the checkout team who just fixed a payment bug. |
| Conflicting scaling needs | The product-photo resizer is CPU-hungry and wants compute-optimised servers; the recommendation cache is memory-hungry and wants memory-optimised servers. They're the same process, so you must over-provision one box to satisfy both. |
| Reliability — one bug takes down everything | A memory leak in the product reviews module can exhaust the process and crash checkout too, even though reviews and checkout are logically unrelated. |
| Locked into one language/framework | You'd like to write the new fraud-detection module in a language with better ML libraries, but it has to live inside the same C#/.NET process as everything else. |
What is a microservice?
The Microservices Architecture pattern structures an application as a set of small, independently deployable services, each built around a business capability. Instead of one shopping-app process, you get a Product Catalog Service, an Order Service, a Customer Service, a Shipping Service and a Notification Service — each independently buildable and deployable, often owned by a separate team. Most services expose a REST (or similar) API consumed by other services or by clients; some communicate asynchronously via messages instead. For example, the Order Service asks the Customer Service to reserve credit; the Shipping Service tells the Notification Service to email a tracking link.
For the exam: each microservice is a small, independently deployable service built around a business capability. In practice: "independent" means independent build and deployment — not necessarily one Git repository per service. Many teams keep multiple services in a single monorepo (Google does this at scale, with tools like Bazel/Nx) yet still build and release each service on its own schedule. Repository layout is an organisational choice; the architectural requirement is only that a service can ship without lock-step coordination with the others.
ShoppingApp.sln
├─ ProductCatalog/
├─ Orders/
├─ Customers/
├─ Shipping/
├─ Notifications/
└─ SharedDb.mdf ← one schema, everyone reads/writes it
Deploy: publish ShoppingApp.dll → copy to server → restart
product-catalog-service/ → ProductDb
order-service/ → OrderDb
customer-service/ → CustomerDb
shipping-service/ → ShippingDb
notification-service/ → (no DB — stateless)
Deploy: each service ships independently, on its own schedule
The Scale Cube
The Scale Cube (from Abbott & Fisher's The Art of Scalability) is a mental model with three independent axes along which you can scale any application. It's a favourite exam question because one diagram cleanly answers "what actually is microservices architecture, scaling-wise?" — and the answer is precise: microservices architecture is Y-axis scaling.
| Axis | What it does | In the shopping app |
|---|---|---|
| X — horizontal cloning | Run N identical copies of the whole app behind a load balancer; each request goes to any clone. | Ten identical copies of the whole monolith, all sharing one database. |
| Y — functional decomposition | Split the application by responsibility; each piece is different code doing a different job. | Separate Product, Order, Customer, Shipping, Notification services. This is what "microservices" means. |
| Z — data partitioning | Run N identical copies, but each owns only a slice of the data; a routing key decides which copy handles a request. | Two identical order-processing clusters, one for customers A–M, one for N–Z, each with its own data shard. |
X and Z both run identical copies — the difference is whether every copy sees all the traffic and data (X) or only a slice of it (Z). Y is the odd one out: the copies aren't identical at all, they're different services doing different jobs. Real systems combine all three: microservices (Y), where each service is itself cloned (X), and the largest, hottest services are additionally partitioned (Z).
Click an axis to see how it scales the shopping application, concretely.
X-axis scaling runs multiple identical copies of the whole application behind a load balancer. It's simple — it's exactly how you already scale a monolith — but every clone still hits the same database and carries the full memory/CPU footprint of every module, even idle ones.
Application vs. database relationship
Microservices change the relationship between application and database more than almost anything else about them. Rather than sharing one database schema, each service owns its own schema — and no other service may reach into it directly. All access to a service's data goes through that service's API.
OrderDb directly.This is, on the surface, odd: it's the opposite of an "enterprise-wide data model," and it causes real duplication — Order Service might cache a copy of the customer's name and shipping address rather than joining across to Customer DB every time. But database-per-service is essential if you actually want the benefits of microservices, because it's what makes services loosely coupled: Customer Service can change its schema, switch database engines, or add a column, without ever coordinating with the Order team. If they shared one schema, you'd be back to a monolith wearing a microservices costume.
For the exam: a service's database is private — no other service may touch it directly. In practice: "private" is an ownership and discipline boundary, not a physical wall. Nothing technically stops another team from opening a connection to OrderDb; the rule is that they must not, and must instead go through the owning service's contract — its API, or the events it publishes. That discipline is the whole point: the moment a second service reads or writes another's tables directly, the two are recoupled and independent deployment is lost.
Database-per-service is where the "no free lunch" part of microservices begins. It means no
cross-service ACID transactions and no cross-service SQL joins — placing an order
that needs to update both OrderDb and CustomerDb atomically now needs a different
strategy entirely. Chapter 10 covers this in full,
with the concrete invoicing example and the event-driven fix.
Microservices vs. SOA
On the surface, Microservices Architecture looks a lot like SOA (Service-Oriented Architecture, covered in Chapter 8) — both structure an application as a set of services. The honest way to describe microservices is: SOA without the enterprise commercialisation baggage — no WS-* web service specifications, no Enterprise Service Bus (ESB).
| Dimension | Traditional SOA | Microservices |
|---|---|---|
| Protocol | WS-* / SOAP web service specifications | Lightweight protocols — REST, gRPC, simple messaging |
| Integration | Centralised Enterprise Service Bus (ESB) doing routing, transformation, orchestration | ESB-like logic pushed into the services themselves ("smart endpoints, dumb pipes") |
| Data model | Canonical schema shared enterprise-wide | Rejects the canonical schema — each service owns its own model |
| Governance | Centralised, often heavyweight (a dedicated integration team) | Decentralised — each team owns its service end-to-end |
Benefits and drawbacks
Be honest with yourself about this trade — nothing on the benefits side is free, and nothing on the drawbacks side is a dealbreaker for the right kind of application.
| ✅ Benefit | Why |
|---|---|
| Tackles complexity | Same total functionality, broken into human-sized, independently understandable services. |
| Independent development | Each service has a well-defined API boundary; a team owns it and can pick whatever tech stack honours the contract. |
| Independent deployment | The Shipping team never coordinates a release with the Order team — enables true continuous deployment. |
| Independent scaling | Deploy exactly as many instances of each service as its load requires, on hardware suited to its workload. |
| ❌ Drawback | Why |
|---|---|
| Distributed systems tax | You now must choose an IPC mechanism and write code to handle partial failure — a slow or down service, not just a broken function call. |
| Partitioned data / eventual consistency | Business transactions spanning multiple services can no longer rely on a single ACID transaction (see Chapter 10). |
| Testing complexity | Testing a feature that spans several services means standing up several services (or careful contract/mock testing). |
| Cross-service changes are hard | A change spanning Product, Order and Notification requires coordinating deployments across three teams — the opposite of the monolith's "recompile everything together" simplicity. |
| Deployment / operational complexity | Many more moving parts — every service has multiple instances that must be configured, deployed, scaled and monitored. This is why platforms like Kubernetes exist (see Chapter 10). |
Small services are a means, not the goal. The goal is decomposing enough to enable agile development and deployment — chasing "as small as possible" for its own sake just maximises the distributed systems tax without buying you anything.
The API Gateway
The problem: direct client-to-microservice
Consider a mobile client rendering a product details page. With the monolith, this is one
call: GET api.company.com/productdetails/{productId} — a load balancer routes it to any instance,
which queries several tables and returns one response.
With microservices, that data is now spread across Product Catalog, Reviews, Recommendations, Pricing and
Shipping services. In theory, the client could call each one directly — each service exposed at its own public
endpoint (https://reviews.api.company.com, etc.):
This breaks down for two concrete reasons:
- Mismatch between client needs and fine-grained APIs. The client wants one page; it now has to make several requests and stitch the results together itself — slow and chatty, especially on mobile networks.
- Web-unfriendly protocols leak out. One internal service might use a binary RPC protocol like Thrift, another an AMQP message queue — neither is browser- or firewall-friendly. Outside the firewall you want HTTP/WebSocket; internal services are free to use whatever's efficient.
- Hard to refactor. If the client hard-codes calls to five specific services, splitting or merging services later means changing every client — mobile app releases are slow and painful to ship.
The solution: API Gateway
An API Gateway is a single entry point that sits between clients and the microservices. It plays the same role as the Facade pattern plays for objects — one simplified interface hiding a complex set of subsystems behind it — just at the scale of a whole system instead of a class.
The Gateway's core responsibilities: request routing (send this path to that service), composition (call several services and merge results into the one response the client actually wants), and protocol translation (HTTP/WebSocket outside, Thrift/AMQP/whatever inside). It often also picks up cross-cutting concerns: authentication, monitoring, load balancing, caching, and request shaping. Many real systems use a Backend-for-Frontend (BFF) variant — a separate, thinner gateway per client type (mobile app, web app, partner API), each tailored to what that client needs.
| Detail | |
|---|---|
| ✅ Encapsulates internal structure | Clients never know how many services exist or how they're split — services can be refactored freely behind the gateway. |
| ✅ Client-tailored APIs, fewer round trips | One aggregated call instead of five — simpler client code, much better on high-latency mobile networks. |
| ❌ Another highly-available component to run | If the gateway is down, every client is down — it must itself be built, deployed, scaled and monitored carefully. |
| ❌ Risk of becoming a bottleneck | Every new microservice endpoint needs a gateway update — if one team owns the gateway, they can become the queue everyone waits behind. |
Chapter 11 shows the gateway as code — both the routing half and the aggregating half — and explains why an aggregating gateway is the component most likely to need a circuit breaker.
Inter-process communication (IPC)
In a monolith, components call each other via language-level method calls — fast, reliable, and (mostly) all-or-nothing. In microservices, every one of those calls potentially crosses a network to a different process, on a different machine, that might be slow, unreachable, or simply down. This single fact — replacing function calls with network calls — is the root of almost every new problem microservices introduces.
Client-service interaction styles
Two independent dimensions describe any interaction: how many participants handle a request (one-to-one vs one-to-many), and whether the caller blocks (synchronous vs asynchronous).
| Synchronous | Asynchronous | |
|---|---|---|
| One-to-one exactly one service instance handles the request |
Request/response — caller blocks until the reply arrives, and expects it promptly. | Notification (one-way, fire-and-forget, no reply expected) or request/async response — caller doesn't block; the reply may arrive later. |
| One-to-many multiple instances may handle the request |
The slides mark this "not meaningful" — but see the note below on scatter-gather. | Publish/subscribe — publish a notification, zero or more interested services consume it. Publish/async responses — publish a request, wait a bounded time for replies from interested services. |
For the exam: some slide decks mark synchronous one-to-many as "not meaningful." In practice: it is a real, named interaction — scatter-gather (a.k.a. fan-out/fan-in): a caller fires the same request at many services or instances in parallel and blocks until it has gathered the replies (or a deadline passes). A search aggregator querying many shards, or a price-comparison call fanning out to many providers and waiting for all of them, are everyday synchronous one-to-many examples.
In the shopping app: the Order Service calling Customer Service to "reserve credit and reply now" is
request/response. The Order Service telling Notification Service "order shipped, no reply
needed" is a notification. The Order Service publishing an
OrderCreated event that Customer Service, Analytics Service and Fraud-Check Service all independently
consume is publish/subscribe — the pattern Chapter
10 builds an entire architecture around.
Synchronous request/response vs. asynchronous messaging
Client sends a request and (usually) blocks the calling thread until a response arrives. Simple to reason about — it reads like a function call — but the caller is now coupled to the callee's availability and latency in real time.
Client sends a message onto a channel and moves on; a reply, if any, arrives as a separate message later. The caller isn't blocked, and doesn't even need the callee to be up right now — the message just waits on the channel.
Messages travel over channels. A point-to-point channel delivers a message to exactly one consumer (for one-to-one interactions). A publish-subscribe channel delivers a copy of the message to every attached consumer (for one-to-many interactions). Popular messaging middleware includes RabbitMQ, Apache Kafka, ActiveMQ and NSQ.
Handling partial failures
The defining new problem of distributed IPC: any call can fail for reasons a local method call never could — the network drops the packet, the other service is overloaded, or it's mid-deploy and simply not there. A monolith's method call never has to think about this. A microservice's remote call always does.
// No explicit timeout: this falls back to HttpClient's default
// of 100 seconds — an eternity to wait on a hot request path.
var response = await _http.GetAsync($"/products/{productId}");
response.EnsureSuccessStatusCode();
public class ProductServiceClient
{
private readonly HttpClient _http;
public ProductServiceClient(HttpClient http)
{
_http = http;
_http.Timeout = TimeSpan.FromSeconds(2); // never wait forever
}
public async Task<ProductDetails?> GetProductAsync(string productId, CancellationToken ct)
{
const int maxAttempts = 3;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
using var response = await _http.GetAsync($"/products/{productId}", ct);
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<ProductDetails>(ct);
bool transient = (int)response.StatusCode >= 500
|| response.StatusCode == HttpStatusCode.RequestTimeout // 408
|| response.StatusCode == HttpStatusCode.TooManyRequests; // 429
if (!transient)
break; // 401, 404, ... — retrying won't help; go to fallback
}
catch (HttpRequestException)
{
// network-level failure: transient, worth retrying
}
catch (TaskCanceledException) when (!ct.IsCancellationRequested)
{
// HttpClient.Timeout fired (not our caller cancelling): transient
}
if (attempt < maxAttempts)
// linear backoff; production adds jitter and honours Retry-After
await Task.Delay(TimeSpan.FromMilliseconds(200 * attempt), ct);
}
return null; // caller shows "temporarily unavailable" instead of hanging
}
}
ct) propagates instead of being swallowed.For the exam: a remote call with no timeout is dangerous — always bound how long you wait. Two details are worth stating precisely: (1) await _http.GetAsync(...) does not wait forever — HttpClient.Timeout defaults to 100 seconds — but 100 s is still far too long on a hot path, so set an explicit timeout or pass a CancellationToken. (2) await releases the calling thread back to the thread pool while the I/O is outstanding, so this isn't literally "one stuck thread per call"; the real danger is exhausting the connection pool and piling up in-flight requests until the service falls over. The advice — bound every wait, don't let a slow dependency stall request handling — is exactly right. Two more things production code cares about: retries are only safe when the operation is idempotent — a GET is, an unqualified POST isn't (retry it and you may create the order twice) — and real retry policies add jitter to the backoff and honour the server's Retry-After header. In practice you don't hand-roll this loop at all: you reach for Polly or Microsoft.Extensions.Http.Resilience, which bundle retry, timeout and circuit-breaker policies behind a single handler.
| Strategy | What it does |
|---|---|
| Network timeouts | Never block indefinitely — always bound how long you'll wait, so resources are never tied up forever. |
| Limit outstanding requests | Cap how many concurrent requests a client can have in flight to one service; once the cap is hit, fail new attempts immediately rather than piling up. |
| Circuit breaker | Track success/failure rates; once the error rate crosses a threshold, stop even trying — fail fast until the service has had time to recover. |
| Fallbacks | When a call fails, do something useful anyway — return cached data, a default value, or an empty result instead of propagating the failure to the user. |
The circuit breaker pattern, properly
It's named after the electrical circuit breaker in your house: if a fault draws too much current, the breaker trips and cuts the circuit — protecting the house from a fire, rather than letting the fault keep drawing power. Once you've fixed the fault, you flip it back and power flows again.
A circuit breaker wraps every outgoing call and moves through three states:
- Closed — the normal state. Requests go through; the breaker just counts successes and failures.
- Open — tripped. Every call fails immediately, without even attempting the network call — "fail fast." This protects both the caller (no wasted waiting) and the struggling downstream service (no pile-up of retries making things worse).
- Half-open — after a cooldown period, the breaker cautiously lets exactly one trial request through. Success ⇒ back to Closed. Failure ⇒ back to Open, cooldown restarts.
Deep dive: a minimal circuit breaker in C#
public enum CircuitState { Closed, Open, HalfOpen }
public class CircuitBreaker
{
private CircuitState _state = CircuitState.Closed;
private int _consecutiveFailures;
private DateTime _openedAt;
private readonly int _failureThreshold;
private readonly TimeSpan _openDuration;
public CircuitBreaker(int failureThreshold = 3, TimeSpan? openDuration = null)
{
_failureThreshold = failureThreshold;
_openDuration = openDuration ?? TimeSpan.FromSeconds(30);
}
public async Task<T> CallAsync<T>(Func<Task<T>> action, Func<T> fallback)
{
if (_state == CircuitState.Open)
{
if (DateTime.UtcNow - _openedAt < _openDuration)
return fallback(); // fail fast, don't even try
_state = CircuitState.HalfOpen; // cooldown elapsed, allow a trial
}
try
{
var result = await action();
_consecutiveFailures = 0;
_state = CircuitState.Closed;
return result;
}
catch
{
_consecutiveFailures++;
if (_state == CircuitState.HalfOpen || _consecutiveFailures >= _failureThreshold)
{
_state = CircuitState.Open;
_openedAt = DateTime.UtcNow;
}
return fallback();
}
}
}
Honest caveat: this minimal version is not thread-safe.
Under concurrent callers, several requests can each read _state == HalfOpen and fire a trial
call at the same time — so "exactly one trial request" only holds single-threaded (as in the simulator
below). A production breaker such as Polly guards the state transitions and the half-open
admission with real synchronisation (a lock or a SemaphoreSlim) so that exactly
one probe runs at a time. Treat the code above as a state-machine sketch, not a drop-in implementation.
Call a flaky "Recommendations Service" repeatedly. Watch failures accumulate, the breaker trip open, and — after a short cooldown — attempt a half-open trial.
Breakers and timeouts stop a failure spreading sideways. They do nothing about work that was already half-committed when the failure hit, or about the duplicate requests your retries just created. Chapter 11 covers the rest of the safety net — sagas, idempotency keys and dead letter queues — and critiques a circuit-breaker implementation with the three mistakes people most often make.
IPC technologies
| Technology | Style | Notes |
|---|---|---|
| REST (HTTP + JSON) | Synchronous request/response | Human-readable, firewall/browser-friendly, huge tooling support. The default choice unless you have a reason not to. |
| gRPC | Synchronous request/response (and streaming) | Binary, Protocol-Buffer-based, strongly typed contracts, much faster than REST/JSON — common for internal service-to-service calls where you control both ends. |
| Thrift | Synchronous, binary RPC | Efficient but not web-friendly — keep it internal, behind the API Gateway. |
| AMQP / STOMP (via RabbitMQ etc.) | Asynchronous messaging | Message broker handles delivery, retries, and both point-to-point and pub/sub channels. |
| Apache Kafka | Asynchronous, log-based pub/sub | High-throughput event streaming — the backbone technology of choice for event-driven architecture (Chapter 10). |
Message formats are a separate choice from the transport: human-readable text (JSON, XML) is easy to debug; binary formats (Avro, Protocol Buffers) are smaller and faster to (de)serialise, at the cost of needing a schema and tooling to read them.
The ancestors: RMI vs CORBA (and their modern heir, gRPC)
Before REST and gRPC, distributed objects talked via RMI (Java Remote Method Invocation) and CORBA (Common Object Request Broker Architecture). The module — and the exam — asks you to compare them, and the comparison is genuinely instructive: every axis below is a question you should ask of any IPC technology.
| Axis | RMI | CORBA | gRPC (modern analogue) |
|---|---|---|---|
| Language dependency | Java-only — both caller and callee must run on the JVM. | Language-independent by design — bindings for C++, Java, Python, and many others. | Language-independent — official code generation for a dozen+ languages. |
| Platform interoperability | JVM-to-JVM only. | Cross-platform via the standard IIOP wire protocol and an ORB (Object Request Broker) on each side. | Cross-platform via HTTP/2 — no broker needed. |
| Interface definition | Java interface extending
Remote — the contract lives in Java code itself. | A separate, language-neutral IDL (Interface Definition Language) file, compiled into per-language stubs and skeletons. | A language-neutral .proto file (Protocol Buffers IDL) compiled into per-language
stubs — CORBA's best idea, kept. |
| Ease of use | Simple if you're all-Java: no IDL, natural Java calls; registry lookup and stub download built in. | Notoriously complex — heavyweight specification, verbose IDL mappings, tricky ORB configuration and vendor incompatibilities. | Straightforward tooling
(protoc), but binary framing means you need tooling to debug. |
For the exam: RMI = simple but Java-locked; CORBA = language- and platform-neutral but complex, contract defined in IDL. In practice: both are legacy technologies you'll rarely deploy today — their trade-off (in-language convenience vs neutral-contract interoperability) lives on in the choice between a language-native RPC and schema-first gRPC, which is why examiners still ask about them.
Evolving APIs
A service's API changes over time, but you can't force every client to upgrade the moment it does. Design for the robustness principle: be liberal in what you accept, conservative in what you send. Minor, backward-compatible changes (adding an optional field) should never break old clients — the service supplies sensible defaults for anything missing, and clients ignore any extra fields they don't recognise.
For breaking changes, embed a version in the URL (/v1/products/{id} vs
/v2/products/{id}) and keep serving both for a deprecation window — either one service instance
handling multiple versions, or separate instances per version behind the gateway.
Service registration & discovery
In a traditional application on physical hardware, a service's network location (IP + port) barely changes. In a cloud-based microservices application it changes constantly — autoscaling adds and removes instances, deployments replace them, failures kill them, and container schedulers move them between hosts. Your client code can no longer hard-code an address; it needs a proper service discovery mechanism.
Underneath both discovery patterns below is a service registry: a highly-available database of currently-live service instances and their locations. An instance registers when it starts, is removed when it terminates, and typically refreshes its registration with a periodic heartbeat.
Client-side discovery
Simple — no extra moving part besides the registry itself — and since the client sees all instances, it can make smart, application-specific load-balancing decisions (e.g. consistent hashing). The drawback: it couples every client to the registry, so you must implement discovery logic in every language/framework your clients use.
Server-side discovery
Discovery logic is centralised in the load balancer, so clients need zero discovery code at all — and many cloud platforms (AWS ELB, Kubernetes Services) give you this for free. The drawback: unless the deployment platform already provides it, the load balancer is yet another highly-available component you must build and run.
Client-side discovery puts the load-balancing decision in the client; server-side puts it in the load balancer. Both still rely on the same underlying idea — a service registry that instances register with — they just differ in who reads from it.
The service registry
Netflix Eureka is the textbook example: a REST API for registering and querying instances. An
instance POSTs to register, sends a PUT heartbeat every 30 seconds to stay registered,
and is removed via an explicit DELETE or by simply timing out. Clients GET the current
list. Other well-known registries: etcd (used by Kubernetes and Cloud Foundry), Consul
(adds health checking), and Apache ZooKeeper (general-purpose distributed coordination, born out
of the Hadoop ecosystem).
For the exam: instances register on startup, heartbeat to stay listed, and are removed when they die; clients or load balancers then avoid them. In practice, discovery is never instant. A dead instance keeps receiving traffic until its heartbeat times out (with Eureka's defaults that can be tens of seconds), registry data is frequently cached client-side and so can be stale, and any requests already in flight to a now-dead instance still fail and must be retried. There is always a convergence window where the registry and reality disagree — which is exactly why the partial-failure tactics above (timeouts, retries, circuit breakers) are not optional.
Self-registration vs. third-party registration
| Self-registration | Third-party registration | |
|---|---|---|
| Who registers the instance? | The instance itself, on startup (and sends its own heartbeats) | A separate service registrar component that watches the deployment environment (polling or subscribing to events) |
| ✅ Benefit | Simple — no extra system component needed | Services stay decoupled from the registry — no registration code needed in every language/framework you use |
| ❌ Drawback | Couples every service to the registry client library, in every language you use | The registrar itself is yet another highly-available component to build and run, unless your deployment platform provides it already |
Three Product Service instances are registered. Send requests and watch them round-robin. Kill an instance and watch it drop out of rotation immediately — no request should ever land on it again.
Chapter 9 quiz
This chapter covered the shape of a microservices system and how its pieces talk to each other. Chapter 10 covers what happens to your data once it's split across all these services — polyglot persistence, event-driven consistency, deployment patterns, and how to actually get from a monolith to this architecture without a risky "big bang" rewrite.