Chapter 04 of 27 · Part 3: Communication style

Interprocess communication and synchronous APIs

How services talk: interaction styles, API contracts, REST and gRPC, partial failure and service discovery.

key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral

Why IPC suddenly matters

In a monolith, modules call each other with plain method calls. Services are separate processes, so every call crosses the network and can fail.

Method call versus network callInside a monolith modules call each other as methods. Between services calls cross the network.Monolith: method callOrdersBillingMicroservices: network call (IPC)Order svcKitchen svc
Services run as separate processes, so they need interprocess communication (IPC).

In-process vs inter-process

A network call is not a method call with a longer wire. Four differences change how you design APIs.

Method call versus network callFour things change when a call leaves the process: speed, data passing, changing the interface, and errors.In-processInter-processSpeedInlined, nearly freePackets: millisecondsDataPass a pointerSerialize, send, parseChangesRefactor both sides at onceSeparate deploys, so a contractErrorsMostly deterministicTimeouts, outages, partialSo design coarser APIs: 1,000 method calls were fine, 1,000 network calls are not.
Source: Building Microservices, chapter 4.
Failure modes and error codesFive kinds of failure between processes, and how HTTP error code ranges tell a client whether to retry.Five failure modes between processesCrashserver went downOmissionno reply at allTimingtoo slow or earlyResponsereply looks wrongArbitraryno agreementRich error codes let a client choose its reaction4xx: the request is wrong404: retrying will not help5xx: the server has a problem503 may pass, so retry. 501: do not.Many failures are short-lived, so retries help, but only if the error says so.
From Building Microservices: do not hide the network from developers. If a call crosses a process boundary, the code should make that visible so nobody is surprised by latency or failure.

Interaction styles

Think about the style before picking a technology. Asynchronous styles keep services loosely coupled.

Interaction stylesTwo dimensions: one-to-one or one-to-many, synchronous or asynchronous.One-to-oneOne-to-manySynchronousAsynchronousRequest / responsewait for the answer(none)Async request / responseOne-way notificationPublish / subscribePublish / async responses

Choosing a communication style

Pick the style first and the technology second. Newman starts by asking: request-response or event-driven?

Choosing a communication styleDecide first between request-response and event-driven. Request-response can be blocking or not. Event-driven is always asynchronous. Common data is a third, indirect option.Which style fits the job?Request-responseI ask, you act, tell me the resultEvent-drivenI announce a fact, others reactSync, blockingAsync, nonblockingAsync, nonblocking onlyCommon data: drop a file or table, others poll for it. Asynchronous, big volumes.
Most systems mix styles. One service can offer a request-response API and also publish events.
Blocking versus nonblockingSynchronous blocking is simple but couples caller and callee in time. Asynchronous nonblocking decouples them but is harder to reason about.Synchronous blockingAsynchronous nonblockingSimple and familiarCallee must be up right nowA slow callee slows the callerCallee can be down for a whileFits jobs that take hours or daysMore choices, more complexityCareful: using async/await on a result still blocks your code. It is asynchronous only underneath.
Communication through common dataOne service writes to a store, others pick it up later, usually by polling. It suits big volumes and older systems.One-way flow through a storeFlavorsImporterFile or tableInventoryCatalogData lake: raw filesWarehouse: fixed schemaShared DB: both writeReaders poll, so latency is high. Avoid shared tables that many services write.
From Building Microservices: Kafka for request-response, or a single-page-app framework for a plain website, are examples of a shiny tool in the wrong place. Start from the style you need.

Events, commands and requests

Both are messages, but they mean different things. A message is the envelope; the event or request is what it carries.

Command versus eventA command goes to one named service, which may refuse. An event is a published fact that many services can read. A correlation id links the command to the event that follows.UIOrder svcEvent topicInventory svcNotification svccommandeventevent carries the correlation idCommand or requestNames one receiver. It may say no.Send/receive, point to pointEventA fact that already happened.Publish/subscribe, many readersNewman prefers the word request: a command sounds like an order, but a service may reject it.
What goes into an eventAn event can carry just an id, everything, or the fields consumers need. Metadata can sit in an envelope or in message headers.Just an IDSmall and private.Listeners must call back.EverythingFewer callbacks, butmore is public to break.Just rightThe fields consumers need,in your own domain terms.Envelope (or message headers)Metadataid, time, version, correlation idPayloadthe business facts only
Rule of thumb: put in an event only what you would also share in a request-response API.
From Practical Event-Driven Microservices: a "just right" event avoids two traps. OrderStreetNumberChanged forces consumers to rebuild the address, while a huge event leaks other domains. Publish what consumers need, and involve them in the design.

