Roads Through the Sky: Connecting Cloud-Native Services
A distributed kingdom survives only when its distant cities know how to speak.
A single castle can rely on hallways. The kitchen knows where the pantry is, guards can walk directly to the armory, and a steward who needs an answer can cross the courtyard and ask for it. Once the kingdom spreads across distant cities, those assumptions disappear. The treasury may live in one region, the guild registry in another, and the market serving the public somewhere else entirely. Each city can function perfectly on its own and still leave the kingdom paralyzed if the roads between them are unreliable or poorly governed.
Cloud-native systems inherit the same problem. Containers, orchestration, managed infrastructure, and serverless functions let us divide software into independently deployable components, but distribution introduces a new architectural responsibility. Those pieces must communicate across networks that introduce latency, failures, changing addresses, security boundaries, and capacity limits. A function may need information from a service running in Kubernetes, while an API may depend on several downstream services before it can answer one request. The network is therefore not plumbing beneath a distributed architecture. It is part of the architecture.
When the Hallway Becomes a Road: Understanding Remote Communication
Inside a monolithic application, communication is deceptively cheap. One component calls another through a method or module boundary, and the machine usually treats the interaction as local work. The caller assumes the other component exists, responds quickly, and receives exactly what was sent. Developers rarely design every local call as though the destination might disappear halfway through the conversation.
A network call changes those assumptions. The destination can exist while remaining unreachable. A request can arrive even if the response never does. DNS can fail, certificates can expire, routes can change, and a healthy service can become unavailable because all workers are already occupied. Cloud-native frameworks may hide much of this behind ordinary-looking client code, but they do not remove the underlying uncertainty.
A local call assumes presence; a remote call must negotiate uncertainty. Every remote dependency therefore introduces time, failure, capacity, and operational behavior into what might otherwise look like a simple function call. Once communication crosses a network boundary, reliability becomes part of the design.
The Treaty Between Cities: Designing Service Contracts
Before deciding how two services communicate, an engineering team needs to decide what they are promising each other. That promise is the service contract. It defines accepted requests, returned responses, data formats, errors, and the compatibility guarantees that allow both sides to evolve without coordinating every deployment.
This is why service communication is not primarily an HTTP problem. HTTP is a transport mechanism, REST is an architectural style, and gRPC is a communication framework. None can repair a contract that exposes unstable internal details or forces consumers to understand how the provider happens to work today. A beautifully documented endpoint can still create a brittle architecture if changing one database field requires six other systems to change as well.
Good contracts expose capabilities rather than machinery. A billing service might allow another system to request an invoice, retrieve payment status, or calculate an outstanding balance without revealing which tables or workflows implement these capabilities. The boundary becomes valuable when the provider can change internally while consumers continue relying on the same promise. A royal courier who reaches every city perfectly is still useless if each city interprets the message differently.
The Merchant Highway: Connecting Services with REST
REST APIs remain one of the most common ways cloud-native services communicate because HTTP is widely understood, broadly supported, and easy to inspect. A service exposes resources or capabilities through URLs, clients make requests using standard HTTP methods, and representations such as JSON travel between them. The resulting interface can work across languages, operating systems, deployment platforms, and organizational boundaries.
Consider an order service that needs customer information before accepting a purchase:
</> http
GET /customers/8f31c2
Accept: application/json
The customer service could respond:
</> JSON
{
"id": "8f31c2",
"status": "active",
"shippingRegion": "west"
}
Nothing in this interaction requires the order service to know whether the customer service runs in a container, a serverless function, or a managed application platform. Deployment decisions remain behind the boundary while the consumer depends on the behavior exposed through the API.
REST becomes less elegant when teams expose too much of a domain through an expanding collection of endpoints. Chatty APIs can force a business operation to make numerous sequential requests, while poor boundaries can cause consumers to reconstruct business rules that belong within the provider. The important question is not whether REST works, but whether the API preserves a useful boundary while keeping communication understandable.
Seven Gates Before Supper: Understanding Network Latency
Suppose an API receives a request that appears simple to the user. To answer it, the application calls the account service, then the inventory, pricing, and permissions services. Each service may be healthy and fast on its own, yet the user experiences the cumulative cost of the entire path.
Latency compounds across synchronous dependencies. Sequential work adds directly to response time, while parallel work still waits for the slowest required dependency. Network hops, serialization, TLS processing, queueing, database access, and retries add more time to a service graph that looked effortless on the architecture diagram.
This is one reason service boundaries should not be created simply because cloud platforms make services easy to deploy. Every boundary has a communication cost. Splitting software creates value when the resulting independence is worth the added complexity of network, deployment, observability, and failure management introduced by the split. A kingdom can have the fastest horses in the realm and still move slowly if every journey requires crossing seven gates.
The Arcane Dispatch: Using gRPC for Service Communication
HTTP and JSON are excellent when interoperability, visibility, and broad client support matter. Internal service communication sometimes has different priorities. Systems that make large numbers of structured calls may care more about compact payloads, well-defined contracts, streaming, and efficient communication between services under common control. gRPC can fit those relationships well.
gRPC commonly uses Protocol Buffers to define contracts. Instead of relying on loosely structured JSON documents interpreted at runtime, teams define messages and operations in a schema that can generate typed client and server code:
</> proto
syntax = "proto3";
service InventoryService {
rpc GetAvailability(AvailabilityRequest)
returns (AvailabilityResponse);
}
message AvailabilityRequest {
string product_id = 1;
}
message AvailabilityResponse {
string product_id = 1;
int32 available_quantity = 2;
}
The engineering lesson is not that gRPC is faster and therefore better. Its value comes from properties that meet certain requirements, including well-defined schemas, compact binary serialization, generated clients, and streaming support. Internal services with high call volumes may benefit from those characteristics, while browser-facing or public APIs may benefit more from the accessibility and inspectability of HTTP and JSON.
Choosing between REST and gRPC should therefore begin with the relationship being designed, not with a contest between technologies. A merchant highway, a military road, and a mountain pass can all be excellent routes while serving different traffic. The strongest architecture chooses the route according to who must travel it, what they must carry, and how much complexity the journey can justify.
The Moving City: Understanding Service Discovery
Communication protocols solve only part of the problem. A service still needs to know where another service lives. Fixed hostnames or IP addresses become fragile in cloud-native environments because workloads move, scale, restart, and disappear as ordinary parts of operation.
Kubernetes illustrates the problem clearly. A pod may be replaced after a failure, a deployment may scale from three instances to twelve, and a new release may gradually replace the previous version while traffic continues. If every consumer had to maintain a current list of those addresses, the architecture would quickly collapse into configuration management.
Service discovery gives callers a stable way to locate a changing set of service instances. Instead of asking which pod currently holds the inventory service, a caller asks for the inventory service itself. The platform resolves that logical identity to an appropriate destination. The deeper value is that service discovery separates identity from location, allowing the workload to move without requiring every consumer to learn its new location.
The Royal Gatehouse: Using API Gateways at the Edge
Browsers, mobile applications, partner systems, and public clients need controlled entry points into a distributed architecture. Allowing each client to communicate directly with every internal service usually creates more coupling than freedom, especially as the number of services grows.
An API gateway provides a stable entrance. It routes requests to internal destinations and can handle cross-cutting concerns such as authentication, rate limiting, request validation, TLS termination, logging, and API version routing. A mobile application can therefore rely on a single external surface rather than understanding the addresses and access rules for every internal service.
The gateway should not become a second monolith disguised as infrastructure. Once business rules, orchestration, and domain logic accumulate there, the boundary stops protecting services and begins owning them. A gatehouse should decide who enters and where they go, not govern every market stall inside the city.
Dividing the Caravan: Understanding Load Balancing
Cloud-native systems often run multiple copies of the same workload. Redundancy improves availability, while horizontal scaling adds capacity as demand grows. Those benefits depend on distributing requests across healthy instances rather than allowing traffic to concentrate on one destination.
Load balancing performs that distribution. A load balancer accepts traffic for a logical service and forwards requests to available backends using strategies such as round-robin, weighted routing, or connection-based decisions. Health checks and readiness signals help determine whether an instance should receive traffic.
That distinction matters during deployment. A process may be alive while still loading configuration, establishing database connections, or warming caches. Sending production traffic too early can create transient failures that are difficult to reproduce later. Reliable load balancing therefore depends on knowing whether a destination is ready, not merely whether its process exists.
The Narrow Bridge: Understanding Capacity Across Services
Load balancing spreads work, but it does not create unlimited capacity. If ten service instances can collectively process one thousand requests per second, sending them two thousand does not make them heroic. It makes them overloaded.
Horizontal scaling can increase capacity, but scaling takes time and eventually encounters limits in databases, networks, quotas, or cost. Automatic scaling does not remove the need to understand those constraints because sudden spikes may reach a service before new capacity becomes available. A database may also remain fixed while dozens of new application instances begin sending it additional traffic.
The system is only as elastic as its dependencies, which cannot grow at the same pace. If an API scales from 10 instances to 100 while each instance increases traffic to the same downstream dependency, successful scaling can cause that dependency to fail faster. Expanding the army does not help when every soldier must cross the same narrow bridge.
Fog on the Road: Designing for Network Failure
Distributed engineers must abandon the idea that network communication is simply working or broken. A connection may be established and then lost, a request may arrive while the response does not, or a destination may become slow enough to be effectively unavailable.
This creates ambiguity that local calls rarely produce. A payment service might charge the customer and then lose the connection before sending its response. The caller cannot safely assume the operation failed, as the work may already have occurred.
Timeouts are therefore essential. A caller needs a finite period after which it stops waiting and decides how to proceed. Too short, and normal latency becomes failure. Too long, and resources remain tied to requests unlikely to succeed. Useful timeout policies come from observed behavior, user expectations, and dependency characteristics.
The Persistent Courier: Using Retries Without Creating a Siege
Retries can recover from temporary failures, but they also create additional load precisely when a system may already be struggling. If thousands of timed-out requests are immediately retried repeatedly, a small slowdown can become a self-imposed siege.
Production retry strategies therefore use limits and delays. Exponential backoff increases the waiting period between attempts, while jitter adds randomness to prevent thousands of callers from retrying simultaneously. Retries should also be reserved for failures where another attempt is meaningful and operations where repetition is safe.
A simplified implementation might look like this:
</> JavaScript
async function fetchWithRetry(url, attempts = 3) {
let delay = 200;
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(1500)
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt === attempts) {
throw error;
}
const jitter = Math.random() * 100;
await new Promise(resolve =>
setTimeout(resolve, delay + jitter)
);
delay *= 2;
}
}
}
The lesson is not the JavaScript syntax. The important behaviors are bounded waiting, limited attempts, and increasing delays between retries. A production system would still need to decide which failures deserve another attempt, whether the operation is idempotent, and how much total latency the caller can tolerate.
The Chain of Castles: Understanding Synchronous Dependencies
REST and gRPC commonly support synchronous communication. One service sends a request and waits for the result before continuing. This is entirely appropriate when the caller genuinely needs an immediate answer.
Problems emerge when synchronous communication spreads too deeply through the architecture. A checkout request might depend on inventory, pricing, promotions, and customer status before it can respond. The transaction now succeeds only when all required services respond within the available time.
The chain creates both latency and availability coupling. Each service can be highly reliable individually, yet the overall path is less reliable because success depends on all required components being available simultaneously. If checkout cannot proceed without confirming inventory, waiting makes sense. If it also wants to update analytics, refresh recommendations, and send an email receipt, forcing the user to wait creates coupling without improving the transaction.
That distinction leads to a different kind of road through the kingdom, one where the sender does not always wait for the messenger to return.
The Borders Between Kingdoms: Preserving Loose Coupling
A distributed system requires communication, but communication does not require every service to depend on every other service. Two services can exchange information while remaining loosely coupled, or they can communicate through an interface that binds their deployment schedules, availability, data models, and failure behavior together. The distinction is architectural rather than technical.
Loose coupling comes from controlling what crosses the boundary. A consumer should depend on a stable contract rather than the provider’s internal schema. A service should request the capabilities it needs rather than reconstructing another service’s domain model locally. Versioning policies should also allow providers to evolve without forcing every consumer to change at the same time.
This is where APIs earn their place in architecture. Their value is not merely that they connect software. Their value lies in defining how much one part of the system is allowed to know about another. A well-designed boundary preserves ignorance.
That ignorance is useful. The order service should know how to request customer status without knowing where customer records are stored. The customer service should not care whether the order service runs on Kubernetes, serverless infrastructure, or something else entirely. Each side needs enough knowledge to collaborate and no more.
Choosing the Right Road: Matching Communication to the Relationship
REST, gRPC, gateways, discovery, and load balancing are often discussed as technologies to compare. The more important question is the relationship between the systems that need to communicate.
Public interfaces usually benefit from technologies that are broadly accessible and easy to inspect. REST over HTTP fits that need well. Internal services under common ownership may benefit from gRPC when typed contracts, streaming, and communication efficiency matter more than human-readable payloads.
Service discovery matters when destinations are dynamic. API gateways help when external consumers need a stable entry point to an evolving internal architecture. Load balancing becomes necessary when a single logical service is represented by multiple running instances.
None of these mechanisms replaces the others because they solve different parts of the communication problem. A production request might enter through an API gateway, be routed to a load-balanced service, discover an internal dependency by logical name, and call that dependency through gRPC. Architecture emerges from how those responsibilities fit together, not from selecting one winner.
Too Many Roads on the Map: Controlling Communication Complexity
As systems grow, engineers often draw service diagrams filled with arrows. Those diagrams can look reassuring because every line proves that the architecture is connected, but complexity can masquerade as sophistication.
Every synchronous arrow represents a dependency that can affect latency and availability. Every public endpoint becomes a contract someone may depend on. Every gateway rule, DNS entry, certificate, timeout, retry policy, and load-balancing configuration becomes part of the production system that somebody eventually has to understand.
Reducing unnecessary communication is therefore an architectural optimization rather than a performance optimization. Sometimes two operations belong in the same service because separating them would create constant network chatter. Sometimes repeated remote calls are evidence that service boundaries were drawn around technical layers rather than meaningful business capabilities.
A distributed architecture should not maximize distribution. It should distribute only what benefits from independence. Kubernetes can run hundreds of workloads, and cloud platforms can connect services across regions, but those abilities are tools rather than obligations. The kingdom does not become stronger because every workshop has been declared an independent city-state.
The Broken Bridge: Designing for Expected Failure
Every remote interaction eventually encounters failure, so the mature question is not whether a connection will fail, but what the system should do when it does.
Different operations need different answers. A product catalog request might fall back to cached information. A payment request may require idempotency guarantees before any retry occurs. A reporting request might tolerate several seconds of delay, while an authentication check may sit directly on a critical user-facing path.
Reliability policy therefore belongs close to business semantics. Infrastructure can provide sensible defaults for timeouts, retries, health checks, and routing, but the application still needs to understand the consequences of repeating or abandoning an operation. A platform cannot decide whether charging a credit card twice is acceptable.
Engineers should classify remote calls by behavior. Ask whether the call is required for the current operation, whether it modifies state, whether repetition is safe, and what the caller should do when the dependency is unavailable. Those questions turn networking from an implementation detail into a matter of engineering judgment.
Racing the Sundial: Designing with Latency Budgets
Distributed systems force engineers to think explicitly about time. Across a network, requests wait for DNS resolution, connection establishment, queue capacity, processing, and the return journey of the response.
A downstream service that technically promises a response but routinely takes thirty seconds may be unusable to a caller with a two-second user-facing deadline. Teams should therefore reason about latency budgets rather than individual service speed. If an API must respond within 500 milliseconds and depends on three synchronous calls, those dependencies cannot each casually consume 400 milliseconds.
The available time must be divided across the complete request path while leaving room for normal variation and failure handling. The system succeeds as a path, not as a collection of isolated benchmarks. A kingdom can have the fastest horses in the realm and still lose the war if every journey requires crossing twelve gates.
The Council of Guilds: Aligning Service Ownership
Communication architecture also exposes organizational boundaries. When one team owns a service used by five others, that service becomes both a technical dependency and a coordination point. Changes to its contract, availability expectations, and deployment practices affect people as much as machines.
Clear ownership reduces that friction. Consumers should know who maintains an API, what reliability they can expect, how breaking changes are introduced, and where incidents are communicated. Providers should know which contracts are public commitments and which interfaces remain internal implementation details.
Without this discipline, service boundaries can become organizational traps. Teams create independent deployments but remain dependent on informal knowledge, undocumented behavior, and synchronized releases. The architecture appears distributed, while the organization still behaves like a single tightly coupled application.
Good cloud-native communication supports independent ownership because contracts are stable enough for teams to work without constant coordination. The real test of a service boundary is not whether it has its own repository. It is whether the teams on either side can change safely without summoning the entire council.
The Invisible Spellbook: Keeping the Network Visible
Modern frameworks provide clients, proxies, service meshes, SDKs, generated code, and platform abstractions that make remote calls easier to write. These tools are useful because teams should not rebuild transport infrastructure for every service.
The danger appears when convenience erases awareness. If a remote call looks exactly like a local method call, developers may forget meaningful timeouts. Automatic retries may multiply traffic without the application realizing it. Generated clients can hide network round trips inside code that appears harmless.
Useful abstractions remove repetitive work while preserving important consequences. Engineers should still know which calls cross service boundaries, how long they may wait, what happens when they fail, and whether they are safe to repeat. Cloud-native architecture rewards teams that make communication easier without pretending it is free.
Roads Worth Maintaining: Connecting the Distributed Kingdom
The journey through The Kingdom in the Clouds began by making applications portable. Containers packed software into repeatable environments, Compose connected small groups of workloads, Kubernetes managed growing populations of services, and cloud infrastructure moved those workloads beyond direct hardware ownership. Serverless architecture then pushed abstraction even further.
Connecting those pieces reveals the next layer of distributed engineering. Services must find one another, contracts must survive independent change, traffic needs controlled entrances and balanced destinations, and network failures require deliberate handling. REST and gRPC provide different forms of conversation, while service discovery, gateways, and load balancers help those conversations reach the right destinations.
None of those mechanisms removes the central fact that every network boundary introduces uncertainty. Good distributed architecture does not try to eliminate that uncertainty. It makes the uncertainty explicit, bounded, and survivable.
That is the deeper lesson behind the roads through the sky. Connecting cloud-native services is not about making every part of the kingdom able to reach every other part. It is about designing communication carefully enough that distance does not quietly become dependency, latency does not become paralysis, and failure does not travel farther than necessary.
Next week, The Kingdom in the Clouds enters The Distributed Realm, where distance changes rules that once seemed obvious. The roads will remain important, but we will no longer assume that every sender should stand waiting for a reply. On Monday, we continue with Messenger Ravens and Royal Couriers: Building with Messages and Events, where communication becomes asynchronous and services begin learning how to cooperate without sharing the same moment.


