API Design for Microservices
API Design for Microservices
When a microservice publishes an API it is not just writing code — it is publishing a contract. Every team that depends on that service trusts the contract to be stable. Breaking it silently is one of the fastest ways to bring down a distributed system at 2 AM. This lesson covers how to define contracts rigorously, how to evolve them without causing downtime, and how to think about backward compatibility as a first-class engineering concern.
What an API Contract Actually Is
An API contract is the complete, machine-readable description of what a service accepts and returns: the URL structure, HTTP methods, request bodies, response shapes, status codes, error formats, authentication requirements, and rate-limit headers. Contracts are more than documentation — they are the test oracle and the deployment gate.
In a Spring Boot 3 ecosystem the de-facto standard for REST contracts is OpenAPI 3 (formerly Swagger). Add springdoc-openapi-starter-webmvc-ui to your service and it generates the spec from your annotations automatically:
The generated JSON spec at /v3/api-docs becomes the contract artifact. You publish it to a schema registry (Confluent, AWS Glue, or a plain Git repository) and consumer teams pin their integration tests to it.
API Versioning Strategies
All APIs change. The only question is how you expose that change to consumers. There are four mainstream strategies, each with trade-offs:
- URI versioning —
/api/v1/orders,/api/v2/orders. Simple, highly visible, easy to route in an API gateway. The downside: you maintain N parallel route trees and clients must opt in explicitly. - Header versioning —
Accept: application/vnd.myapp.v2+jsonorX-API-Version: 2. Keeps URLs clean; harder to test in a browser or share as a link. - Query-parameter versioning —
/api/orders?version=2. Very discoverable but versions are cache-hostile and pollute URLs. - Content negotiation (media type). Most REST-theoretically correct, most operationally painful. Rarely used beyond large platform APIs.
For microservices behind a Spring Cloud Gateway the most maintainable approach is URI versioning combined with gateway routing. The gateway maps /api/v2/** to a new service deployment while /api/v1/** continues to serve the old one:
/api/v1 into a live service that shipped as /api/orders means coordinating a breaking change across every consumer at once. Adding /v1 upfront costs nothing and buys you unlimited future flexibility.
Backward Compatibility Rules
Backward compatibility means existing consumers keep working without any change on their side. The rules are simple to state but easy to violate accidentally:
Safe changes (non-breaking):
- Adding a new optional field to a response body.
- Adding a new optional query parameter.
- Adding a new endpoint (new route, same version).
- Relaxing a validation constraint (accepting more values than before).
- Adding a new enum value — only if consumers are expected to handle unknown values gracefully.
Breaking changes (require a new version):
- Removing or renaming a field.
- Changing a field's type (e.g.,
inttostring). - Making an optional field required.
- Changing the semantics of an existing field without renaming it.
- Removing an endpoint.
- Changing authentication/authorization requirements.
Enforce this automatically. In CI, use a tool like openapi-diff or Optic to compare the new spec against the published baseline and fail the build on breaking changes:
Designing for Consumers: Tolerant Reader & Postel's Law
Robustness in a distributed system demands that clients be tolerant readers: they must ignore fields they do not recognise and not crash on unknown enum values. This is Postel's Law applied to JSON APIs: "be conservative in what you send, liberal in what you accept."
In Jackson (which Spring Boot uses), configure tolerance globally so new response fields do not break old clients:
Versioning Internal vs External APIs
Not every API needs the same versioning discipline. Apply a risk-based approach:
- Internal service-to-service APIs: you own all consumers and can coordinate changes. Tolerate more churn; use feature flags and deploy consumers first (expand-contract pattern).
- APIs consumed by a mobile app: you cannot force a user to upgrade. Treat these as public APIs; maintain old versions for 12–24 months and sunset them with advance notice.
- APIs exposed through an API gateway to third-party developers: even stricter. Publish a deprecation policy (90-day notice minimum), a sunset header, and a migration guide before removing anything.
Spring Boot makes it straightforward to emit Deprecation and Sunset HTTP headers on old endpoints using a filter or response advice:
Error Contract Design
Errors are part of the contract too. Ad-hoc error responses ({"error": "something went wrong"}) force every consumer to write bespoke parsing logic. Use RFC 9457 Problem Details (application/problem+json), which Spring Framework 6 supports natively via ProblemDetail:
A standard error shape means consumer teams can write a single error-handling library and reuse it across all services in the platform.
Summary
API contracts are the load-bearing walls of a microservices architecture. Define them rigorously with OpenAPI 3, version them from the start using URI versioning, automate backward-compatibility checking in CI, and standardise error shapes with RFC 9457 Problem Details. Write clients as tolerant readers so safe changes on the provider side never cause cascading failures. These habits are what separate a maintainable platform from a distributed monolith held together by fear of change.