APIs are contracts

A service and its clients are not compiled together, so a breaking change shows up as a runtime failure. Write the API definition first (API-first), then evolve it carefully.

Evolving an APIBackward-compatible (easy)Add optional request fieldsAdd response fieldsAdd new operationsBreaking (be careful)Support old and new versions togetherVersion in the URL: /v1/ and /v2/Or put the version in the messageSemantic version: MAJOR.MINOR.PATCH. Bump MAJOR only for breaking changes.
Message formatsText: JSON, XMLHuman readableSelf describingVerbose and slower to parseBinary: Protobuf, AvroCompact and fastTyped schema forces API-firstNot human readableAlways use a cross-language format. Never Java serialization.

Schemas and handling change

Make the interface explicit, then change it with care. Schemas catch structural breaks early.

Structural and semantic breaking changesA schema comparison can catch structural breaks, such as a removed field. A semantic break keeps the shape but changes behavior, so only tests catch it.Structural breakSemantic breakcalculate(a, b) becomes calculate(a)Same shape, but add becomes multiplySchema comparison fails the CI buildTests: consumer-driven contractsTools: Protolock (protobuf), openapi-diff, JSON Schema diff, Confluent Schema Registry.Pact is a tool for consumer-driven contract tests.
Five ways to avoid breaking changesPrefer expansion changes, read tolerantly, pick technology that evolves easily, keep the interface explicit and catch accidents early.Expansion changesadd things, do not removeTolerant readertake only what you needRight technologye.g. protobuf field numbersExplicit interfacea schema shows what is publicCatch breaks earlyfail the build, not production
Tolerant readerA consumer that reads only the fields it needs keeps working when other fields are removed or when the payload is restructured.Customer responseEmail service readsfirstnamelastnameemailtelephonefirstnamelastnameemailtelephone is not read,so it is free to removeBe liberal in what you accept. Find fields by name or path, never by position.
Handling a breaking changeLockstep deployment breaks independent deploys. Running two service versions is costly. Emulating the old endpoint, then removing it later, is the expand and contract approach.LockstepChange service and everyconsumer together.Run two versionsTwo codebases and routing.Short canaries only.Expand and contractOne service, old and newendpoints side by side.Expand and contract in three steps1 Expand: /v1 + /v22 Consumers move3 Contract: drop /v1Track who still calls the old endpoint before you remove it.Version in the URL (/v1/) or a header. Avoid keeping more than two versions.
How formats help evolutionJSON names fields in every payload. Protocol buffers number fields. Avro can send the schema with the data.JSON / XMLField names travel inevery payload. Add fieldsfreely; schema is optional.Protocol buffersEach field has a number.Old clients ignore new ones.Used by gRPC.AvroSchema can ride alongwith the payload. Commonfor message payloads.For events, AsyncAPI and CloudEvents try to describe the interface in the same explicit way.
From Building Microservices: a company that gave consumers a year to leave an old interface never checked who still used it. Always track callers, or you cannot remove the old endpoint safely or know who you will break.

Synchronous calls: REST and gRPC

Remote procedure invocation (RPI) means the client sends a request and waits for a reply.

REST and gRPC comparedRESTSimple and familiarTest with curl or a browserFirewall friendlyREST: downsidesRequest/response onlyBoth sides must be upClients must find itgRPCRich operations, compactStreaming, polyglotNeeds HTTP/2

REST, RPC and GraphQL compared

The synchronous options differ in what they assume about client and server.

