2.5 Software Engineering Foundations & Error Handling
2.5.1 A Claude Integration Is Ordinary Software First
It's tempting to treat an LLM feature as a special case exempt from normal engineering practice because it "just calls an API." The blueprint is explicit that the opposite is true: a Claude integration is built with the same engineering discipline as any other software component, and the correct posture is to treat it as a normal piece of software that happens to call a probabilistic model -- not a special case with its own rules.
- •REST APIs & JSON -- the Claude API is HTTPS + JSON; status codes, headers, request/response shape, idempotency, and pagination apply the same as any REST service.
- •Asynchronous programming -- LLM calls are I/O-bound and slow; use async/await and concurrency to parallelize independent calls and keep a server responsive.
- •Version control (Git) -- branching, PRs, and history are the substrate for reviewing and rolling back a prompt or model change.
- •SDLC integration & code review -- fit LLM features into existing CI/CD, and review both code and prompts.
- •Refactoring -- from tidying a function to restructuring a service, a core competency for codebase-modernization use cases.
2.5.2 Async I/O for I/O-Bound LLM Calls
A Messages API call is a slow, I/O-bound network round trip, not CPU work. Making it synchronously inside a request path that's serving other traffic blocks that server unnecessarily while the call is in flight. The SDKs ship async clients specifically so you can await independent calls concurrently instead of serializing them.
import asyncio
from anthropic import AsyncAnthropic
client = AsyncAnthropic()
async def summarize(doc: str):
resp = await client.messages.create(
model="claude-sonnet-4-5", max_tokens=512,
messages=[{"role": "user", "content": f"Summarize: {doc}"}],
)
return resp.content[0].text
async def main(documents):
# Independent calls run concurrently instead of one-at-a-time
return await asyncio.gather(*(summarize(d) for d in documents))2.5.3 Prompts and Model IDs Are Reviewable, Versioned Artifacts
Git branching, pull requests, and commit history aren't just for source code -- they're the substrate for reviewing and, when needed, rolling back a prompt or model-ID change, using exactly the same mechanism used for any code change. A prompt edit that quietly ships straight to production without review is not a smaller risk than a code change without review; it's the same risk, because it changes application behavior just as directly.
The one idea to hold onto
Fit LLM features into existing CI/CD and code review, and review both code and prompts. Prompts and model IDs are reviewable, versioned artifacts -- not incidental strings that live outside the normal SDLC.
This connects directly to refactoring: as a Claude-integrated codebase evolves -- from tidying a single function to restructuring a whole service -- prompts and model IDs move through the same review and version-control discipline as the surrounding code, which is exactly what makes it possible to modernize the codebase (a core Claude Code use case) without losing track of why a given prompt or model choice was made.
2.5.4 Error Handling: Transient vs. Client Errors
Robust integration code distinguishes two fundamentally different failure classes and responds to each differently.
| Status | Class | Correct response |
|---|---|---|
| 429 | Rate limit -- transient | Exponential backoff with jitter, then retry |
| 529 / 5xx | Overloaded -- transient | Exponential backoff with jitter, then retry |
| 400 | Malformed request -- your bug | Fix the payload; retrying unchanged will not help |
| 401 | Bad key -- your bug | Fix authentication; retrying unchanged will not help |
Two classes of error, two correct responses. The SDKs surface these as typed exceptions so code can branch on error class.
A 429 or 529 means the request itself was fine -- the service is asking you to slow down or come back later. Retrying immediately, or in a tight loop, tends to make the underlying congestion worse. Exponential backoff spaces retries out with growing delay; adding jitter (small randomization) keeps many clients from retrying in lockstep and re-creating the same spike they were trying to recover from.
2.5.5 Why Retrying a 400/401 Never Helps
A 400 or 401 means the request itself is wrong -- a malformed payload or an invalid credential. No amount of waiting fixes a bug in the request; retrying the identical request will fail identically, every time. These require fixing the payload or the key, not backoff.
import anthropic
import time, random
def call_with_retry(**kwargs):
for attempt in range(5):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError: # 429 -- transient
time.sleep((2 ** attempt) + random.random())
except anthropic.APIStatusError as e:
if e.status_code >= 500: # 529/5xx -- transient
time.sleep((2 ** attempt) + random.random())
else: # 400/401 -- your bug, don't retry
raise
raise RuntimeError("exhausted retries")Exam trap
Applying the same backoff-and-retry logic to a 400 or 401 as to a 429/529 just repeats an identical failure with extra delay. Distinguish transient (retry) from client-side (fix the request or the key) before choosing a recovery strategy.
Key Takeaways
- ✓The Claude API is a standard HTTPS + JSON REST service -- status codes, headers, request/response shape, idempotency, and pagination all apply.
- ✓Async/await and concurrency are the right tools for I/O-bound, slow LLM calls; the SDKs provide async clients for exactly this.
- ✓Prompts and model IDs are reviewable, versioned artifacts that belong in the same Git/PR/code-review process as code, not incidental strings.
- ✓429 (rate limit) and 529/5xx (overloaded) are transient -- retry with exponential backoff and jitter.
- ✓400 (malformed request) and 401 (bad auth) are client-side bugs -- retrying unchanged will never help; fix the payload or the key.
Check Your Understanding
Test what you learned in this lesson.
Q1.A service makes several independent Messages API calls inside a request handler, one after another, blocking on each. What's the improvement the blueprint calls for?
Q2.A developer edits a prompt directly in production without a PR or review, reasoning that "it's just a string." What's wrong with this?
Q3.A Messages API call returns HTTP 429. What is the correct response?
Q4.A call returns HTTP 401. A developer retries the identical request three times with exponential backoff. What's wrong with this approach?
Practice This Lesson