Module 3 · Published

Module 3: Network Communication with REST and gRPC

Design explicit API contracts, distinguish service location from identity, budget network latency, and observe how REST and gRPC calls behave under failure.

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

  1. Design a resource-oriented HTTP API with explicit success and error contracts.
  2. Explain serialization and compatible contract evolution across independent deployments.
  3. Distinguish service discovery, authentication, authorization, and correlation IDs.
  4. Allocate a latency budget and distinguish connect timeouts, read timeouts, and deadlines.
  5. Demonstrate success, slow responses, and dependency failure using two services.
  6. 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

OperationExampleIntended behavior
GET/v1/items/sku-123Read a representation without requesting a state change.
POST/v1/reservationsCreate a reservation; define duplicate-request behavior.
PUT/v1/preferences/customer-7Replace the resource at a known URI.
PATCH/v1/preferences/customer-7Apply a defined partial update.
DELETE/v1/reservations/r-42Remove 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:

  1. Start both Python services, then open a new HTTP request in Postman.
  2. Select GET and enter http://127.0.0.1:5000/v1/availability/sku-123.
  3. Leave the body empty and select No Auth for this local lab. No custom request headers are required.
  4. Click Send. Inspect the response status, JSON body, response time, and X-Request-ID response header.
  5. Repeat with /missing, then with ?delay_ms=bad, then ?delay_ms=2000 on the known-item URL. Use the evidence table below for expected outcomes.
  6. Keep Postman’s request timeout above the injected delay, for example 5000 ms, so you observe checkout’s 504 rather than Postman stopping first. Checkout’s own dependency read timeout remains 0.6 seconds.

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.

ExperimentExpected outcomeQuestion to explain
Normal lookup200, available 7What does this result promise?
Missing item404How does the client distinguish absence from zero stock?
Invalid delay400Where is input validated?
2000 ms delay504, late inventory completionDid timing out stop server work?
Inventory stopped503; checkout health 200Which boundary failed?
Inventory restored200What 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&lt;br/&gt;Define GetItem types"] --> G["Generate bindings&lt;br/&gt;Client + server code"]
  end
  subgraph runtime[Runtime]
    direction LR
    C["Checkout client&lt;br/&gt;GetItem sku-123"] -->|HTTP/2 + Protobuf| I["Inventory server&lt;br/&gt;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.

DimensionResource-oriented HTTP/JSONgRPC with Protobuf
ContractHTTP operations and schemas, often OpenAPIService methods and typed messages in proto
Toolingcurl, browsers, HTTP tooling; code generation availableGenerated stubs and gRPC-aware tools
Browser useDirect HTTP API access subject to browser policiesCommonly needs gRPC-Web or a gateway
StreamingPossible using suitable HTTP streaming patternsUnary and three streaming RPC shapes
PayloadHuman-readable JSON is convenient for inspectionBinary encoding; schema-aware decoding
Failure handlingExplicit client timeouts and error contractsExplicit deadlines and gRPC statuses
EvolutionMaintain schemas and behavioral compatibilityMaintain field numbers, types, and behavior
PerformanceMeasure payload, connections, server work, and tail latencyMeasure 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