REST resources and verbsClients use a few HTTP verbs on a resource and get back a representation. HTTP also brings caching proxies, load balancers and security tools.GET: read itPOST: create onePUT: change itCustomer resource/customers/42JSON viewOther viewSame few verbs on every resource: no createCustomer or editCustomer methods.How it is stored inside stays hidden. The client sees only a representation.
Hypermedia controlsA response contains links the client follows, so the server can move or change things behind the links without breaking the client.ClientGETAlbum representationname: Give Bloodlink: artistlink: instantpurchaseArtist infoBuy the albumSound idea: clients follow links, not hard-coded URLs.Newman has rarely seen it repay the extra work, so treat it as optional.
RPC: attractive but riskyRPC makes a remote call look local and generates client code from a schema. The same trait hides the network and can force clients and servers to deploy together.Why people like itWatch outRemote call looks like a local callClient code generated from schemagRPC: HTTP/2, fast, many languagesHides the network: chatty designsBinary stubs: removing a field breaksCan force lockstep releasesNewman puts gRPC first when you control both client and server.With many unknown clients, REST over HTTP is usually the better fit.
TechnologyStyleBest forWatch out
gRPC / RPCSync, request-responseInternal calls you control at both endsSchema coupling, hidden network cost
REST over HTTPSync, request-responseMany different clients, the perimeterFew schemas by default, chatty
GraphQLSync queriesEdge: mobile and GUI aggregationCaching, query load, mostly reads
SOAPSync RPC over HTTPLegacy integrationHeavyweight, ignores HTTP verbs and codes
Message brokerAsyncEvents and long jobsAnother system to run

Handling partial failure

If a service is slow or down, callers can pile up and the failure spreads. Use timeouts, limit outstanding requests, and add a circuit breaker.

Partial failure can cascadeMobile appPOST /ordersAPI gatewaywaits foreverOrder serviceunresponsiveWithout timeouts the gateway runs out of threads and the whole API goes down.
Circuit breaker statesClosed allows calls. Too many failures open it so calls fail fast. After a timeout it tries once in half-open.Closedcalls flowOpencalls fail fastHalf-opentry one requestfailurestimeoutsuccess: close againfailure: open again
Also: use timeouts and cap the number of outstanding requests.

Finding services: service discovery

Instance addresses change as services scale and restart, so clients ask a registry. Prefer the platform's built-in discovery.

Service discoveryInstances get dynamic addresses, so clients look them up in a registry. Either the app does it or the platform does.Application-level (client-side)ClientRegistry1 query 2 list of instancesInstance 1Instance 2Instances register themselves(self registration). The clientload-balances (client-side discovery).Platform-provided (recommended)ClientRouter / DNSInstance 1Instance 2The platform registers instances(3rd party registration). The clientcalls one DNS name (server-side discovery).

Discovery options and sharing code

Two practical questions: how do services find each other, and what can they share?

Service discovery optionsDNS is simple but cached entries go stale. Registries such as Consul track live instances. Kubernetes finds pods by labels.DNSWorks with every stack.Entries cache for the TTL,so changes are slow.Point it at a load balancer.Registry (Consul)Services register themselves.HTTP API and DNS SRV records.Health checks built in.Handy beyond Kubernetes.KubernetesA service picks pods by label.Requests go to a healthy pod.Often all you need.etcd stores the state.
Also publish the catalog in a form people can browse, not only machines.
Sharing code between servicesCode that stays inside one service is fine to share. Code that crosses the boundary couples services, because they cannot all upgrade at once.Usually fineRiskyLogging helpers, invisible outsideCopy a service template, no sharingSDKs written by a different teamShared domain objects everywhereServer logic leaking into client libsForcing every client to upgradeYou cannot update a library everywhere at once. Accept several versions in the wild,or put the shared behavior in a service. Clients should choose when to upgrade.
From Building Microservices: DRY means do not duplicate knowledge, but across service boundaries a little duplication is often cheaper than coupling. Netflix client libraries also carried discovery and failure handling, not only the API.

Recap

Quick check

Why does calling three 99.5% services in a row give only 98.5% availability?
Show answer

A synchronous request needs all three to be up at once, so the probabilities multiply: 0.995 x 0.995 x 0.995 is about 0.985.

What is the difference between a structural and a semantic breaking change?
Show answer

A structural break changes the shape of the interface (a field removed, a new required field), and a schema comparison can catch it. A semantic break keeps the shape but changes behavior, so only tests such as consumer-driven contracts catch it.