CHAPTER 9 · ARCHITECTURE

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.

🧠 Why this matters

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.

🌍 Real-world analogy

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 monolithWhat it looks like in the shopping app
Overwhelming complexityNo single developer understands checkout, pricing, inventory, shipping rules and notifications all at once. Every change requires understanding code you didn't write.
Slow developmentThe 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 deploymentOne 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 needsThe 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 everythingA 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/frameworkYou'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.

🎓 Lecture view vs reality

For the exam: each microservice is a small, independently deployable service built around a business capability. In practice: "independent" means independent build and deploymentnot 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.

Shopping Monolith Product CatalogService OrderService CustomerService ShippingService NotificationService Same total functionality — reorganised into independently deployable pieces.
Figure: the shopping monolith decomposed by business capability. Total functionality is unchanged — it's just no longer one deployable.
❌ Monolith: one solution, one database
ShoppingApp.sln
 ├─ ProductCatalog/
 ├─ Orders/
 ├─ Customers/
 ├─ Shipping/
 ├─ Notifications/
 └─ SharedDb.mdf        ← one schema, everyone reads/writes it

Deploy: publish ShoppingApp.dll → copy to server → restart
Every module compiles into one binary. A schema change for Orders can silently break a query written by the Shipping team.
✅ Microservices: five deployables, five schemas
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 Shipping team can deploy ten times a day without asking anyone's permission — as long as they don't break their public API.

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.

Shopping App single instance X — clone identical copies behind a load balancer Y — functional split Product / Order / Customer / Shipping services ← this is microservices Z — partition route by customer ID / region / shard key
Figure: three independent scaling axes, all starting from a single application instance.
AxisWhat it doesIn the shopping app
X — horizontal cloningRun 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 decompositionSplit 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 partitioningRun 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.
🔀 Easily confused

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).

Try it: Scale cube explorer

Click an axis to see how it scales the shopping application, concretely.

Client Load Balancer Shopping Appclone 1 Shopping Appclone 2 Shopping Appclone 3 Shared DB

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.

What to notice: only Y-axis scaling gives each piece its own database and its own deploy pipeline. X and Z scale copies; Y scales decomposition. That's why the slides say "microservices corresponds to Y-axis scaling" — not because X and Z don't matter, but because Y is the axis that actually changes the architecture.

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.

Customer Service Order Service Customer DB Order DB ✅ own data ✅ own data ✅ API call: "reserve credit" ❌ never: SELECT directly from Order DB
Figure: Customer Service may only reach Order data by calling Order Service's API — never by querying 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.

🎓 Lecture view vs reality

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.

⚠️ Watch out

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).

DimensionTraditional SOAMicroservices
ProtocolWS-* / SOAP web service specificationsLightweight protocols — REST, gRPC, simple messaging
IntegrationCentralised Enterprise Service Bus (ESB) doing routing, transformation, orchestrationESB-like logic pushed into the services themselves ("smart endpoints, dumb pipes")
Data modelCanonical schema shared enterprise-wideRejects the canonical schema — each service owns its own model
GovernanceCentralised, 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.

✅ BenefitWhy
Tackles complexitySame total functionality, broken into human-sized, independently understandable services.
Independent developmentEach service has a well-defined API boundary; a team owns it and can pick whatever tech stack honours the contract.
Independent deploymentThe Shipping team never coordinates a release with the Order team — enables true continuous deployment.
Independent scalingDeploy exactly as many instances of each service as its load requires, on hardware suited to its workload.
❌ DrawbackWhy
Distributed systems taxYou 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 consistencyBusiness transactions spanning multiple services can no longer rely on a single ACID transaction (see Chapter 10).
Testing complexityTesting a feature that spans several services means standing up several services (or careful contract/mock testing).
Cross-service changes are hardA change spanning Product, Order and Notification requires coordinating deployments across three teams — the opposite of the monolith's "recompile everything together" simplicity.
Deployment / operational complexityMany 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).
ℹ️ Note

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.):

Mobile Client Product Svc Reviews Svc Pricing Svc Shipping Svc Recommendations Svc 5 round trips, 5 protocols to speak, 5 places the client needs to know about
Figure: five separate calls (and five failure modes) just to render one page.

This breaks down for two concrete reasons:

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.

