Rubric
Contents — domains, guide and mocks

Engineering foundations

CCDV-F 2.415 min read · checked 21 September 2026

Task statementSoftware Engineering Foundations (7.4%) — REST APIs, JSON, asynchronous programming, version control, SDLC integration, code review, and refactoring at small and large scale

One HTTP round trip

Your service
HTTP API
Step 1: Your service to HTTP API: POST /v1/messages
Step 2: Your service to HTTP API: headers: key, version, type
Step 3: Your service to HTTP API: JSON body
Step 4: HTTP API to Your service: status code, e.g. 200
Step 5: HTTP API to Your service: request-id, limit headers
Step 6: HTTP API to Your service: JSON body or error object
Everything the application knows about a call is in these four things: method and path, request headers, status code, and response headers and body.

REST, learned from the API you already call

A REST-style API exposes resources at paths and acts on them with HTTP methods. The Claude API is a clean worked example: you create a message with POST /v1/messages; you create a batch with POST /v1/messages/batches, then read it with GET /v1/messages/batches/{id} and its results at /results, list batches with GET, and cancel with a POST to /cancel; files are created, listed, retrieved and removed at /v1/files. The verb tells you the intent, the path names the thing.

Three ideas earn their place in your code. Statelessness: each request carries everything needed to serve it, which is exactly why the Messages API needs the whole conversation every time. Status codes in classes: 2xx succeeded, 4xx means your request was wrong, 5xx means the server failed. And headers carry the metadata that makes operations possible — the Claude API returns a request-id on every response, rate-limit headers telling you what remains, and retry-after on a 429.

You seeWhose faultWhat the code should do
400 invalid_request_errorYoursFix the request; retrying identical input just repeats it
401 / 403YoursCredentials or permissions — never retry in a loop
413 request_too_largeYoursShrink the payload; consider file references instead of inline data
429 rate_limit_errorSharedBack off, honouring retry-after, with jitter
500 api_error / 529 overloaded_errorTheirsRetry with exponential backoff, then give up and surface it

Error typing and recovery are their own task statement (4.1), so the foundations point is narrower: a 4xx and a 5xx are different categories of event, and code that treats every non-200 as “retry” will hammer an endpoint that will never succeed. Log the request-id with the failure — it is the one identifier that lets someone else investigate.

JSON, and parsing things you did not write

JSON is the interchange format for all of this: objects, arrays, strings, numbers, booleans and null, with double-quoted keys and no comments. Two rules of thumb prevent most JSON defects in a Claude application. First, ignore what you do not recognise: Anthropic's versioning policy allows additive change without a version bump — new optional inputs, new values in enum-like outputs — so a parser that rejects unknown fields or crashes on an unfamiliar enum value will break on a day you did not deploy. Second, never parse by regular expression or by trusting the shape; use the typed models your SDK provides, or validate against a schema.

JSON Schema shows up twice in this exam and both times it is a contract: a tool's input_schema describes what Claude may send you, and a structured-output schema describes what you require back. Schema design is 2.5's subject; what belongs here is the engineering instinct that a contract written down is worth more than a convention everybody remembers differently.

Two ways to read a response

Brittlepython

text = resp.content[0].text
data = json.loads(
    text.split("```")[1])
name = data["customer"]["name"]
if resp.stop_reason == "end_turn":
    save(name)

Defensivepython

block = next(
    (b for b in resp.content
     if b.type == "text"), None)
if block is None:
    return handle_no_text(resp)
try:
    data = json.loads(block.text)
except ValueError:
    return handle_bad_json(resp)
name = data.get("customer", {}).get("name")
The left-hand version fails the first time a field is added, a list is empty, or the answer is a refusal. The right-hand one degrades instead of exploding.

Asynchronous programming

Calls to a model are slow and almost entirely spent waiting on the network. That makes them the textbook case for concurrency: not doing more work at once, but stopping one wait from blocking the others. Every official SDK offers an asynchronous client — AsyncAnthropic in Python, promises in TypeScript — and the useful pattern is nearly always the same: fire off many requests, bound how many are in flight, and collect the results.

Bounded is the operative word. Unbounded concurrency is how a well-meaning script turns into a self-inflicted rate-limit incident: a hundred coroutines all hit the per-minute limit at once, all receive 429, all retry together, and the pattern repeats. A semaphore around the call, backoff that honours retry-after, and jitter so retries do not re-synchronise, are the three parts of a fix.

Bounded concurrency with an async clientpython
import asyncio
from anthropic import AsyncAnthropic

client = AsyncAnthropic()          # same API surface, awaitable
limit = asyncio.Semaphore(8)       # at most 8 requests in flight

async def summarise(doc: str) -> str:
    async with limit:              # queue here, not at the API
        resp = await client.messages.create(
            model=MODEL_ID, max_tokens=512,
            messages=[{"role": "user", "content": doc}],
        )
        return resp.content[0].text

async def main(docs: list[str]) -> list[str]:
    # return_exceptions keeps one failure from cancelling the rest
    return await asyncio.gather(
        *(summarise(d) for d in docs), return_exceptions=True
    )

Two adjacent facts save work. The SDKs already retry transient failures — connection errors, rate limits and 5xx responses — with exponential backoff, honouring retry-after, and both the retry count and the timeout are configurable; write your own retry loop only when you know what the built-in one does not do. And if the work does not need to be concurrent because nobody is waiting, the asynchronous answer may be the Message Batches API rather than more threads.

Version control, review and the pipeline

The rule that makes an AI application maintainable is that prompts, schemas, tool definitions and configuration are source code. They go in the repository, they change on a branch, they are reviewed in a pull request, and their history explains why last quarter's output looked different. A prompt in a wiki page or pasted into a console is a production dependency with no diff, no author and no rollback.

Configuration follows the same split most teams already use for application settings. Claude Code's own settings are a good model of it: a shared .claude/settings.json that is committed so everyone in the repository gets the same rules, and a .claude/settings.local.json for personal overrides that is kept out of commits. Secrets are the exception that proves the rule: they are referenced from the repository and stored outside it, as GitHub repository or organisation secrets rather than as a line in a workflow file.

A change, from branch to production

  1. Branchcode, prompts and schemas together
  2. Pull requestsmall, with a stated intent
  3. Automated checkstests, linting, evaluation set
  4. Human reviewsomeone who can say no
  5. Merge and releasewith a way back

a defect found in production re-enters as a new branch, not a hotfix in the console

Each gate is cheap and each one catches a different class of defect. The evaluation run is the only step that is specific to an AI system.

Claude can take part in that pipeline rather than sitting outside it. The Claude Code GitHub Action runs inside a repository's workflows: mention it in an issue or pull-request comment and it responds there, or give the workflow a prompt and it runs automatically on an event — the documented review workflow posts findings as inline comments on the pull request. Teams keep it bounded with a turn limit and an explicit allowed-tools list, and they keep the human reviewer, because an automated reviewer is an extra pair of eyes and not an approver.

Refactoring, small and large

Refactoring is changing structure without changing behaviour, and the exam's phrase “at small and large scale” is a real distinction. Small scale is local and safe: renaming a variable to what it actually holds, extracting a function, deleting a branch nobody reaches. You do it continuously, under the cover of tests, in the same pull request as the work that revealed the problem.

Large scale is a migration, and it is a different discipline: you cannot stop the world while you do it. The pattern that works is incremental — introduce the new structure beside the old one, route a slice of traffic or one module through it, verify, widen, then remove the old path once nothing calls it. What makes it safe is having something that tells you behaviour has not changed: for a Claude application that is both ordinary tests at the boundary and an evaluation set for the model-facing behaviour.

Traps the wrong answers are built from

Tempting but wrongDo this instead
Retrying every non-200 responseRetry 429 and 5xx with backoff; fix 4xx requests instead of repeating them.
Firing unbounded concurrent requestsBound in-flight work with a semaphore and honour retry-after with jitter.
Extracting fields with string splitting or regular expressionsUse typed SDK models or parse and validate, tolerating unknown fields.
Keeping prompts in a document outside version controlTreat prompts, schemas and configuration as source code with diffs and review.
Committing API keys so a workflow can use themStore them as repository or organisation secrets and reference them from the workflow.
Rewriting a sprawling integration in one pull requestIntroduce a seam, migrate a slice at a time, and delete the old path last.

You should now be able to

  • Describe an API call in terms of method, path, headers, status class and body, and log request-id.
  • Parse model and API output defensively, tolerating unknown fields and missing values.
  • Write bounded concurrent request code, and say when batch processing is the better answer.
  • Keep prompts, schemas and configuration under version control with review gates.
  • Explain the difference between small-scale refactoring and an incremental large-scale migration.

Practice questions

Original questions written for this lesson, in the exam’s style. Answer first, then open the reasoning — every option is explained, including why the wrong ones are tempting.

  1. Question 1

    A nightly job fans out 400 requests at once with no limit. It finishes eventually, but the logs are full of 429 responses and the team's other services start seeing them too.

    Which two changes address the cause? (Select 2.)

    1. ABound in-flight requests with a semaphore sized to the token-per-minute budget.
    2. BBack off using the retry-after header, with jitter added to the wait.
    3. CCatch the 429 responses and retry immediately in a tight loop.
    4. DSwitch to a smaller model so each request uses fewer tokens.
    5. ERaise the client timeout so requests have longer to complete.
    Show answer and reasoning
    1. ACorrect. Rate limits are per organisation and per minute, so the fix is to stop presenting more work than the budget allows at once.
    2. BCorrect. Honouring the server's own hint and de-synchronising retries prevents the retry storm from repeating the collision.
    3. CIncorrect. Immediate retries make the contention worse and can extend the period during which the limit is exceeded.
    4. DIncorrect. It may reduce token pressure slightly, but the job still floods the limit because nothing controls concurrency.
    5. EIncorrect. The requests are being rejected, not timing out; a longer timeout changes nothing.
  2. Question 2

    An integration deployed months ago starts throwing exceptions during response handling. Nothing was released, and the failing line reads a field from a response object by index and then branches on a string value it did not expect.

    What is the correct engineering response?

    1. APin the API version header to an older date so the response shape reverts.
    2. BWrap the handler in a broad exception catch so the job keeps running.
    3. CSelect blocks by type and handle unrecognised values as a default case.
    4. DAsk the model in the system prompt to keep its response format stable.
    Show answer and reasoning
    1. AIncorrect. The version header governs compatibility of documented fields, and additive values are permitted within a version; this is not a rollback mechanism.
    2. BIncorrect. It hides the failure rather than handling it, and the unhandled case still produces wrong behaviour downstream.
    3. CCorrect. Additive fields and new enum-like values can appear without a version bump, so parsing must be positional-independent and tolerant of unknown values.
    4. DIncorrect. The change is in the API envelope, not in what the model wrote, so a prompt cannot affect it.
  3. Question 3

    A team wants to restructure a two-year-old service so that all model calls go through one module. The service is business-critical and released weekly.

    Which plan best fits a large-scale refactor?

    1. AFreeze feature work and rewrite the service on a long-lived branch.
    2. BAdd the new module, migrate a few call sites per pull request, and delete the old path last.
    3. CIntroduce the module for new code only and leave existing call sites as they are.
    4. DHave an automated tool rewrite every call site in one commit, then run the tests.
    Show answer and reasoning
    1. AIncorrect. A long-lived branch diverges from production and delivers all the risk on one day.
    2. BCorrect. Each step is small, reviewable and releasable, and the system works after every one of them.
    3. CIncorrect. This is a reasonable stopgap but leaves the duplication the refactor was meant to remove, including the scattered model identifiers.
    4. DIncorrect. A single mass rewrite is hard to review and gives no intermediate point at which behaviour was verified.

Sources

Drafted with AI assistance and checked against the sources above; expert review is in progress. Spotted an error? Tell us and it gets fixed, dated and listed on how this is written.