Today’s Learning Path
For the September 14 class, 6:00–8:30 PM: foundations (35 minutes), contract critique (25), implementation (35), break (10), failure investigation (25), and gRPC, agentic systems, and project discussion (20). REST implementation is required practice; gRPC implementation is optional.
Learning Objectives
- Design a resource-oriented HTTP API with explicit success and error contracts.
- Explain serialization and compatible contract evolution across independent deployments.
- Distinguish service discovery, authentication, authorization, and correlation IDs.
- Allocate a latency budget and distinguish connect timeouts, read timeouts, and deadlines.
- Demonstrate success, slow responses, and dependency failure using two services.
- Compare REST with typed gRPC calls and justify a protocol choice.
Required Preparation
Review Module 2’s service names, environment configuration, and two-service setup. Bring a terminal, Python 3, and your working Flask/Requests environment from that lab. Read the HTTP methods and status-code sections linked in Resources. If your environment is not ready, pair with a classmate and use the request/response examples for the design activity.
Optional preparation: skim the gRPC core concepts and Protocol Buffers guide. No gRPC installation is needed for the required activity.
Core Concepts
API Contract
Agreement covering operations, schemas, errors, identity requirements, and behavioral semantics.
Serialization
Encoding in-memory values into transferable bytes and decoding them at the receiver.
Service Identity
An authenticated principal representing a workload; a DNS name or caller-supplied label alone is not proof of identity.
Latency Budget
The time available for a complete operation, shared by local work and downstream calls.
Timeout and Deadline
A timeout bounds a particular wait; a deadline marks when the whole operation must stop waiting.
Typed RPC
A remote operation described by a service interface and message types, often with generated client and server code.
From Local Calls to Remote Contracts
A local function can receive an object reference. A remote service receives bytes, reconstructs values, and may run a different release of the code. Network calls add name resolution, connection establishment, TLS negotiation when applicable, queueing, serialization, and response transfer. Any stage can fail independently of the caller.
REST is an architectural style organized around resources and a uniform interface. HTTP APIs commonly apply it through resource URIs, method semantics, representations, stateless requests, and cache behavior. A JSON endpoint alone does not establish all REST constraints. Stateless does not mean the server has no database: each request carries the context needed to understand it.
Inventory lookup across a network
sequenceDiagram participant C as Checkout client participant I as Inventory service participant D as Inventory data C->>I: GET /v1/items/sku-123 I->>I: Authenticate and validate I->>D: Read item D-->>I: Available quantity I-->>C: 200 and JSON representation
Checkout serializes an HTTP request to inventory, which validates the request and reads its data. The response returns status, headers, and a JSON representation. Network and queue time add to application execution time.
Methods and Outcomes
| Operation | Example | Intended behavior |
|---|---|---|
| GET | /v1/items/sku-123 | Read a representation without requesting a state change. |
| POST | /v1/reservations | Create a reservation; define duplicate-request behavior. |
| PUT | /v1/preferences/customer-7 | Replace the resource at a known URI. |
| PATCH | /v1/preferences/customer-7 | Apply a defined partial update. |
| DELETE | /v1/reservations/r-42 | Remove the identified reservation. |
Safe methods do not request a state change. Idempotent methods have the same intended effect when repeated; responses and logs can differ. GET, PUT, and DELETE have idempotent semantics; POST and PATCH are not inherently idempotent. See HTTP method semantics.
A lookup can return 200 with zero availability: the business result is not an HTTP failure. A creation can return 201 with a Location header. Invalid input can return 400; missing or invalid authentication 401; insufficient permission 403; an absent resource 404; and a state conflict such as insufficient stock 409. Document the choices instead of making clients infer behavior from prose messages.
A Contract You Can Review
For this module, available means a nonnegative count of units currently available, not a promise that a later reservation will succeed. Another buyer can change stock between lookup and reservation. The following OpenAPI excerpt describes the normal lookup; extend it with error responses during the activity. OpenAPI documents the interface but does not automatically enforce it at runtime. See the OpenAPI specification.
openapi: 3.1.1
info:
title: Inventory API
version: 1.0.0
paths:
/v1/items/{item_id}:
get:
operationId: getItem
parameters:
- name: item_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Current availability, not a reservation
content:
application/json:
schema:
type: object
required: [item_id, available]
properties:
item_id:
type: string
available:
type: integer
minimum: 0
'404':
description: Item does not exist
JSON numbers, strings, missing fields, and explicit nulls are different values. Specify monetary units and timestamps when those fields appear. Renaming available or changing it to a string breaks callers expecting an integer. Adding an optional response field can work if consumers tolerate unknown fields; adding an enum value may still break exhaustive client logic. Test old clients against new servers and new clients against old servers. A /v1 prefix alone does not guarantee compatibility.
Service Identity and Trust
Four checks before inventory returns stock
%%{init: {"themeVariables": {"fontSize": "24px"}, "flowchart": {"wrappingWidth": 400, "nodeSpacing": 50, "rankSpacing": 12, "padding": 10}}}%%
flowchart TB
subgraph locate[Connect to the right server]
direction LR
D["1. Find inventory<br/>Resolve its address"] --> T["2. Verify server<br/>Check TLS certificate"]
end
subgraph trust[Server checks the caller]
direction LR
A["3. Verify caller token<br/>Identity is checkout"] --> P["4. Check permission<br/>Allow inventory.read"]
end
locate --> trust
A verified checkout token with inventory.read permits the lookup. A caller who only sends X-Service-Name checkout has not proved its identity and must not gain access.
Discovery answers “where can I connect?” Authentication answers “who is this?” Authorization answers “may this identity read or reserve this item?” A DNS name is a locator. In production, verify TLS certificates; use a verified workload credential, such as a token or a client certificate, to authenticate the caller. For tokens, verify signature and expected issuer, audience, and expiration, then check operation permissions. A caller-provided X-Service-Name is just untrusted text.
A correlation ID joins records for a request. It confers no access. Keep credentials out of logs. The local lab below uses loopback HTTP and has no authentication; identify where authentication and authorization would be enforced before turning it into a deployed service. Do not treat the lab as a production security template.
Latency, Timeouts, and Deadlines
For a sequential request path, reason about total time as local work plus downstream waits plus network and serialization overhead. With an 800 ms budget, 100 ms already spent and 100 ms reserved for forming the response leave at most 600 ms for the next dependency. Parallel calls often wait for the slowest required branch. Queueing can dominate during overload.
Measure p50, p95, and p99 as well as error rates; p99 describes the slow tail rather than the typical request. Do not add each service’s p99 and claim it is the exact end-to-end p99. Measure the full path. A handful of classroom requests demonstrates behavior, not a reliable production percentile.
A connect timeout and a read timeout protect different waits. In Python Requests, the read timeout limits the wait between incoming bytes, not the entire response duration; the tuple (0.2, 0.6) is not a strict 800 ms end-to-end deadline. Name resolution, multiple connection attempts, and a slowly trickling body can complicate elapsed time. See Requests timeouts.
An overall deadline should include all stages. Downstream calls receive the remaining budget, with time reserved for cleanup and response handling. Do not grant a fresh full budget at every hop. A deadline-aware client and cooperative server cancellation are needed for stronger bounds than this lab provides.
Caller stops waiting before the server finishes
sequenceDiagram participant U as User participant C as Checkout participant I as Inventory U->>C: Get availability C->>I: Read item Note over I: Injected delay of 2 seconds Note over C: Read timeout after 0.6 seconds C-->>U: 504 dependency_timeout I-->>C: Late response, caller no longer waiting
Checkout sends a request but its read timeout expires before inventory finishes. Checkout returns a controlled gateway timeout. Inventory can still finish later; the client timeout has not cancelled or rolled back the server operation.
A timeout cannot prove a write failed. If a reservation was stored before the response was lost, repeating an unprotected POST may reserve twice. Define an idempotency key with atomic deduplication and outcome lookup, or reconcile the operation before retrying. Retries, backoff, and retry budgets are later-module work; this lab performs one attempt.
Activity: Critique a Reservation Contract
In pairs, critique GET /buy?item=sku-123. It changes state despite using a read method, omits quantity and caller permissions, and leaves duplicate behavior unclear.
Design POST /v1/reservations with an item ID, positive quantity, a success representation, and an explicit error shape. Decide what 400, 404, and 409 mean. Identify who may reserve stock and explain what happens if the response is lost after the write. Exchange designs, find one ambiguity, and revise it. Keep idempotency as a written contract today.
Lab: REST Contracts and Slow Dependencies
Build a checkout-facing API (Service A) that reads inventory (Service B). Reuse your Module 2 Python environment. In a new module-03-lab folder, save the following two files. If you need a fresh environment, use:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install Flask requests
python -m pip freeze > requirements.txt
This installs into the local virtual environment and records resolved versions for your lab. Activate it in each terminal. These development servers are for the local exercise.
Service B: Inventory
Save as inventory.py. The artificial delay is a bounded, local test control, not a public API feature.
import logging
import time
from flask import Flask, jsonify, request
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.get("/health")
def health():
return jsonify(status="ok", service="inventory")
@app.get("/v1/items/<item_id>")
def get_item(item_id):
request_id = request.headers.get("X-Request-ID", "none")
try:
delay_ms = int(request.args.get("delay_ms", "0"))
if not 0 <= delay_ms <= 3000:
raise ValueError()
except ValueError:
return jsonify(error="invalid_delay"), 400
app.logger.info("request_id=%s event=started", request_id)
time.sleep(delay_ms / 1000)
app.logger.info("request_id=%s event=finished", request_id)
if item_id != "sku-123":
return jsonify(error="item_not_found"), 404
return jsonify(item_id=item_id, available=7)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5001, threaded=True)
Service A: Checkout
Save as checkout.py. It validates the dependency representation and distinguishes a slow dependency from a failed connection. It generates a request ID rather than trusting one provided by an external caller.
import logging
import os
import time
import uuid
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
INVENTORY_URL = os.environ.get("INVENTORY_URL", "http://127.0.0.1:5001")
@app.get("/health")
def health():
return jsonify(status="ok", service="checkout")
@app.get("/v1/availability/<item_id>")
def availability(item_id):
request_id = str(uuid.uuid4())
started = time.monotonic()
status = 500
try:
with requests.get(
f"{INVENTORY_URL}/v1/items/{item_id}",
params={"delay_ms": request.args.get("delay_ms", "0")},
headers={"X-Request-ID": request_id},
timeout=(0.2, 0.6),
allow_redirects=False,
) as response:
if response.status_code == 404:
body, status = {"error": "item_not_found"}, 404
elif response.status_code == 400:
body, status = {"error": "invalid_delay"}, 400
elif response.status_code != 200:
body, status = {"error": "dependency_bad_response"}, 502
else:
data = response.json()
if (not isinstance(data, dict)
or data.get("item_id") != item_id
or type(data.get("available")) is not int
or data["available"] < 0):
raise ValueError("invalid inventory representation")
body, status = data, 200
except requests.Timeout:
body, status = {"error": "dependency_timeout"}, 504
except ValueError:
body, status = {"error": "dependency_bad_response"}, 502
except requests.RequestException:
body, status = {"error": "dependency_unavailable"}, 503
finally:
elapsed_ms = round((time.monotonic() - started) * 1000, 1)
app.logger.info("request_id=%s status=%s elapsed_ms=%s",
request_id, status, elapsed_ms)
result = jsonify(**body, request_id=request_id)
result.status_code = status
result.headers["X-Request-ID"] = request_id
return result
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, threaded=True)
Here checkout acts as a gateway for the inventory lookup: 504 means the upstream wait timed out; 502 means an unexpected status or invalid representation; 503 is our documented dependency-unavailable policy. These mappings are API design choices, not an automatic equivalence for every HTTP service. See HTTP server error semantics.
Choose Your Request Client: curl or Postman
Either curl or Postman is accepted. Both send HTTP requests to the same running checkout service. You still need two terminals to run inventory.py and checkout.py; Postman does not host those services. The reservation POST on slide 6 is a design exercise, not an implemented lab endpoint.
For curl, use the commands below in a third terminal. The -i option shows response headers and status; -w reports elapsed time.
For Postman desktop:
- Start both Python services, then open a new HTTP request in Postman.
- Select
GETand enterhttp://127.0.0.1:5000/v1/availability/sku-123. - Leave the body empty and select No Auth for this local lab. No custom request headers are required.
- Click Send. Inspect the response status, JSON body, response time, and
X-Request-IDresponse header. - Repeat with
/missing, then with?delay_ms=bad, then?delay_ms=2000on the known-item URL. Use the evidence table below for expected outcomes. - Keep Postman’s request timeout above the injected delay, for example
5000ms, so you observe checkout’s504rather than Postman stopping first. Checkout’s own dependency read timeout remains0.6seconds.
The Postman web app needs the Desktop Agent to reach local services; the desktop app is the simpler lab path. See the official Postman quick start, request settings, and local request troubleshooting.
Record the same evidence with either client. Client response time measures the whole call to checkout; checkout logs measure its own handler duration. Small differences are expected.
Run and Inspect
In terminal one, run python inventory.py. In terminal two, run python checkout.py. Use a third terminal for requests:
curl -i 'http://127.0.0.1:5000/v1/availability/sku-123'
curl -i 'http://127.0.0.1:5000/v1/availability/missing'
curl -i 'http://127.0.0.1:5000/v1/availability/sku-123?delay_ms=bad'
curl -i -w '\nHTTP=%{http_code} total=%{time_total}s\n' \
'http://127.0.0.1:5000/v1/availability/sku-123?delay_ms=2000'
Expect 200 with seven units, 404 for the missing item, 400 for invalid delay, and 504 after roughly 0.6 seconds for the delayed response on a typical local run. The measured time is an observation, not a precise guarantee. Match the returned request ID to both terminal logs. Inventory should log completion later even though checkout already timed out.
Stop inventory with Ctrl+C, then call the normal availability URL again. Expect 503. Call http://127.0.0.1:5000/health and expect 200: checkout is alive although the dependent workflow is unavailable. Restart inventory and confirm the normal request returns 200 again. Stop both processes with Ctrl+C when finished.
Evidence and Checkpoints
Record actual status, elapsed time, request ID, and a short explanation for each row. Keep logs free of credentials and personal data.
| Experiment | Expected outcome | Question to explain |
|---|---|---|
| Normal lookup | 200, available 7 | What does this result promise? |
| Missing item | 404 | How does the client distinguish absence from zero stock? |
| Invalid delay | 400 | Where is input validated? |
| 2000 ms delay | 504, late inventory completion | Did timing out stop server work? |
| Inventory stopped | 503; checkout health 200 | Which boundary failed? |
| Inventory restored | 200 | What evidence demonstrates recovery? |
For an additional contract-failure check, temporarily return available="seven" from inventory, restart it, and verify checkout returns 502. Restore the integer afterward. Explain why parsing JSON alone is insufficient validation.
If ports are occupied, stop your earlier lab process or change both the listening port and configured URL. If imports fail, activate the environment in that terminal. If you move this into Module 2 containers, bind the apps to 0.0.0.0 inside the containers and set INVENTORY_URL=http://service-b:5001 on their shared network. 127.0.0.1 in checkout’s container points to checkout itself.
Keep your two source files, dependency snapshot, evidence table, and contract sketch. Official assignment requirements, deadlines, and submissions remain in Canvas; this page is public practice guidance.
gRPC: The Same Lookup with a Typed Interface
REST emphasizes resources and HTTP semantics. gRPC exposes named service methods with generated message types. It commonly uses Protocol Buffers over HTTP/2. A unary call has one request and one response; streaming variants support a sequence in either or both directions. See gRPC core concepts.
Define once, generate bindings, then call remotely
%%{init: {"themeVariables": {"fontSize": "24px"}, "flowchart": {"wrappingWidth": 400, "nodeSpacing": 50, "rankSpacing": 12, "padding": 10}}}%%
flowchart TB
subgraph build[Build time]
direction LR
P["inventory.proto<br/>Define GetItem types"] --> G["Generate bindings<br/>Client + server code"]
end
subgraph runtime[Runtime]
direction LR
C["Checkout client<br/>GetItem sku-123"] -->|HTTP/2 + Protobuf| I["Inventory server<br/>Return available 7"]
end
build --> runtime
A proto defines GetItem and its messages. Generated bindings support both sides. At runtime the client sends item_id sku-123 and receives available 7; a deadline still bounds its wait.
syntax = "proto3";
package inventory.v1;
service Inventory {
rpc GetItem(GetItemRequest) returns (GetItemResponse);
}
message GetItemRequest {
string item_id = 1;
}
message GetItemResponse {
string item_id = 1;
int32 available = 2;
}
The numbers are stable wire identifiers, not field ordering. Do not reuse deleted field numbers; reserve them. Compatible wire changes can still change business behavior. An integer type does not enforce nonnegative stock. See Protocol Buffers evolution guidance.
After generating the Python bindings and implementing a server, a client call has this shape (illustrative, not a standalone runnable lab):
response = stub.GetItem(
inventory_pb2.GetItemRequest(item_id="sku-123"),
timeout=0.6,
)
In Python gRPC, this timeout sets the RPC’s deadline. Handle grpc.RpcError and inspect its status: examples include NOT_FOUND, UNAVAILABLE, and DEADLINE_EXCEEDED. These statuses do not map one-to-one to every HTTP status. Set deadlines explicitly; default calls can wait indefinitely. Propagation support depends on the language/runtime, and application work must cooperate with cancellation. Cancellation does not undo a committed write. See deadlines and cancellation.
| Dimension | Resource-oriented HTTP/JSON | gRPC with Protobuf |
|---|---|---|
| Contract | HTTP operations and schemas, often OpenAPI | Service methods and typed messages in proto |
| Tooling | curl, browsers, HTTP tooling; code generation available | Generated stubs and gRPC-aware tools |
| Browser use | Direct HTTP API access subject to browser policies | Commonly needs gRPC-Web or a gateway |
| Streaming | Possible using suitable HTTP streaming patterns | Unary and three streaming RPC shapes |
| Payload | Human-readable JSON is convenient for inspection | Binary encoding; schema-aware decoding |
| Failure handling | Explicit client timeouts and error contracts | Explicit deadlines and gRPC statuses |
| Evolution | Maintain schemas and behavioral compatibility | Maintain field numbers, types, and behavior |
| Performance | Measure payload, connections, server work, and tail latency | Measure the same factors; no universal speed guarantee |
For a public catalog consumed by browsers and external partners, discuss the advantages of HTTP tooling. For internal polyglot services requiring generated interfaces or bidirectional streams, discuss gRPC. Neither choice supplies authorization, idempotency, or business correctness automatically.
Optional extension: implement this proto service using the official Python tutorial, add a deadline, and repeat the slow-service experiment. Compare the observed status and developer workflow with REST. Do not claim a benchmark win from one request.
Project and Agentic Systems Connection
Sketch one synchronous edge in your project workflow: caller, callee, operation, schema, authenticated identity, authorization rule, time budget, and failure response. Bring a problem statement, team intent, and initial business workflow for instructor discussion. The full architecture proposal belongs to Module 4; follow Canvas for official checkpoint instructions.
An agent calling inventory is still a distributed client. A successful HTTP exchange does not prove a tool achieved the user’s business objective. Zero stock is a valid result, a timeout is an uncertain outcome, and an authenticated tool must still be authorized for the requested action. Have the agent report those distinctions before it promises fulfillment or repeats a state-changing call.
Knowledge Check
Does a 600 ms timeout prove inventory did not execute the request?
No. The caller stopped waiting. The request may not have arrived, may still be running, or may have completed with a lost response. A write needs an outcome lookup or a defined deduplication strategy before a retry is safe.
Is inventory:5001 a service identity?
It is a network location. A verified certificate or credential establishes identity; authorization still decides which operation that identity may perform.
Is adding a required request field compatible with an old client?
No. Old clients do not send it. An optional field with a documented default may be compatible, but validate behavior with old clients.
Does gRPC remove the need for latency budgets?
No. A generated stub still performs remote work. Set a deadline, propagate remaining time, and handle failure explicitly.
Why is HTTP 200 with available: 0 not a failed lookup?
The lookup succeeded and reported zero stock. Transport, protocol, and business outcomes are separate layers.
Key Takeaways
- Remote calls add serialization, latency, independent failure, and uncertain outcomes.
- A contract includes errors and semantics as well as field names and types.
- Discovery locates a service; authentication identifies a caller; authorization decides allowed actions.
- Set explicit timeouts and propagate remaining budgets rather than restarting the full budget at each hop.
- A timeout does not prove that the server stopped or that a write did not happen.
- gRPC changes the interface and tooling but does not eliminate distributed failure.
Additional Resources
- HTTP Semantics — RFC 9110 documentation
Required reference for method semantics and status codes; focus on sections 9 and 15.
- OpenAPI 3.1.1 documentation
Reference for machine-readable HTTP contracts.
- gRPC Core Concepts documentation
Required comparison reading for service definitions and RPC shapes.
- gRPC Deadlines documentation
Deadline behavior and propagation across downstream calls.
- Requests Timeout Behavior documentation
Connect and read timeout semantics used in the lab.
- Protocol Buffers Language Guide documentation
Optional reference for field numbers and schema evolution.