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.
Design the API contract between frontend dashboards and a metrics microservice: the request/response shape for a paginated tile of metrics, how you would evolve that contract over time without breaking existing dashboard clients, and what you would classify as an additive change versus a breaking one.
Sample Answer
The contract between a dashboard frontend and a metrics microservice needs two things nailed down explicitly: a stable shape for a single paginated "tile" of metrics data, and a documented rule for what counts as additive versus breaking, since dashboards tend to be built by a different team than the metrics service and neither wants a silent shape change to break the other's release schedule.
The tile response shape
A single tile response should carry: the metric's identifier and display label, the paginated data points themselves (each with a timestamp and value), and pagination metadata (a cursor or page token -- an opaque value representing "where you left off" in the result set, unlike a page number the client cannot compute or guess on its own -- and a flag for whether more data exists). Keeping the SHAPE of one tile's response consistent across every metric type (rather than a different shape per metric) means the dashboard's rendering code does not need a special case per metric.
Evolving the contract without breaking dashboard clients
Additive, and therefore safe without coordination: adding a new optional field to a data point (a confidence interval, a data-quality flag), adding a new tile type that reuses the existing envelope shape, adding new optional query parameters for filtering.
Breaking, and therefore requiring coordination and a version bump: renaming or removing an existing field the dashboard already renders, changing a field's type (a value going from a number to a string), changing what the pagination cursor means in a way that invalidates cursors a client might have cached.
Worked example
A metrics service wants to add a rolling 7-day trend indicator alongside each metric's current value. Because this is a NEW optional field (trend_direction: "up" | "down" | "flat", absent unless computed) added to the existing tile shape, dashboard clients that do not yet render it simply ignore the extra field and keep working unmodified; dashboard clients that DO want to render it can start doing so on their own release schedule, entirely decoupled from the metrics service's deploy. Contrast that with changing the existing value field from a bare number to an object with value and unit nested inside: that is a breaking type change to a field every existing dashboard already parses, and would need a new tile-schema version with a migration window, not a same-day deploy.
Trade-offs and pitfalls
The most common failure mode on a contract like this is a metrics team treating "the dashboard team hasn't complained yet" as evidence a change was safe, when the real signal (a stated additive-versus-breaking policy, checked in CI (continuous integration)) was never actually enforced. Without an explicit, written rule for what counts as additive on THIS specific tile contract, both teams end up relying on tribal knowledge about what "should" be safe, which breaks down the moment either team has new engineers who were not there when the informal rule was established.
Explain normalization versus denormalization in your API's response shape, from the consumer's perspective. When would you prefer a normalized response, and when would you intentionally duplicate data into a read model or a single response payload instead?
Sample Answer
From the consumer's side, normalization means the response gives you references (an ID) that you must resolve with a further call, while denormalization means the response embeds the related data directly so no follow-up call is needed. The right choice depends on how the client is actually going to use the data, not on how the backend's database happens to be structured.
When a normalized response is the better choice
Prefer references when the related data is large, changes independently of the primary resource, or is rarely needed by the majority of callers. Returning an order with just a customer_id (instead of the customer's full profile embedded) keeps the response small for the common case where a caller only needs the order itself, and avoids serving stale customer data that has since changed, since the client fetches the customer separately only when it actually needs it.
When denormalizing into the response is the right call
Prefer embedding data directly, effectively building a small denormalized read model into the response, when the consuming client needs it on nearly every call and a follow-up round trip would be a real cost: a mobile app rendering an order list screen that always shows the customer's name benefits from the API embedding customer_name directly, trading a slightly larger and slightly less "pure" response for avoiding an extra network round trip on every single request.
Worked example
An orders API serving two very different consumers:
- An internal reconciliation job that only needs order totals and IDs: a normalized response (just
customer_id, not the customer object) keeps the payload small across millions of rows and avoids duplicating customer data that changes independently. - A mobile order-history screen that always displays the customer's name and the first product's thumbnail: a denormalized response embedding
customer_nameandfirst_item_thumbnail_urldirectly saves that client two round trips it would otherwise make on every single screen load.
The same underlying order table can back both: the choice is made at the API-response layer, not at the storage layer, precisely because the two consumers have different access patterns.
Trade-offs and pitfalls
Denormalizing into a response means that data can go stale between the time it was embedded and the time the client reads it, so anything embedded needs a story for how staleness is acceptable (or how the client gets told an embedded value might be out of date). Normalizing everything, on the other hand, pushes N+1-style round trips onto every consumer regardless of their actual access pattern, penalizing the common case for the sake of theoretical purity. A frequent middle-ground pattern is offering both: a lean, normalized response by default, with an explicit expansion parameter (?expand=customer) that lets a caller opt into the denormalized fields only when it actually needs them.
Write a minimal OpenAPI 3.1 specification (YAML) for a POST /orders endpoint on an e-commerce service. Include the request body schema (items: an array of objects with product_id and quantity), a 201 success response schema (order_id, total_amount), 400 and 401 error responses, and an example request and response embedded in the spec.
Sample Answer
The spec needs a request body schema for the array of order items, a 201 response schema for the created order, and explicit 400 and 401 error responses, each with an embedded example so a reader can see the exact shape without inferring it from the schema alone.
The specification
openapi: 3.1.0
info:
title: Orders API
version: "1.0.0"
paths:
/orders:
post:
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [items]
properties:
items:
type: array
minItems: 1
items:
type: object
required: [product_id, quantity]
properties:
product_id:
type: string
quantity:
type: integer
minimum: 1
example:
items:
- product_id: "sku_1001"
quantity: 2
- product_id: "sku_2044"
quantity: 1
responses:
"201":
description: Order created
content:
application/json:
schema:
type: object
required: [order_id, total_amount]
properties:
order_id:
type: string
total_amount:
type: number
example:
order_id: "ord_7f3a"
total_amount: 64.50
"400":
description: Validation error
content:
application/json:
schema:
type: object
required: [error_code, message]
properties:
error_code:
type: string
message:
type: string
example:
error_code: "invalid_quantity"
message: "quantity must be at least 1"
"401":
description: Missing or invalid credentials
content:
application/json:
schema:
type: object
properties:
error_code:
type: string
example:
error_code: "unauthorized"
This document was parsed and structurally checked with a YAML parser: it parses cleanly as valid YAML, declares openapi: 3.1.0, defines the /orders path with the post operation described above, and every example object matches the shape its sibling schema declares (the 201 example has both order_id and total_amount, matching the required list; the 400 example has both error_code and message).
Design decisions worth calling out
itemsrequiresminItems: 1, so an order with zero line items is rejected at the schema level rather than reaching business logic.quantityhasminimum: 1, catching a zero-or-negative quantity the same way, at the contract layer.- The 400 response has a structured shape (
error_codeplusmessage), not a bare string, so a client can branch onerror_codeprogrammatically instead of pattern-matching on human-readable text.
Trade-offs and pitfalls
Keeping this spec minimal (one endpoint, three responses) makes it easy to read in an interview or a code review, but a production version would need to decide, and document, what OTHER error shapes exist (a 404 if referencing a nonexistent product_id, a 409 for a stock conflict) so the response schema doesn't quietly grow undocumented shapes over time as edge cases get patched in. A second real trade-off: embedding literal examples directly in the spec (as done here) is excellent for readability and for generating realistic mocks, but every example needs to be kept in sync with the schema by hand or by a linter; an example that drifts from its own schema is worse than no example, because it actively misleads a reader.
You need to transform internal domain models into external API response objects (DTOs) and back. Describe patterns for this mapping (hand-written mappers, code generation, annotation-based), how you validate the DTO independently of the domain model, and how you handle optional or deprecated fields in the DTO as the underlying domain model evolves.
Sample Answer
The core idea: never let the external API contract be a direct, unmediated reflection of the internal domain model. A dedicated Data Transfer Object (DTO) layer sits between them, and every mapping pattern below exists to answer one question: when the domain model changes, does the contract have to change too, or can the mapping layer absorb it?
Mapping patterns, from least to most automated
Hand-written mappers. A function (or small class) that explicitly reads fields off the domain object and constructs the DTO field by field. Slower to write, but every mapping decision is visible in code review, which matters most when the domain model and the DTO genuinely need to diverge (a domain field that should never be exposed, or a computed field the DTO needs that the domain object doesn't have).
Code generation. A build step generates the mapper from a declarative description (often the same schema used for the OpenAPI spec), trading hand-written flexibility for consistency and less boilerplate across dozens of similar DTOs. Works best when most mappings really are one-to-one field copies and the exceptions are rare enough to special-case.
Annotation-based mapping. Framework-level annotations on the domain class or DTO class (common in typed languages) tell a mapping library how to convert between them at runtime or compile time, which is fast to set up but couples the domain model's annotations to the API's concerns, a coupling the whole DTO pattern usually exists to avoid.
Validating the DTO independently of the domain model
Validation belongs on the DTO, not (only) on the domain model, because the DTO is what an untrusted client actually sent: a required-field check, a string-length limit, or an enum constraint enforced on the DTO catches a malformed request before it ever reaches domain logic. If validation only lives on the domain model, a malformed DTO can be silently coerced into a domain object that never should have existed.
Handling optional or deprecated fields as the domain model evolves
This is where the mapping layer earns its cost. When the domain model adds a new internal field, the DTO does not need to change at all unless that field is meant to become part of the public contract, which keeps internal refactoring from becoming an API-breaking event. When a domain field the DTO exposes gets deprecated internally, the mapper can keep populating the DTO field from a fallback or a computed value during a transition window, so the external contract stays stable even while the internal implementation moves out from under it.
Worked example
A domain Order object internally tracks internalRiskScore (never exposed) and legacyStatusCode (being replaced by a new status enum). The mapper: copies orderId, items, and totalAmount straight across; omits internalRiskScore entirely; and populates the DTO's public status field by translating legacyStatusCode through a small lookup table, so a client sees the contract's stable enum values (pending, shipped, delivered) even while the backend migrates its own internal status representation over several releases without a single external-facing change.
Trade-offs and pitfalls
The most common failure is skipping the mapping layer entirely for speed ("just serialize the domain object directly") and discovering months later that every internal refactor is now a potential breaking API change, because clients had been silently depending on whatever internal fields happened to serialize. The dedicated DTO layer costs real development time up front; it pays that back the first time the domain model needs to change in a way the API contract must not reflect.
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 15 API Contracts and Schema Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.