Two teams will build both sides of an interface in parallel. How do you verify it before either side exists?
answer
- Write the interface down before either half exists
- Both builds check the same artefact
- One side proves it produces, one consumes
- A unilateral change should break a build
- Right shape, wrong or stale meaning
basics
~20 sAgree a written, machine-checkable description of the interface first — request and response shapes, field meanings, error cases — then have each side check itself against that same description in its own build: the provider that it can produce those responses, the consumer that it works against a stand-in generated from them.
solid answer
~50 sWrite the interface down before either side is built, in a form both builds can check against rather than prose in a document. It fixes operation names, request and response shapes, field types and optionality, error responses and their meanings, and the units and time semantics of anything ambiguous. Then each side verifies itself against that artefact independently: the provider runs a check that its implementation really returns those shapes and error cases; the consumer builds against a stand-in generated from the same description, so its code compiles and runs against the agreed shape long before the provider is deployable. Both checks run in their own build, so a unilateral change breaks a build instead of surfacing at integration. What this does not verify is meaning — the shape can be right while the values are stale or wrong — so keep an integrated check on the critical paths.
code
pseudocode · 19 linesAGREEMENT participant-status v1:
operation getParticipantStatus(participantId: string)
returns 200 { status: one_of[screening, enrolled, withdrawn],
decidedAt: instant_utc,
sourceFreshnessSeconds: integer >= 0 }
returns 404 { code: "participant_not_found" }
never returns an empty status object
PROVIDER BUILD:
start real service with a withdrawn participant
response = call getParticipantStatus("P-4417")
assert response matches AGREEMENT participant-status v1
assert response.status == "withdrawn"
CONSUMER BUILD:
standin = generate_standin(AGREEMENT participant-status v1)
form = render_visit_form(client_pointing_at(standin))
assert form.banner reflects standin.status
assert form.adverse_event_section locked when status == "withdrawn"go deeper
Know the shape of the idea: the interface is written down first, and the calling side can develop against a stand-in that answers with the agreed responses rather than waiting for the real service.
Explain what belongs in the agreement beyond field types — units, time semantics, enumerated values, error responses — and how each side verifies itself against the same artefact in its own build.
Show where it earns its keep and where it stops: it catches drift and shape breaks early, it never proves values are correct or fresh, so say which integrated checks you keep and why.
Own the cross-team mechanics: who owns the agreement, whether both checks block a build, and the compatibility policy for breaking changes across teams that ship on different cadences.
## The problem this solves When two teams build the two halves of an interface at the same time, the traditional integration point is the first moment either learns what the other actually did. Everything before it is optimism. Agreeing the interface *first*, in an artefact both sides can check themselves against, moves that discovery to the left — into each team's own build — and is one of the more concrete shift-left practices available at a system level. ## The agreement artefact The agreement has to be more than prose in a page nobody re-reads. Written well it fixes: - **operations** and how they are addressed; - **request and response shapes**, field by field, with types, optionality and cardinality; - **field meaning** — units, precision, time zone and clock semantics, identifier format, enumerated values and what each one means; - **error responses** — which conditions produce which failure, and whether the caller should retry; - **compatibility rules** — what either side is allowed to change without ceremony (typically adding an optional field) and what counts as a breaking change. The last two are where most of the value hides. Shape mismatches are cheap to find; disagreement about what an empty list means, or whether a timestamp is the event time or the write time, is the sort of thing that survives into production. ## Two independent checks, both to the left **Provider side.** The team building the interface runs an automated check in its own build that the running implementation really produces the agreed shapes and the agreed error responses. This is the check that fails when someone renames a field or narrows a type without telling anyone — and it fails in *their* build, minutes after the change, not weeks later in someone else's environment. **Consumer side.** The team calling the interface builds and tests against a stand-in generated from the same agreed description, one that answers with the agreed shapes. Their client code, error handling and mapping are exercised before the provider is deployable at all. Because the stand-in is derived from the shared artefact rather than hand-written from a reading of it, it cannot quietly encode the consumer's misunderstanding. The pair of checks gives you the property that matters: **a unilateral change breaks a build**. Drift is detected by tooling instead of by an integration meeting. ## A worked example A clinical-trial data capture platform is being split. One team builds a participant-status service; another builds the visit form that renders the eligibility banner and unlocks the adverse-event section. Neither exists yet. The agreement pins down: `getParticipantStatus(participantId)` returns a status from a fixed set, plus `decidedAt` as an instant in UTC, plus `sourceFreshnessSeconds`; withdrawal returns the status `withdrawn` and never an error; an unknown participant returns a distinct not-found response, not an empty status. Those three decisions are exactly the ones that would otherwise be discovered late. Without `decidedAt` and the freshness value in the agreement, the provider is free to serve a cached projection and the consumer has no way to know the banner is showing a stale-cache read; withdrawal would have surfaced as an unhandled error path in the form; and an unknown participant returning an empty status would have rendered a blank banner with the adverse-event section in an undefined state. The visit-form team then builds and runs its whole client path against a stand-in on day one. The status team's own build fails the day someone changes `decidedAt` to a local-time string. ## What it does not give you This is a check on the *contract*, and the boundary is worth stating plainly in an interview: - **Semantics are not verified.** The provider can return a well-shaped status that is simply wrong or 90 seconds stale. A freshness field in the agreement lets the consumer *detect* staleness; it does not make the data fresh. - **Behaviour under load, real data volumes and real failure modes** stays outside. So do sequences: two calls whose order matters are not described by a per-call agreement. - **The stand-in can drift from reality** if the agreement is not regenerated and re-verified on both sides on every change; a stand-in nobody re-derives becomes a fiction that passes. So keep a small integrated check on the paths that matter — for this example, one that reads a real status after a real withdrawal — and let the agreed-interface checks carry the volume. ## Making it hold organisationally The technique fails for social reasons more often than technical ones. Someone must own the artefact and its change process; both teams must run their side's check as a build-blocking step rather than an optional job; and there must be an agreed answer to "what happens when the consumer needs a breaking change?" — usually an additive change plus a migration window, negotiated rather than pushed. A shared description that only one side verifies against is documentation, and documentation drifts.
- The consumer's checks pass against the stand-in but the live integration returns stale values. What went wrong, and whose check should have caught it?Nothing went wrong with the agreement checks — they verify shape and error cases, not freshness or correctness of values, so a well-formed stale response passes both sides. The fix is partly in the agreement (carry a decided-at instant and a freshness bound so staleness is expressible and assertable) and partly outside it: one integrated check on the critical path that reads a real value after a real state change. Treat contract checks as covering structure and drift, never truth.
- How do you handle a change to the agreed interface that the consumer cannot absorb immediately?Make it additive first: add the new field or operation alongside the old one, let both sides verify against the extended agreement, migrate the consumer, then remove the old shape once no verified consumer uses it. That needs an agreed compatibility rule stated in the artefact and a way to see who is still on the old shape. Pushing a breaking change and expecting the other team to catch up converts a build-time signal back into an integration surprise.
- Why generate the consumer's stand-in from the agreement rather than hand-writing one?A hand-written stand-in encodes the consumer's reading of the interface, so any misunderstanding is baked into the very thing meant to detect it — and it silently stops matching when the agreement changes. Deriving it from the shared artefact means an agreement change flows into the stand-in and the consumer's build fails immediately. The same argument is why the provider verifies against the artefact rather than against its own documentation of itself.
saying these in an interview costs you the question
- Says you cannot test anything until both sides are deployed
- Hand-writes a stand-in from a reading of the document
- Claims contract checks prove the returned data is correct
- Treats a shared document nobody verifies as an agreement
- Runs the provider-side check as an optional job
- Pushes breaking changes without an additive migration step