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.
| Monolith | Microservices | |
|---|---|---|
| Local development | Simple | Harder |
| Deployment | All together | Independent |
| Calls between parts | In-process | Over the network |
| Transactions | Simple | Hard, as in chapter 13 |
| Team independence | Lower | Higher |
| Operational load | Low | High |
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
| Style | Format | Good for | Weak point |
|---|---|---|---|
| REST | JSON over HTTP | Public APIs | Over-fetching |
| gRPC | Protobuf over HTTP/2 | Internal services | Harder from browsers |
| GraphQL | Client-defined queries | Complex front ends | Caching, query cost control |
| WebSocket | Two-way messages | Live updates | Stateful connections |
REST models resources with URLs and standard methods.
| Method | Meaning | Idempotent? |
|---|---|---|
| GET | Read | Yes |
| PUT | Replace | Yes |
| DELETE | Remove | Yes |
| POST | Create or act | No |
| PATCH | Partial update | Not 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.
| Method | How | Trade-off |
|---|---|---|
| Offset | ?offset=1000&limit=50 | Simple, but slow at depth, and rows shift |
| Cursor | ?after=abc123 | Stable 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.