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

ChangeBreaking?Why
Add a new optional field to a responseNoOld clients ignore unknown fields (Jackson does by default in Spring Boot)
Add a new endpointNoNobody calls it yet
Rename or remove a fieldYesOld clients read null or fail
Change a field type (price number → string)YesDeserialization fails
Make an optional request field requiredYesOld callers get 400 Bad Request
  • Be tolerant when reading: ignore fields you do not know.
  • Version breaking changes: keep /api/v1/products running 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.