Careeroza
API Contracts
What is an API Contract?
An API contract is the promise a service makes to its callers: the URL, HTTP method, request body, response body, status codes and error format. Other teams code against this promise, so it must stay stable.
Contract
The product-service contract for one endpoint:
GET /api/products/{id}Example output
200 OK
{"id": 1, "name": "boAt Rockerz 450", "price": 1499.00, "stock": 120, "category": "Electronics"}
404 Not Found
{"type":"about:blank","title":"Not Found","status":404,"detail":"Product 99 not found"}Example: Coding Against a Contract
Example
The interface is the contract. Order logic depends only on ProductClient, so the provider can be a local class today and an HTTP client tomorrow, without changing the order code:
import java.math.BigDecimal;
import java.util.*;
public class Main {
public static void main(String[] args) {
// Two different implementations of the same contract
ProductClient local = new InMemoryProductClient();
ProductClient remote = new FakeHttpProductClient();
for (ProductClient client : List.of(local, remote)) {
OrderPricing pricing = new OrderPricing(client);
System.out.println(client.getClass().getSimpleName() + " -> total Rs "
+ pricing.total(Map.of(1L, 2, 2L, 1)));
}
}
}
record ProductResponse(Long id, String name, BigDecimal price) {}
/** The contract: what order-service needs from product-service. */
interface ProductClient {
ProductResponse getProduct(Long id);
}
class InMemoryProductClient implements ProductClient {
public ProductResponse getProduct(Long id) {
return id == 1L ? new ProductResponse(1L, "boAt Rockerz 450", new BigDecimal("1499.00"))
: new ProductResponse(2L, "Prestige Kettle", new BigDecimal("899.00"));
}
}
class FakeHttpProductClient implements ProductClient {
public ProductResponse getProduct(Long id) {
// In real code: restClient.get().uri("/api/products/{id}", id)...
String json = id == 1L ? "1|boAt Rockerz 450|1499.00" : "2|Prestige Kettle|899.00";
String[] f = json.split("\\|");
return new ProductResponse(Long.parseLong(f[0]), f[1], new BigDecimal(f[2]));
}
}
class OrderPricing {
private final ProductClient products;
OrderPricing(ProductClient products) { this.products = products; }
BigDecimal total(Map<Long, Integer> items) {
return new TreeMap<>(items).entrySet().stream()
.map(e -> products.getProduct(e.getKey()).price()
.multiply(BigDecimal.valueOf(e.getValue())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}Output
InMemoryProductClient -> total Rs 3897.00 FakeHttpProductClient -> total Rs 3897.00
Changing a Contract Safely
| Change | Breaking? | Why |
|---|---|---|
| Add a new optional field to a response | No | Old clients ignore unknown fields (Jackson does by default in Spring Boot) |
| Add a new endpoint | No | Nobody calls it yet |
| Rename or remove a field | Yes | Old clients read null or fail |
Change a field type (price number → string) | Yes | Deserialization fails |
| Make an optional request field required | Yes | Old callers get 400 Bad Request |
- Be tolerant when reading: ignore fields you do not know.
- Version breaking changes: keep
/api/v1/productsrunning while callers move to/api/v2/products. - Document the contract with OpenAPI (springdoc-openapi; see the Spring Boot material).
- Test it: consumer-driven contract tests (Spring Cloud Contract, Pact) check that a provider still satisfies its consumers.
Practice
Exercise: product-service wants to rename price to sellingPrice and add mrp. order-service and the mobile app read price. How do you roll this out without breaking anyone?
Show answer
1. Add the new fields, keep the old one (non-breaking):
{"price":1499.00, "sellingPrice":1499.00, "mrp":2990.00}
2. Tell consumers; they move to "sellingPrice" at their own pace.
3. Watch logs/metrics until no caller reads "price" any more
(or old app versions are retired).
4. Only then remove "price" - or publish /api/v2/products without it
and keep /api/v1 until it has no traffic.