Ashok G

System Design · Draft

15 — Services and APIs

Everything so far treated services as given. This chapter covers how to draw their boundaries, and how they talk.

Monoliths and microservices

A monolith is one deployable application. A microservice architecture splits it into many small services, each with its own data.

MonolithMicroservices
Local developmentSimpleHarder
DeploymentAll togetherIndependent
Calls between partsIn-processOver the network
TransactionsSimpleHard, as in chapter 13
Team independenceLowerHigher
Operational loadLowHigh

Microservices mainly solve an organizational problem: letting many teams ship independently. The cost is that each function call becomes a network call, with everything that entails: latency, partial failure, and tracing across services. For a small team, this is usually a poor trade.

A modular monolith is a good middle ground: one deployable application, with strict internal boundaries. It keeps the simplicity, and it makes splitting a piece out easier later.

Drawing boundaries. Good boundaries follow business capabilities, like payments or inventory. A warning sign is a distributed monolith, where services have to be deployed together. It has the costs of both and the benefits of neither. Services shouldn’t share a database, because that ties them together through the schema.

API styles

StyleFormatGood forWeak point
RESTJSON over HTTPPublic APIsOver-fetching
gRPCProtobuf over HTTP/2Internal servicesHarder from browsers
GraphQLClient-defined queriesComplex front endsCaching, query cost control
WebSocketTwo-way messagesLive updatesStateful connections

REST models resources with URLs and standard methods.

MethodMeaningIdempotent?
GETReadYes
PUTReplaceYes
DELETERemoveYes
POSTCreate or actNo
PATCHPartial updateNot necessarily

Status codes carry meaning, and clients rely on them. 429 means rate limited, 503 means try later, and a 400 won’t improve with a retry.

gRPC uses a strict schema and a compact binary format, and it generates client code. It also supports streaming and deadlines, which suits service-to-service calls.

GraphQL lets a client ask for exactly the fields it needs. That helps mobile apps, but one query can be arbitrarily expensive, so servers need depth limits and cost limits.

Designing APIs that last

Pagination

Returning every row isn’t an option for a large table.

MethodHowTrade-off
Offset?offset=1000&limit=50Simple, but slow at depth, and rows shift
Cursor?after=abc123Stable and fast, but no jumping to a page

Offset pagination makes the database read and throw away every skipped row. A cursor, based on an indexed column, goes straight to the right place, so it’s the better fit for large or changing data.

Versioning and compatibility

Public APIs are hard to change, because clients can’t all be updated at once. Adding optional fields is generally safe. Removing or renaming them isn’t.

Hyrum’s law says that with enough users, every observable behaviour will be depended on by somebody, including things never promised. Even error message text can become an API.

Internal services have the same problem during deployments, because old and new versions run side by side. So changes have to be backward compatible: add the new field, migrate users, and only then remove the old one.

Long-running operations

If an operation takes minutes, a client shouldn’t hold a request open. The usual pattern is to return 202 Accepted with a job ID that the client can check, or to notify it later through a webhook. Webhooks need retries, and signatures so that a receiver can verify the sender.

Rate limits and errors

Public APIs should report limits, with 429 responses and a Retry-After header, and return structured errors. Chapter 18 builds a rate limiter.

API gateways

An API gateway is a single entry point in front of services. It handles shared concerns such as authentication, rate limiting, routing, and TLS.

Keeping these in one place means services don’t each have to implement them. A variant is the backend for frontend, a gateway tailored to one client, such as a mobile app. The risk is a gateway that accumulates business logic until it becomes a bottleneck for every team.

Service discovery

Service instances come and go, so callers can’t use fixed addresses. Service discovery provides a registry of healthy instances. Kubernetes does this with internal DNS and virtual IPs, and tools like Consul do it elsewhere.

Service meshes

Timeouts, retries, encryption and metrics are needed on every service-to-service call. A service mesh moves these out of application code into a proxy beside each service, called a sidecar, such as Envoy. Newer designs handle more of this at the node level, using eBPF.

[Service A] <-> [Proxy] <--- mTLS ---> [Proxy] <-> [Service B]

This gives consistent policy across languages, at the price of extra latency, more resources, and a complex piece of infrastructure to run. For a handful of services, a shared library is usually enough.

Authentication and authorization

Authentication establishes who someone is. Authorization decides what they may do.

Sessions store state on the server, and a cookie holds an ID. They’re easy to revoke. Tokens such as JWTs are signed and self-contained, so services can verify them without a lookup. The trade-off is revocation: a token stays valid until it expires, so they’re usually short-lived and paired with a refresh token.

OAuth 2.0 lets a user grant an app limited access without sharing a password. OpenID Connect adds login on top of it.

Between services, mutual TLS proves each side’s identity (TLS guide).

Authorization is commonly role-based, as in “editors can publish”. Finer-grained needs often use attribute-based or relationship-based models, the latter popularized by Google’s Zanzibar.

A few security habits matter at any scale:

  • Check authorization on the server, for every object. Failing to is among the most common web vulnerabilities.
  • Never trust client input, and use parameterized queries.
  • Keep secrets out of code.
  • Rate-limit logins.

Conway’s law

Conway’s law observes that systems tend to mirror the communication structure of the organizations that build them. Service boundaries and team boundaries tend to converge, so reorganizing one without the other usually doesn’t work.

The takeaway

Microservices trade code complexity for operational complexity, and they pay off mainly for large organizations. Choose API styles by audience, plan for compatibility from the start, and use cursor pagination for large data. Put shared concerns at the gateway or in the platform, and check authorization on every object.