Section 4: Three Calls and a Refusal - API Design (with Pushback)
Executive Summary: This section writes the generator's contract: an internal gRPC service with three calls (NextId, NextIds with a batch of up to 1,000, and Decode), a thin HTTP/JSON gateway that returns IDs as strings, and exactly one interesting error, UNAVAILABLE, which the server returns instead of ever handing out an ID it can't vouch for. Then the interviewer pushes on every choice: why a service and not a library, whether batching breaks ordering, why a number travels as a string, why there is no idempotency key, and why the timestamp isn't returned as its own field. The contract is small because Section 3's arithmetic says the hard part is rate, not features.
The interviewer taps the bit-budget diagram from Section 3: "Fine, you can mint a million a second on paper. What does a caller actually call?" You begin with who the caller is, because that decides the shape. The caller is never a human. It is the messaging service, the orders service, the payments service, each one asking for an ID on the write path of every record. So the API should be boring, fast, and impossible to misuse.
service IdGenerator {
rpc NextId(NextIdRequest) returns (NextIdResponse); // one ID
rpc NextIds(NextIdsRequest) returns (NextIdsResponse); // count: 1..1000
rpc Decode(DecodeRequest) returns (DecodeResponse); // debugging only
}
message NextIdResponse { int64 id = 1; }
message NextIdsRequest { uint32 count = 1; } // > 1000 -> INVALID_ARGUMENT
message NextIdsResponse { repeated int64 ids = 1; } // ascending
message DecodeResponse { int64 timestamp_ms = 1; // Unix ms, epoch already added back
uint32 region = 2; uint32 worker = 3; uint32 sequence = 4; }
"Notice what the request does not carry," you say. "No caller name, no entity type, no region, no hint about which table the ID is for. The generator doesn't need to know. An ID for a payment and an ID for a chat message are the same kind of object: 64 bits that will never be issued again."
The Hot Path: Client Library and Prefetch
Callers don't speak this protocol directly. They link a thin client library whose public surface is a single function, nextId(), that returns from a local buffer. The library talks to the service with NextIds in the background:
- The buffer holds 100 IDs by default and is refilled when it drops to half.
- Any buffered ID older than 1 s is discarded, never handed out. That keeps the "sorting by ID approximates creation order within about a second" promise from Section 2.
- With a full buffer, a caller using up to ~100 IDs a second rides out a generator blip of up to about 1 s without noticing.
Here's the common case for a caller with a warm buffer. The messaging service in eu-west calls nextId(), and the library answers from IDs worker 17 handed it in an earlier refill:
The point of the picture is the first return arrow. The common case touches no network at all, so the p99 ≤ 2 ms service SLO from Section 2 is the cost of a refill, not of every write. Leo's own message hit a cold buffer, so his ID was minted on the spot; Section 6 traces that slow path.
Key Insight: The API callers program against is not the RPC. It is a local function backed by a buffer. Most of the latency and availability budget is won in the client library, before a packet leaves the host.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson