API Versioning, Deprecation and Backward Compatibility Questions
Evolving an API without breaking existing consumers: versioning strategies (URI, header, media-type), distinguishing breaking from non-breaking changes, deprecation policies and sunset timelines, and coordinating migrations at scale. Covers communicating changes, supporting multiple versions concurrently, and compatibility guarantees across heterogeneous clients.
Propose a layered approach to detect accidental breaking changes introduced by PRs. Include static OpenAPI diffs, consumer-driven contract tests, integration tests in ephemeral environments, and runtime compatibility checks. Explain how these checks fit into CI/CD, how to handle intentional breaking changes, and how to reduce false positives.
Sample Answer
Layered approach (short summary)
- Fast static diff of OpenAPI on PR
- Consumer-driven contract (CDC) tests run by consumers against provider mocks
- Integration tests in ephemeral environments (PR preview)
- Runtime compatibility checks (shadowing, schema validation, telemetry)
Details and CI/CD placement
- Stage A — Pre-merge, quick checks (fast fail):
- OpenAPI static diff: run openapi-diff (or Swagger/OpenAPI diff tool) as a CI job on PR changes. Fail for breaking diffs by default, but emit human-readable report (endpoints, status/codes/schema changes).
- Linting & semantic-version hinting: ensure CHANGELOG/semver annotation present.
- Stage B — Consumer-driven contract tests (PR + nightly):
- Use Pact or Spring Cloud Contract. Consumers publish contracts to a Broker; provider CI pulls consumer contracts and runs verification against provider mock or unit-slice. Run in PR pipeline when providers change; run consumer-side tests when consumer PRs change.
- Stage C — Ephemeral integration environments (pre-merge merge pipeline or gated job):
- Spin ephemeral namespace (k8s) using the PR image(s), deploy dependent services as mocks or their latest images. Run integration suites and end-to-end scenarios with realistic test data. Use test-data isolation, seeded fixtures, and DB snapshots.
- Stage D — Runtime compatibility & observability (post-deploy):
- Shadow production traffic (traffic mirroring) to the new deployment without impacting users; validate responses against OpenAPI schema and contract expectations in real time.
- Enable schema validation middleware, log violations, alert if rate exceeds threshold.
- Add sampling and dashboards (Grafana) and contract violation metrics.
Handling intentional breaking changes
- Require an explicit “breaking-change” flag in PR and a linked design doc/migration plan with:
- API version bump or new URL namespace (semantic versioning).
- Deprecation headers and transition window (e.g., 3 months).
- Consumer migration checklist and opt-in feature flags.
- Approval gates: product + architecture + impacted-consumer owners sign-off.
- If flagged, openapi-diff should not auto-fail but must require documented justification; CDC runs to surface affected consumers; schedule coordinated rollout and timeline.
Reducing false positives / noise
- Baseline comparison: compare diffs against the approved mainline OpenAPI rather than transient generated files; ignore formatting-only changes.
- Tolerances & whitelists: allow safe non-breaking changes (descriptions, examples) in diff config; permit benign status-code additions under configurable rules.
- Consumer test stability: use pinned contract versions for deterministic verification; retry/flaky test handling and quarantine lists with tickets.
- Test data determinism: seed DBs, use deterministic IDs, mock external calls to eliminate network flakiness.
- Progressive enforcement: start by reporting (non-blocking) for new rules, measure false-positive rate, then tighten to block after threshold.
- Use semantic analysis: flag only changes that alter request/response shape, required fields, or status semantics, not cosmetic edits.
Operational considerations & trade-offs
- Faster developer feedback requires static diffs and local contract verification; heavier E2E runs increase CI time—mitigate via parallelization, test selection, and caching images.
- Ephemeral envs cost more; use them for important PRs or nightly gates and sample lower-critical changes.
- Maintaining Pact/Broker and contract ownership adds overhead but greatly reduces missed consumer impact.
- Shadow traffic is safest for production verification but requires careful traffic control and privacy considerations.
Example toolchain suggestions
- OpenAPI diff: openapi-diff, Swagger-diff, or custom script with json-schema-diff
- CDC: Pact + Pact Broker or Spring Cloud Contract
- Orchestration: GitHub Actions / GitLab CI / Jenkins for pipelines; Kubernetes + Helm/Kustomize for ephemeral envs
- Runtime: Envoy/NGINX mirroring, JSON Schema validators, Prometheus + Grafana, Sentry/logging for violations
Outcome
This layered, risk-tiered pipeline catches most accidental breaking changes early (static/contract), validates in realistic environments (ephemeral E2E), and confirms real-world compatibility post-deploy (shadowing + metrics), while providing controlled, auditable paths for intentional breaks and minimizing false positives through baselining, whitelists, and progressive enforcement.
Discuss trade-offs between contract-first API design (OpenAPI/Protocol Buffers) and code-first approaches for a platform supporting many clients. Cover developer velocity, backward compatibility, SDK generation, governance, and testing. Provide a recommendation on when an organization should adopt contract-first and how to transition if needed.
Sample Answer
Contract-first (define OpenAPI/Protobuf before implementation) vs code-first (implement then derive contract) each fit different stages and priorities. For a platform with many clients, trade-offs center on consistency, velocity, and long-term maintenance.
Developer velocity
- Code-first: faster for small teams or exploratory APIs — engineers iterate in-language and push features quickly.
- Contract-first: upfront cost (schema design, reviews), but prevents rework by surfacing design decisions early and speeds large-team coordination.
Backward compatibility
- Contract-first: explicit schema evolution rules (field deprecation, semantic versioning) make compatibility predictable; tooling can enforce breaking-change checks.
- Code-first: breaking changes often leak into clients because contracts are implicit; harder to audit history.
SDK generation
- Contract-first: first-class advantage — OpenAPI/Protobuf enable consistent, language-specific SDKs, reducing client implementation effort and bugs.
- Code-first: auto-generated contracts vary in quality; may require hand-maintained SDKs, increasing drift.
Governance
- Contract-first: enables governance (linting, policy checks, approval pipelines) and a single source of truth for product and security reviews.
- Code-first: governance is ad hoc; harder to standardize across teams and enforce cross-cutting policies.
Testing
- Contract-first: facilitates contract testing (consumer-driven contracts, schema validation), mock servers, and CI checks before implementation; integration tests become more reliable.
- Code-first: tests focus on runtime behavior and often miss contract-level mismatches early.
Recommendation
Adopt contract-first when you have multiple teams/clients, need stable public APIs, require SDKs, or must enforce security/compliance. For internal, fast-evolving prototypes, code-first may be acceptable short-term.
Transition plan
- Start with high-value APIs: pick public or widely-used endpoints to define contracts first.
- Introduce tooling: OpenAPI/Protobuf linters, schema CI checks, breaking-change diff tools, and SDK generators.
- Establish governance: contract review process, versioning strategy (semver for endpoints, protobuf field rules), and deprecation policy.
- Incremental migration: generate contracts from existing services where possible, add contract tests and mock servers, and publish SDKs alongside old clients.
- Educate and pilot: run workshops, create templates/standards, and require contracts for new APIs; measure client adoption and reduce manual SDK maintenance over time.
This approach balances initial productivity loss against long-term scale, reliability, and reduced client friction — critical for a platform serving many clients.
Compare rate-limiting algorithms: fixed-window, sliding-window, token-bucket, and leaky-bucket. Explain how each behaves under bursty traffic, accuracy in distributed deployments, implementation complexity, and which use cases each suits best (e.g., per-user bursts vs steady throughput caps).
Sample Answer
Brief summary: each algorithm enforces rate limits differently — trade-offs are burst handling, accuracy across nodes, and implementation complexity. Below I compare fixed-window, sliding-window (counter or log), token-bucket, and leaky-bucket across the requested dimensions and recommend use cases.
Fixed-window
- Bursty traffic: allows bursts at window boundaries (e.g., close to 2x if requests fall at both ends).
- Distributed accuracy: simple but suffers from boundary anomalies unless centrally coordinated; approximate when sharded.
- Complexity: very low (per-key counter + timestamp).
- Best for: coarse limits where occasional bursts are acceptable (e.g., daily quotas, coarse API tiers).
Sliding-window (counter or rolling window + interpolation)
- Bursty traffic: smooths boundary effects; provides more accurate per-window counts than fixed-window.
- Distributed accuracy: better than fixed-window if implemented with a central store or windowed counters; distributed implementations require more coordination or probabilistic approximations.
- Complexity: moderate (store multiple sub-window counters or timestamps).
- Best for: per-user burst control with stronger fairness across window boundaries.
Token-bucket
- Bursty traffic: naturally allows bursts up to bucket capacity then enforces long-term rate via refill rate — ideal for controlled bursts.
- Distributed accuracy: needs synchronized state (central store or consistent hashing + local buckets with reconciliation); otherwise approximate.
- Complexity: moderate (maintain tokens, refill logic, capacity).
- Best for: per-user/connection bursts with steady long-term throughput (upload bursts, API rate + burst allowance).
Leaky-bucket (as queue or fixed drain)
- Bursty traffic: smooths bursts by queuing and draining at fixed rate; can drop or delay excess.
- Distributed accuracy: like token-bucket, needs coordinated queue semantics for strict global guarantees; easier to reason about steady throughput.
- Complexity: moderate to high if queuing requests; simple if implemented as counter/drain.
- Best for: enforcing constant throughput (eg. network egress shaping, background job consumption).
Recommendations for solutions architect:
- If you need simple, low-cost limits per tenant: fixed-window with a central cache and conservative limits.
- If you want fair smoothing across windows: sliding-window counters (or hybrid sub-windows).
- If you need burst allowance but steady long-term rate: token-bucket (common choice).
- If you need strict steady output/queueing: leaky-bucket.
Distributed deployments: prefer a central rate-store (Redis with INCR+TTL or Lua), or use local token-buckets combined with periodic token reconciliation or a distributed algorithm (consistent hashing + sticky routing) to balance accuracy vs latency.
Design a rolling deployment strategy that serves both old and new API implementations concurrently to enable gradual client migration. Requirements: percentage-based traffic routing (canary), sticky sessions when necessary, per-version observability, rollback path on error thresholds, and coordination with data schema changes. Describe how you'd implement this using Kubernetes, Istio/Envoy, and an API gateway.
Sample Answer
Clarify requirements: gradual percentage-based routing, optional sticky sessions, per-version telemetry, automatic rollback on error thresholds, and safe schema migration. Proposed components: Kubernetes for workloads, Istio/Envoy for L7 traffic routing & telemetry, an API gateway (e.g., Kong/Apigee/NGINX) for public ingress, and Flagger + Prometheus + Jaeger for canary automation and observability.
High-level design:
- Deploy two versions of the service in K8s: v1 and v2. Use Deployment labels: app: api, version: v1|v2.
- API Gateway handles auth, rate limits, provides public URL and can perform path or header-based traffic splitting initially for coarse control.
- Istio VirtualService + DestinationRule split traffic by weight to k8s service subsets (v1/v2). Envoy provides session-affinity options (cookie or consistent-hash) for sticky sessions that target subset metadata.
Automated canary + rollback:
- Use Flagger with Istio provider. Flagger will incrementally shift weights (e.g., 1%, 5%, 25%, 50%...) and evaluate metrics from Prometheus (latency, 5xx rate, error budget) and tracing from Jaeger. On breach of configured thresholds, Flagger triggers an automatic rollback to v1.
- Define SLOs and error thresholds (e.g., 5xx rate >1% for 5m) in Flagger.
Per-version observability:
- Instrument app to include version metadata in metrics and traces (span/tag: service.version). Prometheus metrics include job/instance/version labels; Grafana dashboards and Kiali show service mesh flows per version. Logs enriched with version labels sent to ELK/EFK.
Sticky sessions example (cookie-based hash) via Istio/Envoy (VirtualService snippet):
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: api
spec:
hosts: ["api.example.com"]
http:
- match:
- headers:
end-user:
exact: "internal-client"
route:
- destination:
host: api.svc.cluster.local
subset: v2
weight: 20
- destination:
host: api.svc.cluster.local
subset: v1
weight: 80
retries:
attempts: 2
perTryTimeout: 2s
headers:
response:
set:
Set-Cookie: "route=v2; Path=/; HttpOnly"
(Use Envoy cookie-based session affinity in DestinationRule or an EnvoyFilter when precise hashing is needed.)
Schema change coordination:
- Prefer backward- and forward-compatible changes: additive fields, nullable columns, versioned endpoints.
- Use dual-write pattern or expand-and-contract migrations:
- Expand DB (add columns, new tables).
- Deploy v2 that writes new schema but can read old and new (dual-read if needed).
- Migrate consumers gradually (canary clients).
- Once traffic on v1 is drained and no reads depend on old schema, run contract cleanup.
- Use feature flags to toggle new behavior; gate DB writes until canary passes.
Operational playbook:
- Pre-check: health checks, readiness probes, baseline metrics.
- Canary run: Flagger increments weights; monitor real-time dashboards and distributed traces.
- On success: promote v2 to 100% via Istio/VirtualService or allow Flagger to complete rollout and update Kubernetes Service selector to v2.
- On failure: Flagger triggers rollback; run postmortem; if schema-incompatible change caused failure, immediately stop writes from v2 (feature flag), revert app or DB migration per rollback playbook.
Trade-offs & notes:
- Istio-based routing gives fine-grained control and telemetry but adds complexity & resource overhead.
- Use API Gateway for cross-cutting concerns (auth, rate-limit) and Istio for internal traffic splitting.
- Automate with Flagger/Argo Rollouts to reduce human error; ensure robust alerting and runbooks for DB rollbacks (schema changes are hardest to reverse).
This approach ensures gradual client migration, sticky sessions when needed, per-version observability, automated rollback on thresholds, and safe coordinated schema changes.
Define a contract-testing strategy to catch breaking API changes before they reach production. Include consumer-driven contract testing, provider verification, schema diffing, CI gating, and a contract registry. Discuss tooling options (e.g., Pact, OpenAPI-diff), CI integration patterns, and how to reduce noise and false positives.
Sample Answer
Requirements & goals:
- Prevent breaking API changes from reaching production by verifying consumer expectations against provider behavior early in the pipeline.
- Fast feedback for consumer teams, low false positive rate, traceability of who broke what.
Strategy (high-level):
-
Consumer-driven contracts (CDC)
- Consumers write contracts that capture expected requests/responses for critical flows.
- Use Pact (or Pactflow for enterprise) so consumers publish pacts to a contract registry/broker on merge of consumer PR.
- Enrich contracts with metadata: consumer name, provider name, version, tags (branch, env), and intent (critical/non-critical).
-
Provider verification
- Provider CI pulls pacts from the broker (tagged with main/branch) and runs provider verification tests against a provider instance (ideally in a test container or test environment).
- Verification results (pass/fail, logs) are pushed back to the broker for visibility and auditing.
-
Schema diffing & linting
- Maintain canonical OpenAPI/JSON Schema for provider surface. Use openapi-diff (or swagger-diff, OpenAPI-diff) in PRs to detect incompatible changes (e.g., removed endpoints, required property additions).
- Run Spectral and schema validators to catch style/semantic problems early.
-
CI gating patterns
- Consumer PR: run unit + contract generation tests; upon success, publish pact to broker under branch tag (non-blocking for consumers).
- Provider PR: run unit tests, then run provider verification against pacts tagged for target branch. Fail PR if verification fails for critical pacts (blocking).
- Gate merges to main if openapi-diff reports breaking changes or if provider verification fails for main-tagged pacts.
- For cross-team feature work, use ephemeral environments / provider mocks for end-to-end validation before merging.
-
Contract registry / governance
- Use a broker (Pact Broker, Pactflow) or central registry that stores pacts, verification statuses, and supports tagging/promoting contracts across environments.
- Maintain a policy: only promoted (verified) contracts are trusted for production deployments.
Tooling options (recommended):
- Pact + Pact Broker / Pactflow: mature CDC ecosystem, verification reporting, web UI, tagging, matrix, broker webhooks.
- openapi-diff (or swagger-diff): PR-time breaking change detection on provider OpenAPI specs.
- Spectral: linting OpenAPI for best practices.
- Mock servers: pact-mock-service, WireMock (for broader protocol support).
- CI: GitHub Actions / GitLab CI / Jenkins pipelines to orchestrate publish/verify steps and invoke diff tooling.
Reducing noise & false positives
- Prioritize: require blocking verification only for high-risk/critical consumer contracts; non-critical contracts produce warnings.
- Semantic versioning: bump major for breaking changes; use openapi-diff to confirm expected bump. Automate a checklist that enforces version bump when breaking diffs are detected.
- Tagging & environments: consumers publish branch-tagged pacts; provider verifies only relevant tags to avoid coupling unrelated work.
- Contract TTL / expiry: expire stale contracts and require re-verification after X days.
- Flakiness handling: retry small number of times, capture request/response logs, attach traces; mark flaky tests and create tickets rather than blocking indefinitely.
- Granular assertions: encourage consumers to assert only what they rely on (avoid asserting entire body if only a field is used).
- Mock-level validation: use schema validation and example-based tests to avoid brittle matching rules.
- Automated triage: when diff/verification fails, generate a triage ticket with failing pact, consumer, and provider logs to accelerate resolution.
Operational considerations & trade-offs
- Cost vs safety: full verification matrix (all consumers × all provider versions) is safest but costly. Use tag-based promotion to reduce scope.
- Developer experience: keep consumer publish quick and non-blocking; provide clear failure messages and a contract debugging page in the broker.
- Backward compatibility: prefer additive, optional changes; when breaking changes required, publish migration guidance and deprecate endpoints for a defined window.
Example CI flow (concise)
- Consumer PR merge to feature branch → run tests, publish pact to broker with branch tag.
- Provider PR opens → CI pulls pacts tagged for branch/main as appropriate → run provider verification job against test instance → if critical failures, block merge.
- When provider merges and promotion completes, provider CI re-verifies main-tagged pacts and promotes verification status in the broker.
Metrics & success criteria
- Time-to-detect (goal: within CI run)
- Number of production-breaking API incidents (goal: 0)
- Mean time to repair contract failures
- Percentage of pacts verified against latest provider
This strategy balances early consumer-driven detection (Pact), schema-based diffs for author-time safety (openapi-diff), gated provider verification in CI, and a contract registry for traceability — while reducing noise through tagging, prioritization, semantic versioning, and operational controls.
Unlock Full Question Bank
Get access to all API Versioning, Deprecation and Backward Compatibility interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.