Protect API response types, units and compatibility
Application background
A preview API returns a page title and the time spent fetching it. Two separate applications read that response. They expect durationMs to be a number measured in milliseconds, such as 12.
A provider refactor could return "12" as text, or return 0.012 seconds while keeping the old field name. Both changes can break a caller even if the response still looks like valid JSON.
The response contract that callers rely on
This is a proposed API agreement for the exercise, not a generated schema claiming the implementation is complete:
| Field | Required meaning | Example |
|---|---|---|
title |
String containing the selected page title | "Guide" |
durationMs |
Non-negative number in milliseconds, not numeric text or seconds | 12 |
{"title":"Guide","durationMs":12}
The supplied decoder accepts that shape and rejects "12" as the duration. It cannot infer units from a JSON number, so the contract must state them.
A contract describes meaning as well as shape. Agreement on field names alone does not establish agreement on types, units or error behavior.
Your assignment
Deliver: Write the producer/consumer response agreement and compare representative responses against it. Include both type changes and unit changes that leave the JSON shape intact.
Required behavior: The provider and consumers agree on field presence, runtime types, units and error shapes. Static language declarations do not validate bytes received over HTTP. Compatibility is demonstrated with the supported old consumer behavior.
The required first milestone is a working local implementation of the behavior above. The numbered implementation steps define the scope. The cloud architecture is a later extension, not something the starter has already provisioned.
Get the code and run the supplied example
The code is in the public junior-to-staff repository. Install Git and Python 3.12+. No AWS account or Python packages are required for this first run. If you already have a checkout, use it and skip cloning.
git clone https://github.com/Soulful-Iris/junior-to-staff.git
cd junior-to-staff
python3 examples/architecture-starts/02_the_contract_nobody_breaks_by_accident.py
Supplied file: examples/architecture-starts/02_the_contract_nobody_breaks_by_accident.py. You can also read or download the source here (download file, source below).
Read the supplied code · 02_the_contract_nobody_breaks_by_accident.py
"""Local mechanism demonstration for 02-the-contract-nobody-breaks-by-accident. No AWS resources are created."""
def decode(body):
duration=body.get('durationMs')
if not isinstance(body.get('title'),str): return 'invalid title'
if isinstance(duration,bool) or not isinstance(duration,(int,float)) or duration<0: return 'invalid durationMs'
return 'shape accepted; unit meaning still requires the contract'
for body in [{'title':'Guide','durationMs':12},{'title':'Guide','durationMs':'12'}]: print(body,decode(body))
This program is a mechanism demonstration: it runs the small scenario in one process and prints the result. It is not an HTTP service, a complete application, or an AWS deployment. A successful run demonstrates this mechanism only. It does not establish the workload or failure guarantees of the application you will build.
Example output from the supplied run:
Generated IDs and timestamps may differ. Compare the state transitions and outcomes.
{'title': 'Guide', 'durationMs': 12} shape accepted; unit meaning still requires the contract
{'title': 'Guide', 'durationMs': '12'} invalid durationMs
Set up your implementation workspace
Create work/02-the-contract-nobody-breaks-by-accident/ in your checkout (or use a separate repository). Copy the supplied mechanism into that directory as mechanism.py, then extract its state transitions into functions you can call from your implementation. The record and module names below describe what you must implement. They are not a promise that files with those names already exist. Keep a README.md beside your implementation with its exact run commands and observed results.
Local components and state to implement
This table names the records, interfaces or decision inputs for your deliverable. Unless a name is explicitly linked to supplied source above, it is something you create. Implement the local state transitions first, then connect the HTTP, storage or worker boundaries required by the steps.
| Record / module | Key or interface | Responsibility |
|---|---|---|
| response_schema | title:string,durationMs:number >= 0 | Runtime validation plus documented units. |
| consumer_examples | supported client version and input/output | Actual decoding and usage behavior. |
| error_contract | status,code,message,request_id | Stable failure interpretation across versions. |
Implement the assignment
1. Write the wire contract
Use the existing request/response boundary lab as the starting application. Document exact JSON, content type, required fields, units and error responses. Keep successful and failed examples for each supported consumer.
2. Validate the runtime boundary
Parse and validate received data before domain logic uses it. Check booleans separately when the language treats them as numbers. Gateway request validation, where configured, does not automatically validate provider responses or semantic units.
3. Exercise old and new consumers directly
Send the numeric response and then the string response through the actual decoding path. Add an optional field and observe whether each supported client tolerates it. Do not assume every decoder ignores unknown fields.
4. Evolve with an explicit adapter
Introduce a new field/version for changed units or meaning, preserve the old representation during the support window, and derive both from one canonical value. Record owner and retirement evidence without coupling the guide’s deployment to new checks.
Demonstrate the completed local result
| Action | Expected visible result |
|---|---|
| Run the starting program | Numeric 12 is accepted. String "12" is rejected. |
| Change milliseconds to seconds without renaming | The semantic example reveals the incompatible meaning. |
| Send an unexpected content type | The API follows the documented reject/parse policy instead of silently bypassing validation. |
Handoff: In your implementation README, include the start command, one successful operation, the failure case above and the resulting stored state or decision. State which dependencies are simulated. Someone with a fresh checkout should be able to reproduce this without your chat history.
Workload assumptions and capacity decisions
These are constructed exercise assumptions. The stated workload is a design target. The local demonstration does not establish that throughput. Use the estimation constants to check units before choosing capacity.
| Input or objective | Calculation / consequence |
|---|---|
| Two consumers released independently | A provider rollout cannot assume simultaneous consumer upgrades. |
| 12 milliseconds versus "12" | The latter violates the numeric runtime contract even if a permissive caller coerces it. |
| 0.012 seconds in durationMs | Type-valid but semantically wrong. Units belong in the contract and examples. |
Map the local implementation to AWS
Deployment status: local only. Running the supplied command creates no AWS resources and configures no cloud connections. The diagram is a proposed deployment of the completed application. Each box needs either a deployed runtime, a provisioned service or an explicitly external dependency.
Read the diagram by following the arrows from the entry point: application code accepts the request or event, the state owner commits it, and any worker produces the later result. The table ties those roles to code and adapter work. Multiple boxes do not imply multiple Python files already exist.
The gateway handles a configured request boundary. Provider response validation and consumer decoding are separate application responsibilities.
| Local responsibility | Cloud destination and role | Implementation still required |
|---|---|---|
| Local HTTP boundary or the endpoint you will add | Amazon API Gateway: HTTP request boundary | Create routes and an integration. Translate requests and responses and configure identity validation. |
| Python operation or worker function | AWS Lambda: provider application | Write a Lambda event adapter, package its dependencies and give its role only the required resource actions. |
| Application or worker process | Amazon ECS: consumer application | Build a container and task definition. Supply configuration, task roles and graceful shutdown behavior. |
| Local file, object fixture or exported payload | Amazon S3: versioned contract examples | Implement upload/download and metadata adapters, scoped access, object naming, retention and incomplete-upload cleanup. |
| Local counters, timestamps and diagnostic output | Amazon CloudWatch: boundary diagnostics | Emit bounded metrics and logs, build the named operational view and configure retention and access. |
Provision resources, then connect the application
| Resource or boundary | Initial configuration and reason |
|---|---|
| Gateway | Configure body models/content-type handling deliberately. Validate domain formats in the handler. |
| Versioning | Store schemas and supported consumer examples with the implementation commit. |
| Diagnostics | Log safe schema error paths, not entire sensitive request/response bodies. |
Use one disposable AWS environment for the cloud exercise. Put the named resources in infra/template.yaml or your existing IaC tool, pass resource IDs through configuration, and scope each runtime role to its own tables, buckets and queues. The diagram is a design to implement. It is not a claim that these resources have been deployed. Record the commands you used to deploy and remove the exercise resources.
For concrete provisioning commands, configuration wiring and cleanup, use the AWS foundation guide. It includes a deployable table/queue/object-storage foundation and explains which application and service adapters you still implement.
A provisioned queue or table does not make the local program use it. Configure resource IDs in the deployed runtime, replace the local adapter, and replay the same successful and failing operation against that runtime. Record the deployed commit and observable result, then remove the disposable resources using your infrastructure tool.
Extend the design after the baseline works
Add a strict consumer that rejects unknown fields. Decide whether an additive response change requires a version for that supported client, and document the evidence behind the decision.
Follow-up scenarios and worked designs
Follow-up 1 · The request content type differs
A caller sends text/plain and a malformed numeric query parameter. Which gateway checks apply?
Worked design and implementation
REST basic validation checks required parameter presence/nonblank values, not numeric formats. Body validation requires a matching model or a deliberate $default/reject policy. Validate domain types in the handler.
Trace one rejected request. Consider POST /items?limit=abc with Content-Type: text/plain. A required parameter is present, but it is not a valid integer. Define allowed content types explicitly and reject unsupported ones before interpreting the body. Parse and validate limit in the handler with bounds.
Deliver a small decision table: unsupported media type → 415, malformed supported body → documented 400, invalid numeric limit → documented 400. Keep these application choices separate from the gateway's particular validation features. A gateway presence check is not proof of domain validity.
Follow-up 2 · The provider evolves
Add an optional field while an old consumer remains deployed. What should fail?
Worked design and implementation
The compatible addition should pass agreed consumer tolerance checks. Removing a required field or changing its meaning must fail. Include strict-consumer behavior explicitly instead of assuming all additions are harmless.
Compare old and new consumers. Add optional display_title while retaining required url. A tolerant consumer ignores the addition, while a strict consumer that rejects unknown fields may still break. Record that difference rather than declaring every additive JSON change compatible.
Supply two serialized responses and the consumer behavior for each. Then change the meaning of an existing numeric field without changing its type and show why schema shape alone misses the break. Name the owner who approves the wire contract and the deployed clients included in the compatibility claim.
Supplied mechanism practice
- Executable request/response boundary lab — includes its own run command, fixtures and validation limits.
These exercises verify specific boundaries. Completing their reference tests does not implement or assess the full project.