API Contracts and Schema Design Questions
Defining the interface contract between producers and consumers: request/response payload shapes, data models, field-level validation, nullability, and enum/typing decisions (including safely evolving an enum's allowed values without breaking clients). Covers contract-first design with OpenAPI and JSON Schema (authoring specs, generating SDKs and mock servers, catching breaking changes in CI), mapping internal domain models to external DTOs as both evolve independently, and designing a stable error-response contract (structured error codes, correlation IDs, retryable classification). Also covers the leadership and behavioral practice of establishing and governing contract standards across teams. The contract is the durable artifact clients depend on.
How do you lead API and schema governance across multiple engineering teams without becoming a bottleneck? Describe the standards, review rituals, exception process, and coaching mechanisms you would put in place so teams can move quickly while still protecting contract quality and data integrity.
Sample Answer
The mechanism that actually scales is making the RIGHT PATH the fast path: most schema changes should be able to pass through lightweight, automated checks with no human review bottleneck at all, reserving deliberate human review for the genuinely risky category of changes, and building a clear, fast exception process for the inevitable case that does not fit the standard.
Standards
Write down, concretely, what counts as additive (safe, no review needed beyond automated checks) versus what needs review (removing or retyping a field, tightening a validation rule, anything that could break an existing consumer). Vague standards ("use good judgment") do not scale past a handful of teams; specific, automatable rules do.
Review rituals
Reserve actual human review time for the standard's genuinely risky category, not for every schema change. A short, regular forum (a 30-minute weekly API-design review, not a per-PR gate) where teams bring proposed breaking changes or genuinely novel contract designs keeps the review load proportional to actual risk instead of proportional to total change volume.
The exception process
Every governance model eventually meets a team with a legitimate reason to deviate (a genuine deadline, a design the standard did not anticipate). The exception process needs to be fast and lightweight, or teams will route around the standard entirely rather than use it; a same-day escalation path to a small, named decision-making group, with a requirement to document the exception and revisit it later, keeps the standard's credibility intact without becoming an unconditional blocker.
Coaching mechanisms
Governance that only shows up as a gate at review time teaches people to satisfy the gate, not to internalize the underlying judgment. Pairing the standard with office hours, a small set of worked examples showing WHY a rule exists (not just what it says), and reviewing an early draft with a team before their PR is nearly done all shift the standard from "the thing that blocks my merge" to "something that helped me design this well before I'd invested a week in a shape that needed to change."
Worked example
An organization adopts an automated OpenAPI-diff check as the default gate: additive changes merge with zero human involvement, and the CI (continuous integration) check specifically flags anything it classifies as breaking, routing that PR to a lightweight, asynchronous review queue rather than a scheduled meeting. A team proposing a genuine breaking change (removing a deprecated field two quarters after announcing the deprecation) posts it to the weekly review forum with the automated diff attached; the forum approves it in five minutes because the actual analysis (is this really safe, has the deprecation window passed) was mostly done automatically already, and the meeting exists to catch judgment calls the automation cannot make, not to re-derive facts the CI check already established.
Trade-offs and pitfalls
The classic failure mode is a governance model that reviews everything with equal weight, which either becomes a bottleneck teams learn to route around (shipping through side channels, or simply not asking) or burns out the reviewers, who end up rubber-stamping routine changes because there is too much volume to give genuinely risky ones real attention. The other common failure: an exception process so slow or so poorly documented that teams stop using it and just break the rule quietly instead, which is worse than either following it or having a visible, tracked exception.
How should an API's error contract let a client tell a retryable failure (for example, a downstream dependency outage) apart from a non-retryable one (a validation error, an authorization failure)? Walk through the response shape, the HTTP status codes you would map to it, and what you would expose for internal observability.
Sample Answer
The response shape needs a field a client can check WITHOUT having to remember which HTTP status codes are retryable, because that mapping is exactly the kind of implicit knowledge that drifts out of sync between server and client over time. An explicit retryable: true/false field in the error body, backed by a consistent status-code mapping, is more robust than expecting every client to have memorized "5xx generally means retry, 4xx generally doesn't, except for 429."
The response shape
Every error response includes: an error code (a stable string), a retryable boolean, and where relevant a suggested retry_after hint (for a rate-limited or temporarily-unavailable case). A downstream dependency outage returns retryable: true; a validation failure or an authorization failure returns retryable: false, explicitly, rather than leaving the client to infer it from the status code alone.
Status-code mapping
Downstream outages and other transient failures map to 503 (Service Unavailable) or 504 (Gateway Timeout); validation errors map to 400; authorization failures map to 401 or 403. The important discipline is consistency: the same underlying failure category should always produce the same status code and the same retryable value, every time, so a client's retry logic can be written once against the CONTRACT rather than against the behavior of one specific endpoint it happened to test against.
Internal observability
Every error response, retryable or not, should be logged internally with its correlation ID, error code, and enough context (which downstream dependency failed, if any) to build a dashboard distinguishing "we are seeing elevated retryable failures from dependency X" from "clients are sending an elevated rate of invalid requests," which are operationally very different signals requiring different responses from the team.
Worked example
A checkout API depends on an inventory service. When the inventory service times out:
{
"error": {
"code": "inventory_service_timeout",
"retryable": true,
"retry_after_seconds": 2
}
}
mapped to HTTP 503. When a client submits an order with a negative quantity:
{
"error": {
"code": "invalid_quantity",
"retryable": false
}
}
mapped to HTTP 400. A client's retry logic checks error.retryable directly (and respects retry_after_seconds when present) rather than maintaining its own hardcoded table of "which status codes are safe to retry," which is exactly the kind of client-side assumption that drifts out of sync with server behavior as the API evolves.
Trade-offs and pitfalls
Relying purely on HTTP status code conventions without an explicit retryable field forces every client to independently encode assumptions about which codes mean what, and those assumptions silently diverge across different client implementations over time. The opposite risk, marking something retryable that actually is not (a validation error mistakenly flagged retryable) sends a client into a pointless retry loop that will never succeed and burns capacity on both sides for no benefit, so the classification itself needs to be treated as a correctness-critical part of the contract, not an afterthought bolted onto whatever status code was already being returned.
Walk me through a time you helped your team improve API or data-model consistency by introducing a review practice, template, or standard. How did you get buy-in, and what measurable outcome showed the change was worth it?
Sample Answer
The strongest version of this story treats "getting buy-in" as its own real problem to solve, not an afterthought after the standard was written: a review checklist or template that nobody asked for and that adds friction to every pull request will be quietly ignored, no matter how sound its content is, unless the people adopting it were part of shaping it or can see a concrete problem it actually prevented.
What a strong answer walks through
The recurring problem that motivated the standard. Name a specific, repeated pain point (three separate incidents where a field was renamed without warning, or a recurring pattern of inconsistent error shapes across services making client-side error handling brittle), not an abstract desire for "more consistency."
How you got buy-in, specifically. Did you pilot the template on your own team first and bring results, not just a proposal, to a wider forum? Did you involve the engineers who would actually use it in drafting it, so it reflected real workflow rather than an ideal imposed from outside? Buy-in earned through a concrete, already-demonstrated win is far more durable than buy-in secured by a mandate from above.
The measurable outcome. A credible answer names something you could actually point to: a drop in a specific class of production incident, a measured reduction in review-comment cycles for API design discussions, or an increase in some adoption metric (percentage of new endpoints using the reviewed template) tracked over a real time window, not a vague "things got better."
Worked example shape
"We had three separate incidents in one quarter where a field was silently renamed or retyped in an API response, each caught only after a client broke in production. I proposed a lightweight schema-change checklist as part of the pull request template, but rather than mandating it top-down, I piloted it on my own team's PRs for a month first, tracking how many schema-affecting changes it actually caught before merge. It caught two would-be breaking changes in that pilot month alone. I brought that concrete result, not just the proposal, to the broader engineering review, and adoption followed because people could see it had already prevented two real incidents rather than being a hypothetical improvement. Six months after wider rollout, breaking-change incidents traced to unreviewed schema changes dropped from three in the prior quarter to zero."
Trade-offs and pitfalls
A common weak point is describing the STANDARD in detail while glossing over how buy-in was actually secured, which is usually the harder and more interesting half of this story; a checklist's content is rarely the reason adoption succeeds or fails. Another common weak spot: an outcome metric that is really just "adoption happened" rather than a downstream result (fewer incidents, faster reviews) the standard was actually meant to produce; adoption is a leading indicator, not the outcome itself.
Compare JSON and Protocol Buffers (protobuf) as serialization formats for APIs. Discuss trade-offs across latency, payload size, schema evolution, developer ergonomics, client diversity, human-readability, and tooling. Provide scenarios where you would choose JSON for a public REST API and where you would choose protobuf for internal, high-throughput communication, and why.
Sample Answer
A strong first answer: pick JSON when you need human-readable, browser-friendly, loosely-coupled contracts (a public REST API, a webhook payload, anything a developer might read in a curl response), and pick Protobuf when you control both ends of the wire (the wire = the literal bytes sent over the network, not the source code or an in-memory value) and need small, fast, strongly-typed messages (internal gRPC calls [gRPC: a common RPC framework built on Protobuf], high-throughput or high-volume traffic between your own services).
The axes that actually decide it
Payload size and latency. Protobuf is a binary, tag-length-value format: field names never travel on the wire, only small integer field numbers do, and values are packed (varints -- a compact encoding that uses fewer bytes for smaller integers -- for integers, no quoting for strings). JSON repeats every field name as a string on every message. For small, frequent messages the difference in serialization/deserialization cost and wire size adds up.
Schema evolution. Schema evolution is where the format choice matters most, because API contracts keep changing long after they first ship, and the two formats handle that change very differently. Protobuf ties every field to a stable field NUMBER, not to its position or name: a reader that does not recognize a field number simply skips it, so adding a new field is safe by construction, and renaming a field is free (the name is compile-time only, never on the wire). The one hard rule is that a field number must never be reused for a different meaning once it has shipped. JSON has no built-in schema at all; "schema evolution" for JSON is really "JSON Schema evolution," and safety depends entirely on the discipline the team layers on top (never treating a field as positionally required, always tolerating unknown fields).
Developer ergonomics and human-readability. JSON wins here outright: you can read it in a browser network tab, log it, curl it, and paste it into a bug report with zero tooling. Protobuf messages are opaque bytes without the compiled descriptor; debugging typically means adding a debug JSON-encoding path or using a tool that understands the .proto file.
Client diversity and tooling. JSON needs nothing beyond a standard library in essentially every language ever shipped, which matters when the API is public and you do not control the client. Protobuf needs the compiler and generated stubs for each client language, which is a real integration cost for external, unknown consumers but a small one-time cost inside a service mesh you already control.
Worked example: what the format choice actually costs on the wire
Take a tiny, realistic message: {"id": 42, "name": "widget", "in_stock": true}.
As compact JSON (no extra whitespace), this is:
{"id":42,"name":"widget","in_stock":true}
That is 41 bytes.
Encoding the same three fields as Protobuf (field 1 = id, varint; field 2 = name, length-delimited; field 3 = in_stock, varint), by hand-rolling the tag-length-value bytes. Every field starts with a tag byte, and that byte is not arbitrary: it packs the field number together with a wire type (a small numeric code -- 0 for varint, 2 for length-delimited -- that tells the decoder how many bytes to read and how to interpret them) using the formula tag = (field_number << 3) | wire_type. You can verify each one by hand below:
- Field 1 (varint, wire type 0): tag = (1 << 3) | 0 = 8 =
0x08; value 42 fits in one varint byte0x2A-> 2 bytes. - Field 2 (length-delimited, wire type 2): tag = (2 << 3) | 2 = 18 =
0x12; length byte0x06, then the 6 raw ASCII bytes of "widget" -> 8 bytes. - Field 3 (varint, wire type 0): tag = (3 << 3) | 0 = 24 =
0x18; valuetrueencodes as0x01-> 2 bytes.
Total: 12 bytes (08 2a 12 06 77 69 64 67 65 74 18 01).
reduction=(1−4112)×100%≈70.7%
That 70.7% is specific to this exact 3-field, mostly-numeric message; it is not a universal constant. Industry write-ups on protobuf vs. JSON commonly report a broader range (roughly 50 to 85% smaller, 3 to 10 times faster to parse) across many message shapes, which is consistent in direction with this worked example but is a separately-reported range, not something this specific calculation proves on its own.
When the axes actually point in different directions
For an internal, high-throughput RPC path exchanging large model metadata or batched inputs between services you own end to end, Protobuf's size and schema-evolution guarantees dominate and the tooling cost is a one-time investment. For a public REST API serving a heterogeneous client base (mobile, web, and IoT devices you do not control), JSON's zero-tooling accessibility usually outweighs the wire-format savings, unless payloads are large enough (media, bulk exports) that the size difference becomes the bottleneck.
What should a well-designed API contract specify about the data itself, beyond just the list of endpoints? Walk through what you would document for request and response shapes, required versus optional fields, and how the contract communicates its own versioning and evolution guarantees to consumers.
Sample Answer
A well-designed contract needs to specify three things beyond the bare list of endpoints: the exact SHAPE of every request and response, which fields are required versus optional (and what happens if an optional field is absent), and an explicit statement of how the contract is allowed to change over time. The first two tell a consumer what to send and expect today; the third tells them what they can safely assume will still be true tomorrow.
What "shape" needs to cover
Request and response shapes. Every field's type, whether it can be null, and any constraints on its value (an enum's allowed values, a string's format, a number's valid range) should be documented, not left to be inferred from a single example response. A field that is sometimes an integer and sometimes a string in practice is a defect in the contract, not a detail an example can paper over.
Required versus optional fields. A field marked required must always be present; a client that hardcodes an assumption an optional field will always be there is building on an assumption the contract never made. This distinction matters most when the contract evolves: adding a new field as optional is safe, adding it as required is a breaking change for every existing client.
How the contract communicates its own evolution
The most important, and most often skipped, part: state the contract's OWN compatibility policy up front, not just its current shape. Concretely, that means documenting:
- What counts as an additive (backward-compatible) change: adding a new optional field, adding a new enum value if clients are expected to handle unknown values, adding a new endpoint.
- What counts as a breaking change: removing or renaming a field, changing a field's type, making an optional field required, removing an enum value.
- How consumers will be notified of a breaking change and what migration window they get.
Worked example
A payments API's contract states: "Fields marked optional may be omitted by the server; do not assume their presence. New optional fields may be added at any time without a version bump. Any field removal, type change, or optional-to-required change will ship behind a new major version, announced at least 90 days in advance." A consuming team reading that sentence knows exactly what defensive coding they need (tolerate unknown fields, do not treat every field as guaranteed) and what they can rely on (a stable major version will not silently break them).
Trade-offs and pitfalls
Documenting the shape without documenting the evolution POLICY is the most common gap: teams often ship a precise OpenAPI spec for the current shape but say nothing about what happens when that shape needs to change, which leaves every consumer to guess (and usually guess wrong, by coding against the current shape as if it were permanent). The opposite failure is a policy that exists in a wiki page nobody reads instead of living next to the spec itself; a compatibility policy that is not co-located with the contract it governs might as well not exist.
Unlock Full Question Bank
Get access to all 16 API Contracts and Schema Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.