Software Architect · Module 07
An API is a promise between systems. A good contract outlives clients, teams, and several versions of the implementation.
REST · GraphQL · gRPC · idempotency · compatibility
The transport matters, but the contract matters more: what we accept, what we return, which errors are possible, and what can be safely retried.
An API should be boringly predictable
A good contract isn't memorable for its style. It's clear, complete, and leaves no dangerous ambiguity.
REST, GraphQL, gRPC, and WebSocket solve different problems. REST is simple for resource-oriented operations. GraphQL gives clients flexibility over the shape of the response. gRPC is useful for typed service-to-service calls. WebSocket fits two-way real-time channels.
But professional API design doesn't start with the transport. It starts with the schema, status codes, errors, idempotency, rate limits, backwards compatibility, and the deprecation policy.
Errors are part of the contract
If the GPS only says "didn't work," the driver doesn't know whether the tank is empty, the road is closed, or the address was wrong.
Clients need machine-readable errors: a code, a developer-facing message, the offending field, retryability, a correlation id. A bare 400 Bad Request with no structure forces the client to parse text or guess.
Errors have to be as stable as a successful response. If a downstream system builds behaviour on PAYMENT_REQUIRES_ACTION, you can't swap that out for an arbitrary string tomorrow.
The real world retries: timeouts, retries, double clicks, duplicate webhooks, worker restarts.
Example: idempotent payment creation
If someone presses the elevator call button twice, the elevator shouldn't arrive twice and leave twice. The intent is one.
The POST /payments endpoint accepts an Idempotency-Key. The server stores the key, the request fingerprint, and the result of the operation. A repeat of the same request returns the same payment intent. A repeat with the same key but a different body returns a conflict.
That contract protects the money, the UX, and support. The client can safely retry after a timeout.
Anti-example: a breaking change disguised as cleanup
If you change the lock on the front door without warning, the issue isn't that the new lock is better.
The team renames userId to accountId because it's "more correct." The internal frontend was updated; external clients broke. No version, no changelog, no migration window, no deprecation warning.
A good API respects the promises it has already made. The new model can be better, but the migration path is part of the decision.
- Which errors can the client handle automatically? - What happens on a retry? - How do we add a field without a breaking change? - Where is the deprecation policy written down?