Appendix C
Advanced control boundaries in web applications#
- Created
- Updated
Use this appendix when a design depends on state continuity across requests, HTTP method semantics, transactional invariants, replicated state during network partitions, client activation, runtime-generated interfaces, a detailed application contract, event-view rebuilding, or endpoint and cache policy. The Architecture chapter keeps the decisions that every project must recognize; the sections below develop implementation choices and audits that are useful only when those boundaries are present. The Scaling chapter introduces the operational choice when replicas cannot communicate; the CAP proof and lab develop that case.
C.1 Stateless requests still depend on state#
Statelessness describes what a service needs to process one request, not whether the application remembers anything.
The final 2026-07-28 revision of the Model Context Protocol (MCP) removes the initialization handshake and the Streamable HTTP Mcp-Session-Id header.
Servers must not infer protocol context from earlier requests on the same connection; the core carries no protocol session across calls (Model Context Protocol, 2026).
This is a versioned protocol example, not a requirement that the course project expose MCP or that older MCP traffic use the same design.
The table below compares what a client carries forward and what the deployment must still provide.
| Continuity design | What a later request carries | Deployment obligation |
|---|---|---|
| Hidden protocol session | A session identifier | Route to the instance that owns it, or share the protocol-session state |
| Explicit application handle | A resource handle such as workspace_id |
Authorize the operation and resolve the referenced state from every eligible instance, or retain affinity |
An explicit handle moves continuity rather than deleting it. An application may select state from the authenticated principal and ordinary request parameters; when workflow state cannot be derived from them, a service can mint a handle and require later calls to return it. The service still has to store or reconstruct the referenced state and define its owner, lifecycle, concurrency rules, expiry, and cleanup. A handle is an address, not authorization: authenticate the caller, then authorize that principal to perform the requested operation on the referenced tenant or workspace on every request. If the state remains available only on the instance that minted the handle, affinity has merely moved behind a new identifier.
Retries add another state boundary. If a mutation commits but its response is lost, a retry may reach a different instance and apply the mutation again. Do not assume that a protocol request identifier makes a domain operation idempotent. Use an application-level command identifier or idempotency key, bind it to the caller, handle, operation, and relevant arguments, and store the mutation and reusable result atomically in the authoritative application state.
State placement audit. Assume two interchangeable server instances, A and B, share one application store.
Alice's validated token identifies principal alice in tenant T1 with events:write; creating a workspace grants her access to it.
Bob's token identifies principal bob in the same tenant with the same scope, but he has no access to Alice's workspace.
The server derives these identities from the validated tokens, not from client metadata or a resource handle.
- Alice calls
create_workspaceand receivesW1. - Alice calls
record_event(W1, work_item_id=I42, command_id=C7, type=work_started); instanceAcommits the event, but the response stream breaks. - The client retries on instance
Bwith a new protocol request identifier and the sameC7. - Bob calls
record_event(W1, work_item_id=I42, command_id=C8, ...). - The workspace is now
archived, and the stated lifecycle makes archived workspaces read-only; Alice callsrecord_eventagain.
For each request, record the authenticated principal and scope, authoritative state before the request, decision to apply, return, or reject, and the invariant or evidence supporting that decision.
Design the flow so C7 produces one event and its retry returns the original result, Bob cannot act merely by knowing W1, and the archived write returns a defined application error such as workspace_archived rather than silently recreating state.
Finish by naming which information is carried by the client and which state remains authoritative on the server.
C.2 Distinguish HTTP method semantics from domain retry safety#
The Pressure chapter's exercise starts from the domain contract: inspect all promised effects of one logical command. HTTP adds standardized method semantics that clients and intermediaries can use. In RFC 9110, sections 9.2.1–9.2.2, safe means the client requests no state change, while idempotent means repeating the request preserves its intended server effect.
| Method | Safe? | Idempotent by method semantics? |
|---|---|---|
GET, HEAD |
Yes | Yes |
PUT, DELETE |
No | Yes |
POST |
No | No general guarantee |
Identical responses are unnecessary.
For example, an API could return 204 after deletion and 404 on a retry because the resource is already absent; the intended removal still happened once.
The RFC also permits incidental effects such as an access-log entry per request.
The exercise's domain activity event is different: exactly one event for the transition is a promised application outcome.
A POST command can have an application-specific retry guarantee.
For the tracker, one design binds a stable command key to the caller, workspace, operation, and relevant arguments, then atomically enforces that key with the state change, activity event, and reusable result.
Specify conflicting payloads, key retention, and concurrent attempts as part of the contract.
The key and HTTP method alone do not implement that guarantee; the state-placement audit above and Appendix F's effect audit locate its enforcement.
HTTP semantics check. Explain whether each observation contradicts idempotency: a repeated deletion returns a different status; repeated completion adds another promised domain event; repeated retrieval adds an incidental access-log entry.
For each, name the intended effect and distinguish it from the response or diagnostic record.
Then explain what evidence a client would need before automatically retrying the application's POST command.
C.3 Preserve durable invariants with transactions#
A database transaction groups operations into one unit with four commonly named properties: atomicity, consistency, isolation, and durability, or ACID (Härder & Reuter, 1983). Atomicity means the operations commit together or not at all. Consistency means a committed transaction leaves the data within its declared invariants; the database cannot invent those application rules, so they still have to be expressed through constraints, guarded writes, or validated domain logic. Isolation describes how concurrent transactions are allowed to observe and affect one another. The exact guarantee depends on the selected isolation level rather than on the mere presence of a transaction. Durability means a successful commit survives the failures covered by the storage system's contract.
The work-item lifecycle contains a useful transaction boundary. A successful transition changes the current state and records the corresponding activity event. If either write escapes alone, the activity feed and work item disagree. The same invariant is threatened when two requests make a decision from the same old state or when the application returns success before persistence is durable.
C.3.1 ACID transaction lab#
Choose one question at a time. For a failed second write, compare application-managed writes with one transaction and inspect whether state and event commit together. For concurrent requests, inspect the displayed stale-check program before comparing its read-committed and serializable histories; compare the conditional-write alternative separately. For restart, keep the transaction fixed and change only whether acknowledgment precedes or follows the durable commit.
ACID transaction lab
Keep state and history consistent
Predict the stored state and client result, then reveal an authored trace. These examples execute no database operations. Transaction scope, the concurrent program and acknowledgement timing answer different questions.
This is a deterministic transaction model, not a benchmark of a particular
database. Real isolation and durability depend on the database, transaction
boundaries, constraints, storage configuration, and failure model. For
actual atomic rollback and process-restart records, try the optional
course workspace
(persisted-retry/). It does not reproduce the isolation-level
comparison.
The program traces execute no SQL and do not imply that every database implements an isolation level or durable commit in the same way.
For actual atomicity and restart practice, open persisted-retry/README.md in the course workspace.
Its checks concern the receiver's SQLite transaction and process failure; they do not compare PostgreSQL isolation levels.
For an implementation-specific claim, identify the real transaction boundary, constraints, isolation setting, acknowledgment point and documented failure model, then test the claimed property against that storage path.
C.4 Partitions and replicated state#
C.4.1 A register proof#
Consider a register initially holding 0, replicated at nodes L and R, with communication between them partitioned:
- In execution A, no write occurs and a client reads at R.
A valid read returns
0. - In execution B, a client writes
1at L and receives success, then another client begins a read at R. Linearizability requires1, since no later write occurs. - R has received no message about the write, so its local observations can be identical in A and B.
Returning
0violates B; guessing1violates A. Waiting indefinitely for communication sacrifices availability, and refusing the read does not fulfill the register's read contract.
Requiring L to wait before acknowledging the write moves the availability loss to the write; it does not remove the conflict. This is the indistinguishability argument behind the impossibility result (Gilbert & Lynch, 2002).
A system that requires a majority quorum may preserve one history by rejecting or delaying a request that reaches an isolated minority. A system that accepts writes on both sides may keep responding, but the replicas can diverge and later need domain-specific reconciliation. Neither label chooses the right policy on its own. The decision depends on which operations may be stale, rejected, delayed, merged, compensated, or repaired without violating the domain.
C.4.2 CAP partition lab#
Split one region from a two-region majority and apply the same two work-item transitions under both policies. Inspect the replies before healing the network: reconciliation can change stored state, but it cannot retract a success that a user already observed.
CAP partition lab
Compare write policies during a partition
Helsinki becomes isolated from the Frankfurt–Dublin majority. Alice and Bob then try to start the same work item from opposite sides of the split.
The model holds workload, topology, and failure constant so the policy is visible. The write-invariant check alone does not establish linearizability for all reads and writes. Real systems also define quorums, timeouts, leases, and conflict resolution.
The lab models one write invariant and a fixed topology; preserving that invariant alone does not establish linearizability for every read and write. Linearizability's real-time ordering is also distinct from ACID atomicity, which concerns a transaction taking effect entirely or not at all. Real systems need explicit definitions for reads as well as writes, failure detection and timeouts, quorum membership, leases, conflict resolution, and the consistency guarantee exposed to clients.
C.5 Rebuild a view without repeating its effects#
A retained event history can support several derived views when it records the inputs needed to compute them. Kreps (2013) explains how deterministic processing of the same ordered history reconstructs state. Changing the computation can instead produce a new view from that history.
Suppose a workspace dashboard currently counts all activity and now needs separate counts for started and completed work.
An illustrative retained fixture contains E1: work_started, E2: work_completed, and E3: work_started, all for workspace W1 in that order.
Replaying those records through the new aggregation gives two starts and one completion.
The rebuild writes a separate summary and records its last processed position; the application can keep serving the old summary until the new one catches up to an agreed position and is verified.
Events arriving during the rebuild must be included through that cutover position, then processing must continue without skipping or double-counting them.
Recomputing the summary must not resend the notifications originally triggered by those events. Keep the projection that updates the summary separate from notification delivery. If delivery itself needs recovery, its durable record and receiver-enforced idempotency belong to the effect boundary described in Appendix F. Rebuilding derived data and resuming a workflow are different uses of history.
Record the event identities, workspace, type, schema and aggregation versions, ordering, retained starting point, and acceptable summary lag. Expired or incomplete records may prevent a rebuild; the course tracker's activity history does not necessarily contain every work-item field or permission. Name which views the retained facts support and who owns their rebuild and freshness.
C.6 Model valid data combinations#
A transaction constrains how a valid state change becomes durable. Constructive data modeling asks which combinations ordinary code should be able to construct before that boundary is reached. In The Unreasonable Effectiveness of Constructive Data Modeling, Alexis King describes defining the supported "positive space" with ordinary records and tagged variants, then using exhaustive case analysis where the language supports it (King, 2026). King's earlier Parse, Don't Validate frames a boundary check as a transformation that returns a more precise representation carrying what it learned rather than only reporting success (King, 2019). These are design arguments rather than comparative evidence, and the goal is not maximum type precision.
For the result of a state-transition decision, a loose record does not require the payload that each outcome needs:
type TransitionDecision = {
accepted: boolean;
next?: WorkItem;
current?: WorkItem;
event?: ActivityEvent;
reason?: TransitionError;
};
It permits accepted: true without a next item or event, and accepted: false without a current item or reason.
Model the required payload for each branch instead:
type TransitionDecision =
| { kind: "accepted"; next: WorkItem; event: ActivityEvent }
| { kind: "rejected"; current: WorkItem; reason: TransitionError };
The discriminator makes each branch's required payload explicit, and narrowed type-checked code cannot treat a rejected decision as if it exposed a transition event. It does not prove that the event matches the correct transition unless the inner types or constructor also enforce that relationship. The value is an in-memory decision, not a durable fact. Untrusted, untyped, or deserialized input crossing a boundary still needs runtime parsing. Before committing a decision, the request path must authenticate and authorize the caller; the persistence operation must conditionally match the authoritative state or version, then atomically update the item and append the event. Malformed deserialization, mutation, or unsafe casts can bypass the representation, while concurrency and retries can violate surrounding invariants even when each individual value is valid.
Selected-invariant check. In the specification or acceptance material you maintain, show one broad record or result and one contradictory value it permits. Replace it with a stack-native tagged union, sealed hierarchy, or guarded constructor. Where the stack supports checked variants, add a compile-fail type fixture; add a runtime test for any parser or guarded constructor at a data boundary, and reuse an existing supported-behavior test. Record one important invariant that remains enforced elsewhere, such as authorization or atomic concurrent transition.
C.7 Activate delivered interfaces#
| Approach | Activation unit | Main benefit | Main constraint |
|---|---|---|---|
| Hydration | Selected client component trees | Reuses server HTML for interactive regions | Runs client code to recover state and listeners |
| Progressive hydration | Ordered portions of a client tree | Prioritizes important interaction | Selected client regions still require hydration |
| Islands architecture | Explicit interactive regions | Static HTML elsewhere; work can be deferred or avoided | Shared state, navigation, and communication across islands |
| Resumability | Serialized state and handler boundaries | Continues without replaying the application | Serializable state and framework compiler or runtime constraints |
These approaches describe different choices about boundaries and activation, and they can be combined. Islands can use hydration for their interactive regions. A page can also combine server-only output with hydrated client components: React Server Components keep their implementation out of the browser bundle while composing their output with Client Components.
Islands partition a page into static and interactive regions and attach a loading strategy to each interactive region. Resumability changes how execution crosses the server-client boundary: component boundaries, application state, and listener references are serialized into the delivered HTML so the browser can continue without replaying the application (Vepsalainen et al., 2024). A resumable framework can therefore create fine-grained islands automatically.
The strongest fit for explicit islands is an application with meaningful static regions and a smaller amount of separable interactivity. Research on edge-powered islands found that work could be deferred or avoided, but also concluded that the benefit becomes harder to obtain as an application grows more dynamic and difficult to divide into independent portions (Vepsäläinen et al., 2025). Inter-island state, navigation, and communication then become architectural costs rather than incidental implementation details.
C.7.1 Follow one control from HTML to interaction#
Use Table C.3 to compare one server-rendered event form:
HTML arrives → content can be displayed → required behavior becomes available
→ user submits → handler validates → authoritative result renders
For hydration, identify which client tree runs to recover state and listeners before the submit handler is ready. For an island, identify the region and the event that triggers its loading; the island may still hydrate. For resumability, identify the serialized state and handler reference that permit continuation without replaying the application. A delayed dynamic import by itself does not establish resumability.
Before inspecting a real page, predict which work runs before the first interaction and which work the interaction may wait for. As optional depth, use one production build of your chosen project: preserve the initial HTML, inspect its network and execution records, then submit the form and verify the accepted result. Record the activation strategy, state owner and communication path you can support from those records. One inspected build does not compare frameworks, and this exercise does not require implementing several frameworks or assuming typical transfer sizes.
C.8 Runtime UI generation changes the control boundary#
The earlier sections ask where a view is produced and how it becomes interactive. A separate question is how much of its structure and behavior model output may choose at runtime. A component written by a coding agent during development is not runtime-generative UI in this sense: once reviewed and committed, it is ordinary application code at runtime. The boundary expands as runtime output moves from filling a host-selected component to selecting component composition or executable behavior.
Casas (2026) describes a practical spectrum from fixed components, through declarative descriptions rendered from an approved catalog, to generated executable code. Treat this as a design lens rather than a settled taxonomy. The table below shows how the application's control obligation expands with the model's runtime freedom.
| Runtime model output | Application must control | Main trade-off |
|---|---|---|
| Data for one approved component | Component implementation, behavior, and prop schema | Predictable behavior with little freedom over layout |
| Declarative descriptor using approved components/actions | Schema, renderer, design system, and action registry | Composition within a grammar, with validation and schema-evolution burden |
| Generated HTML, CSS, and JavaScript | Isolated execution boundary, mediated data/tool bridge, and limits | Broadest expressiveness with the largest failure, review, and containment load |
This axis can be combined with server-side or client-side rendering, hydration, islands, or resumability. It does not replace those choices. A declarative descriptor is still untrusted input: reject unknown components and actions, validate data bindings and accessible names, bound size and nesting, and authorize an action when it is invoked. Resolve action identifiers through trusted application code; naming an action in generated data does not grant authority to perform it.
Executable generated UI expands the boundary further. It needs a separate-origin sandbox or equivalent isolated runtime without ambient credentials, a narrow bridge to host data and tools, explicit network, storage, navigation, and tool permissions, resource limits, observability, and a safe fallback. Running generated code in a browser does not by itself provide that boundary.
C.8.1 Show the context behind a generated view#
A generated view can satisfy its schema while concealing why particular information was selected or omitted. Show a compact context receipt that lets the user inspect the inputs and transformation behind the view. For example, a lecture-material selector could display:
Inputs: aggregate counts from predefined audience choices and a named revision of the curated reading list.
Sent to the model provider: those counts and reading-list entries; no names, individual responses, or private notes.
Transformation: selected and ordered existing readings; the complete list remains available.
External action: none; the result only changes this view.
This is an illustrative design, not a claim about a deployed system. Populate the receipt from application-controlled input and action records, not the model's recollection of what it used. Keep it accurate when context changes or a deterministic fallback replaces generation, and describe any additional data the application sends. Offer a route to inspect the source material and revise the selection criteria.
The receipt makes data use and selection inspectable; it does not itself enforce privacy or authorize an action. Those boundaries still belong to the application, and the receipt should avoid exposing sensitive input values merely to demonstrate transparency.
C.8.2 Field note — Constrain generation around the hard part#
For Slideotter, reliable layout was essential. Geometry, text-fit, and render-baseline checks appeared before the browser studio.
This left geometry, spacing, overflow, and media placement under application control while the model selected content and supported layouts. Declarative layout expansion allowed variety within that contract. I gave the model freedom around the property I needed the system to enforce reliably: layout.
C.9 Descriptor boundary audit#
Create one small event-summary data fixture and keep it unchanged. Then:
- Define the prop schema and payload for one fixed summary component.
- Define minimal prop schemas for an approved registry such as
metric,eventList, andbutton, plus a small action allowlist. - Express the same fixture as a declarative descriptor.
- Reject unknown components, raw HTML or script, unregistered actions, controls without accessible names, and excessive nesting. For each rejection, name the enforcing layer, error, and test.
- Use the fixed read-only summary as the fallback.
- Identify the additional containment and evidence required before the application could execute model-generated code.
The descriptor schema and action vocabulary are API contracts. The next section examines what follows once such contracts cross client and server boundaries.
C.10 Choose an application interaction style#
An interface distributes knowledge between consumers and providers: which addresses or operations exist, how data is represented, what errors mean, and how either side can change independently. Several interaction styles can expose the same behavior while placing coupling in different locations.
| Style | What the consumer knows in advance | Where change pressure concentrates | Typical operational concern |
|---|---|---|---|
| Resource-oriented HTTP | Resource addresses, representations, methods, and statuses | Resource and representation compatibility | Caching, idempotency, pagination, and versioning |
| Hypermedia-driven REST | Entry points, media types, and stable relation semantics | Relation semantics and completeness of advertised controls | Link validity, client interpretation, and adoption |
| GraphQL | A typed schema and the fields needed for each operation | Schema governance, field evolution, and deprecation | Authorization, resolver behavior, and query cost |
| RPC | Named methods and request or response message contracts | Method and message compatibility | Deadlines, retries, idempotency, and streaming flow |
Resource-oriented HTTP uses the uniform method and status semantics of HTTP. Hypermedia-driven REST adds links or controls so representations advertise available transitions; HATEOAS is therefore a REST constraint rather than a separate peer to REST (Fielding, 2000). GraphQL lets a consumer select a response shape against a typed schema. RPC exposes named operations and explicit messages. GraphQL and RPC commonly travel over HTTP, so transport and application interaction style remain separate choices.
Event streams, queues, webhooks, and publish-subscribe interfaces add another decision: communication can become asynchronous. That can reduce temporal coupling while making delivery guarantees, ordering, replay, idempotency, and schema evolution explicit parts of the contract.
C.10.1 Compare four annotated exchanges#
Hold one intent constant: start work on item 42 and obtain its current state, title, workspace and available next action. These compact exchanges are illustrative contracts, not executed requests or interchangeable protocol definitions. Every server still owns authorization, lifecycle validation and the atomic state-and-event commit.
Resource-oriented HTTP. The consumer knows the route, PATCH representation and state vocabulary in advance.
PATCH /work-items/42
Content-Type: application/json
{"state":"in_progress"}
HTTP/1.1 200 OK
Content-Type: application/json
{"id":"42","state":"in_progress","title":"Prepare room","workspace":{"id":"w7","name":"Course"},"allowedTransitions":["done"]}
Requesting an assignee name requires a documented representation extension or another known resource request. Resource URLs and HTTP methods alone do not give the client a discoverable workflow.
Hypermedia-driven REST. The consumer knows the entry resource and the media type's start relation, including its POST semantics; it follows the advertised address.
GET /work-items/42
HTTP/1.1 200 OK
Content-Type: application/vnd.course.work-item+json
{"id":"42","state":"open","_links":{"start":{"href":"/actions/start-42"}}}
POST /actions/start-42
HTTP/1.1 200 OK
Content-Type: application/vnd.course.work-item+json
{"id":"42","state":"in_progress","title":"Prepare room","workspace":{"id":"w7","name":"Course"},"_links":{"complete":{"href":"/actions/complete-42"}}}
An advertised assignee relation can supply the next address, but the client must already understand that relation's meaning.
Changing a URL does not require client reconstruction when it actually follows the control; changing relation semantics still requires coordination.
GraphQL. The consumer knows the schema and selects the result fields; the transport status is separate from the operation's result and errors.
mutation Start($id: ID!) {
startWorkItem(id: $id) {
item { id state title workspace { name } availableActions }
}
}
variables: {"id":"42"}
result:
{"data":{"startWorkItem":{"item":{"id":"42","state":"in_progress","title":"Prepare room","workspace":{"name":"Course"},"availableActions":["complete"]}}}}
Adding assignee { name } changes the selection when that field is available.
The server still has to authorize and execute its resolver work; one endpoint is not a cost bound.
RPC. The consumer knows the service, method and message schema, often through a generated client.
WorkItemService.StartWork({work_item_id: "42"})
=> WorkItemView {
id: "42", state: IN_PROGRESS, title: "Prepare room",
workspace_name: "Course", available_actions: [COMPLETE]
}
Returning an assignee name requires a compatible message change or another method call. The function-like notation does not remove deadlines, ambiguous outcomes or retry obligations.
Use Table C.5 to locate the consumer knowledge in each exchange. No style wins every row: choose from consumers, change patterns, network conditions, tooling and ownership. In the project's chosen style, preserve one old consumer request or script, add an optional related field on the server, and rerun that unchanged consumer against a fresh fixture. Check its expected response and the state/event outcome; then make an intentionally incompatible change in a disposable copy and confirm that the check detects it. This optional extension uses the contract exercise below and the existing project, rather than requiring four API implementations.
C.11 Executable HTTP contracts#
An API description becomes more durable when representative requests and expected responses are executable.
Hurl expresses HTTP sessions in plain text and can assert status codes, headers, and response bodies without depending on the application's implementation language.
For example, check that moving a work item into its active stage updates its state and records one corresponding activity event.
The routes and JSON below are illustrative; adapt them to the application's public contract.
Before each run, seed workspace workspace-9 with an open work item item-42 and no work_started event for that item.
POST {{base_url}}/work-items/item-42/transitions
Content-Type: application/json
{
"to": "in_progress",
"actor": "student-7"
}
HTTP 200
GET {{base_url}}/work-items/item-42
HTTP 200
[Asserts]
jsonpath "$.id" == "item-42"
jsonpath "$.state" == "in_progress"
GET {{base_url}}/events?workItemId=item-42&type=work_started
HTTP 200
[Asserts]
jsonpath "$.items" count == 1
jsonpath "$.nextCursor" == null
jsonpath "$.items[0].type" == "work_started"
jsonpath "$.items[0].workItemId" == "item-42"
jsonpath "$.items[0].workspaceId" == "workspace-9"
jsonpath "$.items[0].actor" == "student-7"
Save the scenario as lifecycle.hurl and supply the instance's base URL:
hurl --test --variable base_url=http://localhost:3000 lifecycle.hurl
This file is both an example of the interface and a deterministic integration test. It can be run locally or in continuous integration, and a failure points to the request or assertion that no longer matches the implementation. If implementations A, B, and C expose a compatible HTTP boundary, the same scenario can be run against each one by changing the base URL rather than rewriting the test in each implementation's language. These checks cover one successful transition; separate rejection, retry, and concurrency cases are needed to establish their corresponding guarantees.
Hurl does not replace browser testing: it checks the HTTP boundary, while a tool such as Playwright checks behavior through a browser, including rendering and interaction. Although Hurl can repeat requests and assert response duration, a small contract scenario is not evidence that the system will behave well under representative load. The common project specification does not require an HTTP API, so this technique is optional for implementations that expose one rather than a universal project requirement.
C.12 Use the boundary that matches the risk#
These techniques solve different problems. Explicit handles and idempotency protect continuity across requests; transactions protect durable invariants under concurrency and failure; constructive models reduce contradictory in-memory states; activation approaches move browser work; descriptor schemas constrain runtime composition; isolation contains executable output; interaction styles distribute coupling; and HTTP examples preserve an interface contract. Choose the smallest boundary that makes the relevant decision observable and enforceable.
C.13 Endpoint and cache policy#
Use these details when automated demand or fragmented cache reuse is a relevant pressure in the Architecture chapter.
C.13.1 Automated consumers change demand and exposure#
The Robots Exclusion Protocol lets a cooperating crawler discover access preferences in /robots.txt.
It is not authorization or a content-security boundary, and listing a path there makes the path discoverable.
Information that must remain private still needs an application-layer access control; public information should be designed with the assumption that a permitted consumer can copy it.
Rate limiting controls resource use rather than deciding whether every automated consumer is malicious.
An HTTP 429 Too Many Requests response can explain the limit and carry Retry-After, but RFC 6585 deliberately leaves the identity and counting policy to the application.
Limits based only on an IP address can group unrelated users or be distributed across many addresses, while limits tied to an account, tenant, capability, or costly operation require their own fairness and privacy decisions.
The OWASP automated-threat catalog treats scraping as one form of automated use among several, and its practical implication here is to begin from the endpoint and intended outcome rather than from a generic “block bots” goal. For one exposed path, record the legitimate consumers, information returned, authoritative action or cost triggered, likely abuse pattern, first useful signal, control point, acceptable false-positive cost, and recovery or override path. Cache safe public reads, minimize exposed and retained data, protect expensive or consequential actions, and observe origin work per useful outcome before adding a challenge to every request.
C.13.2 Query parameters and cache identity#
Query parameters can fragment the cache even when the response is identical.
For example, /offer?utm_source=newsletter and /offer?utm_source=search may serve the same page but occupy separate cache entries.
The proposed No-Vary-Search response header lets an origin declare which query parameters a supporting cache may ignore when matching URLs:
No-Vary-Search: params=("utm_source" "utm_medium")
With this policy, differences in those campaign parameters can permit reuse of a stored response, while a content-changing parameter such as colour still distinguishes responses.
Unlike Vary, which selects responses using request headers, No-Vary-Search concerns the URL's query component.
It changes matching, without rewriting the URL or relaxing storage, freshness, or validation requirements.
List only parameters known to be safe to ignore so that new, unknown parameters still distinguish responses. Identical HTML is insufficient if reuse would bypass required server processing, such as authorization or signature verification. An ignored parameter also cannot be relied on to bypass the cache for debugging. As of October 2026, the HTTP Working Group specification remains a draft: verify syntax and behavior in the target caches before relying on it. Harry Roberts's practical introduction develops the campaign-URL example further.
C.14 References#
- Model Context Protocol. (2026, July). Model Context Protocol Specification, Revision 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28
- Casas, R. (2026, June). Beyond Components: Designing Generative UI for MCP Apps. AI Engineer, YouTube video. https://www.youtube.com/watch?v=hCMrEfPG2Yg
- Vepsalainen, J., Hevery, M., & Vuorimaa, P. (2024). Resumability—A New Primitive for Developing Web Applications. IEEE Access, 12, 9038–9046. https://doi.org/10.1109/ACCESS.2024.3352891
- Vepsäläinen, J., Vuorimaa, P., & Hellas, A. (2025). The Potential of Serverless Edge-Powered Islands for Web Development. Journal of Web Engineering, 24(1), 1–38. https://doi.org/10.13052/jwe1540-9589.2411
- Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures [Phdthesis, University of California, Irvine]. https://ics.uci.edu/~fielding/pubs/dissertation/top.htm
- King, A. (2019, November). Parse, Don’t Validate. Personal technical essay. https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
- King, A. (2026, July). The Unreasonable Effectiveness of Constructive Data Modeling. Software Should Work conference presentation. https://www.youtube.com/watch?v=0BXuYlNrUmE
- Härder, T., & Reuter, A. (1983). Principles of Transaction-Oriented Database Recovery. ACM Computing Surveys, 15(4), 287–317. https://doi.org/10.1145/289.291
- Gilbert, S., & Lynch, N. A. (2002). Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. SIGACT News, 33(2), 51–59. https://doi.org/10.1145/564585.564601
- Kreps, J. (2013, December). The Log: What Every Software Engineer Should Know About Real-Time Data’s Unifying Abstraction. LinkedIn Engineering. https://www.linkedin.com/blog/engineering/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying