Chapters

6 Service API Contracts: Versioning and Compatibility

design-patterns
soa
api

6.1 Start With the Decision

Old field devices may use one API for years. A new version must not break their commands.

6.2 Route Overview

This is part 2 of 2. Review Service API Contracts: Durable Device Interfaces for the preceding evidence.

6.3 Learning Objectives

  • Define versioning strategies with explicit inputs, errors, and change rules.
  • Validate references with a concrete scenario and pass criteria.

6.4 Chapter Roadmap

  • Versioning Strategies
  • Practical IoT Default
  • Compatibility Rules
  • Compatibility Test
  • Compatibility Change Review
  • Quiz: Versioning Compatibility
  • Lifecycle Headers
  • Service Discovery
  • Quiz: Service Discovery for Devices
  • Checkpoint: Versions and Discovery
  • Rate Limits and Retries
  • Do Not Hide Overload
  • Idempotent Commands
  • Try It: Retry Contract Card
  • Checkpoint: Retry Behavior
  • Worked Example: Smart Building API Review
  • Design Review Checklist
  • Common Pitfalls
  • Key Concepts
  • Label the Diagram
  • Code Challenge
  • Summary
  • Key Takeaway
  • Knowledge Check
  • Quiz: Rate Limits and Retry Behavior
  • Try It Yourself: Design an API Migration
  • Interactive Quiz: Match API Design Concepts
  • Interactive Quiz: Sequence the Steps
  • References
  • What’s Next
  • Navigation

6.5 Versioning Strategies

Use Figure 6.1 to decide whether a proposed change belongs inside the current contract or needs a parallel version. The visual is worth inspecting before choosing URL, header, or query versioning because transport syntax cannot rescue an undocumented compatibility or retirement policy.

Three API versioning choices for IoT service contracts: URL path versioning as the usual IoT default, header or media versioning for capable SDKs and internal clients, and query versioning for deliberate compatibility bridges; a compatibility gate separates additive changes from breaking changes that need a parallel version, migration, deprecation, and sunset
Figure 6.1: API versioning lifecycle choices for IoT service contracts

Read Figure 6.1 from the three entry choices toward the compatibility gate. Path versioning makes the contract obvious to constrained clients, header versioning suits capable managed clients, and query versioning is a deliberate bridge rather than a default. The gate is the important connection: additive changes may remain compatible, while breaking changes require migration, deprecation, and a recorded sunset.

6.5.1 URL Path Version

Example: /v1/devices

Best when clients are constrained, documentation must be obvious, and gateway routing should be simple.

6.5.2 Header Version

Example: Accept: application/vnd.example.v1+json

Best when clients are capable, URLs should stay clean, and content negotiation is already part of the platform.

6.5.3 Query Version

Example: /devices?version=1

Useful for compatibility in some systems, but easy to misuse and less clear as a long-term public contract.

Practical IoT Default

For device-facing HTTP APIs, URL path versioning is usually the clearest operational choice. Many embedded clients, diagnostics tools, gateways, and support workflows are easier to reason about when the version is visible in the path. Internal service APIs may choose header or package-based versioning if the organization can enforce client behavior.

6.6 Compatibility Rules

Use compatibility rules before deciding that a new version is required.

6.6.1 Usually Compatible

  • Add an optional response field.
  • Add a new endpoint.
  • Add an optional query parameter with the same default behavior.
  • Add an enum value only when clients are documented to tolerate unknown values.
  • Add a link, metadata object, or trace identifier.

6.6.2 Usually Breaking

  • Remove or rename a field.
  • Change a field type or unit.
  • Change default behavior.
  • Make an optional request field required.
  • Change resource identity or URL structure.
  • Change visible semantics while keeping the same shape.
Compatibility Test

Ask this question before changing an API: “Would an old client continue to parse, interpret, and act on the new response correctly without a firmware or app update?” If the answer is not clearly yes, treat the change as breaking.

6.7 Compatibility Change Review

Before merging an API change, record the exact contract change, the oldest supported client that must still work, and the compatibility proof. Include schema examples, status-code behavior, retry/idempotency effects, and the metric that will show whether old firmware, apps, SDKs, or partner integrations are still calling the old shape.

6.8 Lifecycle Headers

API versions need an exit path. A deprecation policy tells clients that a resource should no longer be chosen for new work. A sunset policy tells clients when the resource is expected to become unavailable.

HTTP/1.1 200 OK
Deprecation: @1767225599
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
Link: <https://example.com/>; rel="deprecation"; type="text/html"

1. Announce Publish a migration guide before the old endpoint becomes the wrong default.

2. Mark Return deprecation metadata on the old version while it still functions.

3. Measure Track which devices, apps, tenants, and integrations still call the old version.

4. Migrate Move firmware, apps, SDKs, and partner integrations to the replacement.

5. Remove Return a clear permanent failure only after the policy and exception path are complete.

Versioning handles the shape of the contract. Discovery handles where requests go after a client has chosen the contract.

6.9 Service Discovery

In dynamic infrastructure, service instances move, scale, and fail. Service discovery keeps clients from hardcoding addresses.

Inspect Figure 6.2 to separate the stable service contract from the changing set of runtime instances. This distinction matters to the chapter’s running argument because a client can depend on a service name and behavior without learning which instance happens to serve the next request.

Service discovery flow with service registration, lookup, and invocation
Figure 6.2: Service discovery flow: registration, discovery, and invocation

Follow Figure 6.2 in lifecycle order. A service instance first registers its location and health, a caller or routing component then discovers an eligible destination, and only then does invocation occur. Read the return path as operational evidence too: stale registration or failed health policy can break discovery even when both client and service code are otherwise correct.

6.9.1 Client-Side Discovery

The caller queries a registry, receives healthy instances, and chooses where to send the request.

Good fit for capable internal services that can cache, retry, and observe registry failures.

6.9.2 Server-Side Discovery

The caller sends traffic to a stable load balancer, gateway, or service name. Infrastructure chooses the backend instance.

Good fit for devices and simple clients that should not contain discovery logic.

Now inspect Figure 6.3 to see how the same lifecycle changes when the client is deliberately kept simple. The design question is no longer whether discovery exists, but which infrastructure component owns it and how that ownership preserves a stable endpoint for deployed devices.

Server-side discovery where a client calls a stable load balancer that routes to healthy service instances
Figure 6.3: Server-side discovery: Load balancer abstracts service location from clients

Read Figure 6.3 from the client to the load balancer and then across the healthy service instances. The client knows only the stable front door; the load balancer consumes health and location evidence before selecting a backend. This returns to the contract theme: placement and scaling can change without pushing registry logic or volatile addresses into constrained firmware.

6.9.3 Kubernetes Service

Provides a stable virtual endpoint for a set of Pods. Clients use the Service name while Kubernetes updates the backing endpoints.

6.9.4 API Gateway

Centralizes authentication, rate limits, routing, TLS termination, schema enforcement, and public endpoint stability.

6.9.5 Service Registry

Stores service instances and health status. Examples include Consul, Eureka, or registry systems built into orchestration platforms.

6.9.6 DNS and Global Routing

DNS and global routing are useful for public entry points, regional routing, and failover, but they should be paired with health checks and operational runbooks. Review the path in order: a stable name selects a region or gateway, health policy removes an unsafe destination, and the client observes TTL and retry rules before another lookup. The contract must also say what happens to active sessions and writes during failover. This keeps global discovery aligned with the chapter’s promise that clients depend on stable behavior rather than current instance addresses.

Blueprint BinaCheckpoint: Versions and Discovery

You now know:

  • URL path versioning such as /v1/devices is usually the clearest device-facing default.
  • Additive optional fields often stay compatible; removed fields, unit changes, default changes, or semantic changes usually need a new version.
  • Constrained devices should call a stable API gateway or DNS name while Kubernetes Services, EndpointSlices, Consul, Eureka, or internal routing move backends.

6.10 Rate Limits and Retries

IoT APIs must expect retry storms. Devices reconnect after outages, cellular networks drop packets, and firmware bugs can repeat requests too aggressively.

6.10.1 Per-Device Limit

Limits a single credential or device identity so one faulty device cannot consume the whole API.

6.10.2 Per-Tenant Limit

Protects shared infrastructure when one customer fleet behaves badly.

6.10.3 Global Limit

Protects the platform from overload and preserves headroom for control-plane recovery.

When a client exceeds a limit, return a specific response and a retry hint:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json

{
  "type": "https://example.com/",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Device exceeded its telemetry write limit.",
  "retry_after_seconds": 30,
  "trace_id": "req-7f2a"
}
Do Not Hide Overload

Returning 200 OK with an application-specific “try later” field teaches clients to ignore HTTP semantics. Use the status code, retry hint, and a structured error body so generic gateways, SDKs, and logs can understand the failure.

6.11 Idempotent Commands

Retries are normal in IoT. A command API should prevent duplicate execution when a device, app, or gateway retries after a timeout.

POST /v1/devices/d-17/commands
Idempotency-Key: 01J64P6RBXW3
Content-Type: application/json

{
  "type": "set_mode",
  "mode": "eco"
}

6.11.1 Server Responsibility

Store the idempotency key with the command result. If the same key is received again, return the original result instead of creating a second command.

6.11.2 Client Responsibility

Generate a stable key for one logical action. Reuse that key only for retries of the same action.

Try It: Retry Contract Card

Choose one command endpoint and write the retry contract before implementation:

  • Logical action: the exact command that one idempotency key represents.
  • First request result: status code, command identifier, and response body returned when the command is accepted.
  • Duplicate retry result: response returned when the same key arrives again after a timeout.
  • Retry boundary: how long the key is retained and when a client must create a new key.
  • Overload behavior: the 429, Retry-After, and problem-detail fields clients should obey.
  • Suppression signal: the metric or log entry that proves duplicate dispatch was suppressed.

Blueprint BinaCheckpoint: Retry Behavior

You now know:

  • Rate limits should exist at per-device, per-tenant, and global levels.
  • Overload should use 429 Too Many Requests, Retry-After, and application/problem+json instead of hiding failure inside 200 OK.
  • Idempotent command APIs store one Idempotency-Key per logical action so a retry returns the original result rather than creating a duplicate command.

The worked example reviews these pieces as one API surface.

6.12 Worked Example: Smart Building API Review

Scenario: A smart building platform manages sensors, HVAC controllers, occupancy zones, and alerts. The team wants one API surface for dashboards, automation rules, and firmware management.

1. Name Resources Use /v1/buildings, /v1/buildings/{buildingId}/zones, /v1/devices, /v1/devices/{deviceId}/telemetry, and /v1/devices/{deviceId}/commands.

2. Separate Query From Write Use telemetry query endpoints for history and a command endpoint for desired-state changes. Do not overload telemetry writes to also trigger control actions.

3. Add Compatibility Rules Allow optional fields for new sensor capabilities. Create a new version before changing units, field names, or command semantics.

4. Protect the Platform Rate limit by device identity and tenant. Return 429 with Retry-After for retryable overload.

5. Hide Internal Discovery Dashboards and devices call the API gateway. Internal services use Kubernetes Services or a registry behind the gateway.

The review result is not a bigger API. It is a smaller, clearer contract: stable resource paths, explicit versioning, predictable error behavior, and discovery choices hidden from clients that do not need them.

6.13 Design Review Checklist

6.13.1 Resource Shape

  • Are paths nouns rather than commands?
  • Are collection and item paths consistent?
  • Does the API hide database structure?
  • Are aggregate endpoints available where dashboards would otherwise be chatty?