Mobile Client API Gatewayroute · aggregate · translate Product Svc Reviews Svc Pricing Svc Shipping Svc
Figure: the client speaks to exactly one thing. The Gateway routes, aggregates multiple service calls into one response, and translates protocols.

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 structureClients never know how many services exist or how they're split — services can be refactored freely behind the gateway.
Client-tailored APIs, fewer round tripsOne aggregated call instead of five — simpler client code, much better on high-latency mobile networks.
Another highly-available component to runIf the gateway is down, every client is down — it must itself be built, deployed, scaled and monitored carefully.
Risk of becoming a bottleneckEvery 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).

SynchronousAsynchronous
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.
🎓 Lecture view vs reality

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

Synchronous, request/response (e.g. REST, gRPC)

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.

Asynchronous messaging (e.g. queues, Kafka)

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 — the naive call
// 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();
It won't hang literally forever, but a slow downstream service can tie up pooled connections and in-flight requests for the full 100 s — work piles up faster than it drains and the caller degrades too, a cascading failure.
✅ Timeout + bounded retry + fallback
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
    }
}
Bounded wait, bounded retries, and an explicit fallback path — but only for failures where a retry can actually help: 5xx/408/429 and network or timeout errors. A 401 or 404 breaks straight to the fallback, and a real caller cancellation (ct) propagates instead of being swallowed.
🎓 Lecture view vs reality

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.

StrategyWhat it does
Network timeoutsNever block indefinitely — always bound how long you'll wait, so resources are never tied up forever.
Limit outstanding requestsCap 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 breakerTrack success/failure rates; once the error rate crosses a threshold, stop even trying — fail fast until the service has had time to recover.
FallbacksWhen 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

🌍 Real-world analogy

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 calls pass through normally OPEN calls fail immediately, no network hit HALF-OPEN let one trial request through error rate ≥ threshold timeout elapses trial succeeds trial fails
Figure: Closed → Open when failures cross the threshold. Open → Half-Open once the recovery timeout elapses. Half-Open → Closed on a successful trial, or back to Open on another failure.
Deep dive: a minimal circuit breaker in C#
CircuitBreaker.cs
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.

Try it: circuit breaker simulator

Call a flaky "Recommendations Service" repeatedly. Watch failures accumulate, the breaker trip open, and — after a short cooldown — attempt a half-open trial.

CLOSED
What to notice: while OPEN, calls are rejected instantly (no simulated "network wait") — that's the fail-fast benefit. After the 4-second cooldown the breaker allows exactly one trial call in HALF-OPEN: one success closes it, one failure reopens it and restarts the cooldown.
💡 Where this goes next

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

TechnologyStyleNotes
REST (HTTP + JSON)Synchronous request/responseHuman-readable, firewall/browser-friendly, huge tooling support. The default choice unless you have a reason not to.
gRPCSynchronous 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.
ThriftSynchronous, binary RPCEfficient but not web-friendly — keep it internal, behind the API Gateway.
AMQP / STOMP (via RabbitMQ etc.)Asynchronous messagingMessage broker handles delivery, retries, and both point-to-point and pub/sub channels.
Apache KafkaAsynchronous, log-based pub/subHigh-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.

AxisRMICORBAgRPC (modern analogue)
Language dependencyJava-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 interoperabilityJVM-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 definitionJava 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 useSimple 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.
🎓 Lecture view vs reality

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

Client Service Registry Instance A Instance B ① query ② call (client picks & load-balances)
Figure: the client queries the registry and load-balances between instances itself.

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

Client Load Balancer Service Registry Instance A Instance B request
Figure: the client just calls the load balancer; it — not the client — queries the registry and routes the request. AWS ELB is a real-world example.

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.

🔀 Easily confused

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).

🎓 Lecture view vs reality

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-registrationThird-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)
✅ BenefitSimple — no extra system component neededServices stay decoupled from the registry — no registration code needed in every language/framework you use
❌ DrawbackCouples 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
Try it: service discovery walkthrough

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.

What to notice: killing an instance immediately removes it from the registry (in real systems this happens once its heartbeat expires) — the load balancer / client never routes to it again, and traffic keeps flowing across the survivors.

Chapter 9 quiz

💡 Where to go next

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.