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 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.
What schema and data-contract decisions matter most for a multi-tenant API platform where each tenant can have its own response-shaping rules and audit requirements? Consider shared-versus-per-tenant fields, versioning implications, and audit metadata in the payload, and explain how these choices keep the platform maintainable as the tenant count grows.
Sample Answer
The central decision is where tenant-specific variation lives in the schema: as a shared response shape with tenant-scoped VALUES, or as genuinely different response shapes per tenant. Nearly always, the former is safer and more maintainable, even when it feels like it under-serves a tenant's specific customization request.
Shared fields versus per-tenant fields
Keep the CORE response schema identical across every tenant (the same field names, same types, same required/optional structure), and let tenant-specific behavior express itself through VALUES within that shared shape (a tenant's configured response-shaping RULES determine which optional fields are populated, not a fundamentally different schema per tenant). The alternative, letting each tenant effectively define its own response shape, means every client integration and every internal consumer has to branch on "which tenant's shape am I looking at," which multiplies the number of contracts you are actually maintaining by the number of tenants.
Versioning implications
A schema change now has to be evaluated against every tenant's configuration, not just a single global consumer base: an additive field is still safe, but ANY validation you'd normally treat as safe to tighten (making an optional field required, for instance) needs to be checked against every tenant's current configuration, since a tenant relying on the old, looser behavior would otherwise break silently. This argues for being especially conservative about ever narrowing the contract once tenants are live against it.
Audit metadata in the payload
Include a lightweight audit envelope on every response, not bolted on separately: a tenant_id, and where relevant a policy_version field showing which version of that tenant's response-shaping configuration produced this specific response. This turns "which rule produced this shape for this tenant" from a support-ticket investigation into something visible directly in the response the client already has.
Worked example
Tenant A configures the platform to include a computed risk_score field on order responses; Tenant B does not use that feature at all. The CONTRACT for both tenants is identical: the schema declares risk_score as an optional field, present when a tenant's configuration enables it and absent otherwise, never a schema difference between "Tenant A's order shape" and "Tenant B's order shape." A client library written once against this one contract works correctly for every tenant without needing tenant-specific parsing logic.
Trade-offs and pitfalls
The tempting shortcut, letting a tenant fully customize their own response shape (their own field names, their own structure) to satisfy an early enterprise customer's specific request, becomes an ongoing maintenance and versioning liability the moment a second tenant asks for something similar but not identical: now there are effectively N contracts to keep backward compatible instead of one. The shared-shape-with-tenant-scoped-values pattern costs some flexibility up front (a tenant genuinely cannot get a schema shape no other tenant has) but is what keeps the platform's contract, and its evolution story, tractable as tenant count grows into the thousands.
Tell me about a time you had to push back on an API or data-model design that seemed simpler for short-term delivery but would have created long-term pain for downstream consumers. How did you make the case, and what was the final decision?
Sample Answer
The strongest version of this story is not "I was right and they were wrong," it's showing the concrete, specific downstream cost the simpler design would have created, translated into terms the other side of the table actually cared about (a delivery deadline, a support burden, a migration cost six months out), and then describing how the actual decision got made once that cost was visible to everyone.
What a strong answer walks through
The situation: name the specific design choice under pressure (a schema shortcut, a field reused for two purposes, a response shape copy-pasted from an unrelated endpoint) and the delivery pressure driving it, honestly, without caricaturing the other side's position as simply wrong.
The case you made: the strongest version of this case is concrete, not principled in the abstract. "This will be hard to maintain" rarely moves a deadline; "this specific field reuse means every future consumer has to special-case whether this response came from path A or path B, and we already have three planned features that will need to tell them apart" is something a stakeholder can actually weigh against the deadline.
How the decision actually got made: did you get the design changed outright, negotiate a smaller fix with a follow-up ticket to do it properly, or lose the argument and later have to deal with the consequence you predicted? All three are legitimate answers; the weakest version of this story claims a clean win with no friction, which reads as either an easy problem or a polished retelling.
The outcome: what happened afterward, concretely, that validates (or complicates) the case you made at the time.
Worked example shape
"We were under a two-week deadline to ship a partner integration, and the plan was to reuse an existing internal user_id field on the response as the partner-facing identifier, saving us from adding a new field and updating a few internal services. I pushed back because I could see two features already on the roadmap that would need a distinct partner-facing identifier separate from the internal one (partner-scoped API keys, and a planned data-residency requirement that needed to know which records were partner-visible). I proposed adding a new partner_ref field instead, which took two extra days but meant we did not have to do a breaking migration eight weeks later when the partner-scoped-API-keys feature landed and genuinely needed that separation. The team agreed to the two extra days once the specific upcoming conflict was visible, not because of a general 'good practice' argument."
Trade-offs and pitfalls
A common weak point in this story is framing it as pure technical purism ("clean code" or "best practices") rather than a concrete, forecastable cost; interviewers are listening for whether you can translate a technical concern into terms a non-technical stakeholder, or a deadline-driven engineering lead, would actually act on. Equally common: telling this story as an unqualified win when the honest answer is more nuanced (you got a smaller compromise, or the team shipped the shortcut anyway and you were right six months later); either honest version is stronger than a suspiciously frictionless win.
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.
Your team wants to add a computed field to an existing API response, but the value is expensive to calculate and could materially change the response time. How would you decide whether to compute it synchronously, cache it, materialize it in storage ahead of time, or expose it through a separate endpoint instead?
Sample Answer
The decision hinges on two independent questions: how often is this field actually read relative to how often the underlying data changes, and how expensive is the computation relative to the latency budget of the endpoint it would live on. Getting the pairing right (cheap-and-frequent versus expensive-and-rare) is what separates a good answer from a checklist of four generic options.
The four options, and when each wins
Compute synchronously, inline in the response. Only viable if the computation is genuinely cheap relative to the endpoint's existing latency budget; adding a moderately expensive computation directly into a hot-path response risks making every caller pay a cost that only some of them actually need.
Cache the computed value. A strong fit when the underlying data changes far less often than the field is read (a product's aggregate rating computed from reviews that arrive far less frequently than the product page is viewed): compute once, serve many times, with a clear invalidation trigger (recompute when a new review lands, or on a time-based TTL (time-to-live) if slight staleness is acceptable).
Materialize it in storage ahead of time. The right choice when the computation is too expensive to redo per-cache-miss and the read pattern is frequent and predictable enough to justify pre-computing and storing the result as data, updated by a background job or an event trigger rather than computed on any request path at all.
Expose it through a separate endpoint. The right choice when only a MINORITY of callers actually need the field, since folding an expensive computation into the primary response penalizes every caller (including the majority who never wanted it) to serve the few who do; a separate, explicitly-named endpoint lets the cost be paid only by the consumers who ask for it.
Worked example
A product page's response currently includes basic fields (name, price, description) served in under 50ms. The team wants to add a computed similar_products field requiring a moderately expensive similarity computation across the catalog, roughly 200ms.
- If most callers of this endpoint (analytics jobs, internal tooling) never use
similar_products, but the customer-facing web page always renders it: expose it through a separate endpoint (GET /products/{id}/similar) so the majority of callers keep their fast response, and the web page issues a second, parallel request specifically for the expensive field. - If the underlying catalog data changes only a few times a day but the product page is viewed millions of times: cache the computed value, recomputing on a catalog-change event rather than per-request, so the 200ms cost is paid rarely instead of on every page view.
Trade-offs and pitfalls
Folding an expensive field directly into the primary response "for convenience" is the most common mistake: it looks simpler in the short term (one endpoint, one call) but silently taxes every caller of that endpoint with the new field's cost, including callers who will never read it. The opposite mistake, splitting out a separate endpoint for a field nearly every caller actually needs, just adds an extra round trip for the common case; the decision genuinely depends on measuring who calls this endpoint and how they use the response, not on a general preference for either simplicity or slimness.
Unlock Full Question Bank
Get access to all 8 API Contracts and Schema Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.