6.13.2 Compatibility

  • Are additive changes clearly separated from breaking changes?
  • Are enums documented for unknown values?
  • Are default values stable?
  • Is each version tied to a migration and deprecation policy?

6.13.3 Operational Behavior

  • Are rate limits applied by device, tenant, and platform?
  • Do retryable responses include Retry-After?
  • Are commands idempotent?
  • Do error responses include trace IDs?

6.13.4 Discovery

  • Do devices see a stable gateway or DNS name?
  • Are internal service names managed by the orchestration platform?
  • Are health checks part of routing?
  • Can a region or instance fail without changing firmware?

6.14 Common Pitfalls

6.14.1 One Version Forever

Keeping one version while changing behavior breaks deployed devices quietly. Version breaking changes and publish a lifecycle policy.

6.14.2 Chatty Dashboards

A dashboard that calls every device one by one multiplies latency and load. Add list, filter, aggregate, and pagination patterns.

6.14.3 Registry Logic in Firmware

Firmware should not need to know Pod IPs, service registries, or cluster topology. Give devices stable public endpoints.

6.15 Key Concepts

  • REST API: A resource-oriented HTTP API using standard methods and status codes.
  • API contract: The stable combination of paths, methods, schemas, errors, lifecycle policy, and behavior.
  • Backward compatibility: The ability for old clients to keep working with a newer server.
  • Deprecation: A signal that a resource should no longer be chosen for new work.
  • Sunset: A signal that a resource is expected to become unavailable at a future time.
  • Service discovery: The mechanism that maps a stable service name or registry entry to healthy service instances.
  • API gateway: A stable entry point that can centralize routing, authentication, rate limits, and policy enforcement.
  • Idempotency key: A client-generated value that lets the server recognize retries of the same logical operation.

6.16 Summary

This chapter covered API design and service discovery for IoT service architectures:

  • Model APIs around stable resources and standard HTTP behavior.
  • Use new versions for breaking changes, not every additive change.
  • Communicate lifecycle with deprecation documentation and sunset dates.
  • Keep constrained devices behind stable gateways or DNS names.
  • Use service discovery inside the platform where clients can handle it.
  • Add rate limits, retry hints, and idempotency to survive real network behavior.
Key Takeaway

In one sentence: an IoT API should let services evolve without forcing every deployed device, dashboard, and partner integration to change at the same time.

6.17 Knowledge Check

Design a migration for an IoT building platform that must replace /v1/devices/{id}/telemetry with a new schema.

Context:

  • Existing firmware can update, but not all devices connect every day.
  • Dashboards and automation rules read the same telemetry API.
  • The new schema adds quality flags and changes the unit representation.
  • Operators need to know which clients still call the old version.

Tasks:

  1. Choose a versioning strategy for the device-facing API.
  2. Classify each schema change as compatible or breaking.
  3. Define deprecation and sunset signals.
  4. List the usage metrics you need before removal.
  5. Decide whether devices should call a gateway, registry, or direct service endpoint.

Deliverable: A one-page migration plan with endpoint names, timeline, client communications, and rollback criteria.

6.18 References

6.19 What’s Next

6.19.1 Build Fault-Tolerant Services

SOA Resilience Patterns

Next, connect API behavior to circuit breakers, timeouts, bulkheads, retries, fallbacks, and failure isolation.

6.19.2 Deploy the Service Platform

SOA Container Orchestration

Use containers, Kubernetes, observability, and service routing to run the APIs behind the contracts.

6.19.3 Revisit Service Boundaries

SOA and Microservices Fundamentals

Use API evidence to decide whether a boundary should remain internal, become a service, or be split further.

6.19.4 Review Reference Models

IoT Reference Models and Patterns

Map the API layer to device, edge, platform, application, and operations responsibilities.

6.21 Continue Your Route

This final part closes the route from Versioning Strategies through Navigation. Return to Service API Contracts: Durable Device Interfaces or continue from the design-patterns module index.