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.
Draft a concise API contract (endpoints, request/response examples, and audit behavior) for an internal feature-flagging service used by multiple teams. Include endpoints for creating a flag, evaluating a flag on a low-latency path, toggling a flag, and retrieving audit logs. Note the performance and safety considerations your schema choices need to account for.
Sample Answer
A feature-flag service's contract centers on one asymmetry: the evaluate path is read constantly, on a hot path, by every service that checks a flag, while create/toggle/audit are low-volume management operations. The contract should reflect that asymmetry directly in its shape, not just in the implementation behind it.
The four endpoints
Create a flag (POST /flags): accepts a flag key, a human-readable description, and a default value, and returns the created flag's identifier and current state. This is a low-frequency, administrative operation, so its response can afford to be verbose (full flag metadata) without any latency concern.
Evaluate a flag (GET /flags/{key}/evaluate?context=...): the hot path. The request carries an evaluation context (user ID, environment, or other targeting attributes); the response should be as small as possible, essentially just the resolved boolean or variant value, because this call happens on every request path that checks the flag and any extra payload weight is paid millions of times over.
Toggle a flag (PATCH /flags/{key}): flips a flag on or off (or changes its targeting rules), and should return the new state plus a version or timestamp so a caller can confirm the toggle actually took effect and see what it changed from.
Retrieve audit logs (GET /flags/{key}/audit): returns a paginated history of who changed the flag, when, and what the before/after state was, which is the accountability trail for a service that can change application behavior without a code deploy.
Worked example: the evaluate response shape
{
"flag_key": "new_checkout_flow",
"value": true,
"reason": "targeting_rule_match"
}
Compare that to a create response, which can be much richer:
{
"flag_key": "new_checkout_flow",
"description": "Enables the redesigned checkout flow for eligible users",
"default_value": false,
"created_at": "2026-07-28T12:00:00Z",
"version": 1
}
The evaluate response is deliberately minimal (three small fields); the create response can afford ten times the payload because it runs orders of magnitude less often.
Performance and safety considerations the schema has to account for
- Evaluate must be cacheable and fast to parse, so its schema stays flat and small; nesting a full targeting-rule explanation into every evaluate call would add latency to the busiest path in the system for the sake of a debugging convenience almost nobody needs on every call.
- Toggle must be auditable by construction, not as an afterthought: every toggle response including a version number means a caller (and the audit log) can always answer "what did this flag look like immediately before this specific change."
- The evaluation context in the evaluate request needs a stable, documented shape (which attributes are supported for targeting) so that adding a new targeting dimension later is an additive, backward-compatible schema change, not a breaking one for every existing caller.
Trade-offs and pitfalls
The most common mistake is putting all four operations behind one uniform, verbose response schema for consistency's sake, which quietly taxes the highest-traffic endpoint (evaluate) to make the lowest-traffic ones (create, audit) marginally easier to read. A feature-flag contract earns its keep specifically by treating "how often is this called" as a first-class input into how big its response is allowed to be.
What is OpenAPI (Swagger), and how does an OpenAPI specification improve API design, documentation, and developer experience? Explain how you would use the spec in continuous integration to detect breaking changes before deployment and to generate SDKs and mock servers.
Sample Answer
OpenAPI (the specification that was originally called Swagger) is a machine-readable, language-agnostic way to describe an HTTP API: every endpoint, its request and response shapes, its status codes, and its authentication requirements, written down in one YAML or JSON document. It matters because that single document becomes something both humans and tools can act on: developers read it as documentation, and tools consume it to generate mock servers, client SDKs (software development kits), and automated compatibility checks.
What the specification actually buys you
Design and documentation. Because the spec is structured (not prose), documentation generated from it (via tools like Redoc or Swagger UI) stays interactive and precise: a reader can see the exact shape of a request body and try a live call against a mock server, not just read a paragraph describing it.
Developer experience. A new engineer, or an external partner, can generate a client library in their language of choice directly from the spec instead of hand-writing HTTP calls and guessing at field names from an outdated wiki page.
Using the spec in CI (continuous integration) to catch breaking changes before deployment
The core idea: treat the OpenAPI document itself as a versioned artifact, and diff the new version against the last deployed version on every pull request.
- Store the spec in the same repository as the code, versioned alongside it, so a pull request that changes behavior also shows the corresponding spec diff in review.
- Run a schema-diff tool in CI (categories of tool: OpenAPI diff checkers, contract-testing frameworks) that classifies each change as additive (a new optional field, a new endpoint) or breaking (a removed field, a field made required, a changed type). Additive changes pass automatically; breaking changes fail the build unless explicitly approved.
- Generate SDKs and a mock server directly from the spec as a build step, so client teams consuming the generated SDK get a compile error (not a runtime surprise) when a breaking change slips through, and so a mock server is always available for frontend and QA work without waiting on the real backend.
Worked example
Suppose a pull request changes a response field total_amount from a string to a number. The CI pipeline pulls the previously-deployed spec, diffs it against the new one, and flags: "field total_amount on POST /orders response changed type from string to number, this is a breaking change for any existing consumer parsing it as a string." That failure blocks the merge until either the team reverts to an additive change (adding a new field total_amount_numeric instead of retyping the existing one) or explicitly bumps a major version and communicates the break.
Trade-offs and pitfalls
The spec is only as trustworthy as the CI gate that enforces it: an OpenAPI document that is hand-edited and never validated against the running service's actual behavior can drift silently, and the mock servers and generated SDKs built from it become confidently wrong. The breaking-change detector also needs real judgment layered in, not blind automation: a change that is technically additive (a new required header) can still break existing clients in practice, so treating the diff tool's classification as the final word rather than a first pass is a common mistake.
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 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.
Explain contract-first (OpenAPI-first) versus code-first API development. Discuss how each approach impacts documentation quality, SDK generation, design reviews, iteration speed, and coordination with product and client teams in a cross-functional environment.
Sample Answer
Contract-first (also called design-first) means writing the OpenAPI specification before any implementation code exists, and treating that spec as the source of truth the rest of the process is built around. Code-first means implementing the endpoints first and generating the spec afterward, usually from code annotations. The trade-off is speed now versus alignment later: code-first ships an initial version faster, contract-first spends that time up front and gets it back many times over as the API grows.
How each choice plays out in practice
Documentation quality. Contract-first documentation is authored intentionally and reviewed as a deliverable in its own right, so it tends to describe intent, not just implementation detail. Code-first documentation is generated from whatever the code happens to do, so it is only as good as the annotations a developer remembered to write, and it can drift the moment someone edits a handler without updating the annotation.
SDK generation. Contract-first lets you generate client SDKs (software development kits), in multiple languages, before a single endpoint is implemented, because the spec is a complete, self-contained artifact. Code-first SDK generation has to wait for the code to exist, and if the spec is only produced from annotations, gaps in those annotations become gaps in the generated client (an endpoint the annotations mislabeled generates a client method with the wrong shape).
Design reviews. A written spec is something a reviewer, a frontend engineer, or an external partner can read and comment on before any implementation exists, which surfaces disagreements about the shape of the contract while they are still cheap to fix. Reviewing a code-first API usually means reviewing a pull request that already contains the implementation, so a disagreement about the contract's shape now means reworking real code, not just a document.
Iteration speed. For a small, fast-moving internal service with few consumers, code-first is genuinely faster: you skip the spec-authoring step and let the framework generate documentation as a side effect. That advantage shrinks, and can invert, once there are multiple independent consumer teams who need the contract stabilized before they start their own work.
Coordination across teams. This is where contract-first tends to win as the organization grows. A stable, reviewed spec becomes the coordination artifact multiple teams can build against in parallel: product managers get a concrete, reviewable artifact to confirm the contract actually matches the intended business requirements before a single line of implementation exists, instead of discovering a scope mismatch during a late-stage demo; frontend engineers write against mocked responses generated straight from the spec, and partner integrations get a concrete document to implement against, all before the backend team ships a single endpoint.
Worked example: the same endpoint, two orders of operations
Contract-first: write the OpenAPI document for POST /orders (request schema, response schema, error responses) -> review it with the frontend and partner teams -> generate a mock server from it so the frontend team starts building immediately -> implement the backend against the now-frozen spec -> validate the real implementation against the spec in CI.
Code-first: implement POST /orders -> annotate the handler with the framework's OpenAPI decorators -> generate the spec from those annotations -> frontend team waits for either the real endpoint or a manually-maintained mock, because no independent spec existed to generate one from earlier.
Trade-offs and pitfalls
Contract-first fails when the spec is written once and never re-validated against the implementation: without a CI (continuous integration) check that diffs the running service's actual behavior against the spec, the two drift apart just as easily as in code-first, except now with a false sense of security because "we have a spec." Code-first fails quietly: a subtle annotation mistake (a field the code treats as optional but the annotation marks required) produces a generated SDK that is wrong in a way nobody notices until a client hits it in production. Neither approach is safe without a CI gate that continuously checks the deployed behavior against the published contract, whichever one is authoritative.
That is every published API Contracts and Schema Design question for Cloud Architect so far. Browse the other topics in this category, or practice this one interactively.