Claude · 09 · Part 5

Working with Real Projects

Ten everyday engineering jobs, worked on the bookshelf project: understanding unfamiliar code, adding an API, testing, debugging a production-style failure, refactoring, tuning a query, reviewing, documenting, using Git and fixing a broken build. For each, you get the prompt pattern, what to verify, and — where possible — real output from a real run.

10
Scenarios
5
Passing tests
4→1
SQL statements
Information current as of: 2026-10-30
How to read this page honestly. No Claude Code session was run for this page. The code shown is what a good agent session should produce; it was written by this guide's author and then verified by real Maven, Git and curl runs, whose output is labelled Actual output. Where a step shows what an agent would be asked, it is a prompt pattern, not a recorded transcript. This is the same line the guide draws everywhere: executed means executed.
01
Scenario 1 · Understand

An Unfamiliar Codebase

You inherit bookshelf version 2. Start broad, then narrow — the docs' own recipe — and keep it read-only (Source 1). Delegating wide reads to a subagent keeps the file-by-file exploration out of your main context.

Prompts, in order (typed into a session)
give me an overview of this codebase explain the main architecture patterns used here what are the key data models? how is a POST /api/books request validated and stored? name each class involved use a subagent to find every place that touches the database
What a good answer should reconstruct: bookshelf v2
Client BookController@Valid requestreturns DTOs BookService@Transactionalmaps to BookDto BookRepositoryentity graph: author AuthorRepositoryfindByName H2 (in-memory)book · author tables ApiExceptionHandlerNotFound → 404 ProblemDetail

Verify: ask it to list the classes it used for each answer, then open two of them. For a typed language such as Java, a code-intelligence plugin makes this kind of tracing more precise by adding go-to-definition and find-references (Source 2).

02
Scenario 2 · Implement

Adding REST Endpoints

The task prompt (Chapter 8's five parts)
Move bookshelf from the in-memory list to JPA + H2. Add GET /api/books/{id} (404 as a ProblemDetail for unknown ids) and POST /api/books (validate: title and author not blank, year 1450..2100; invalid -> 400; return 201 with the created BookDto). Controllers return DTO records, never entities. Follow the existing controller style, add tests for the 404 and the validation, and run ./mvnw -B test, showing me the output.

The result of that task is the code below (written by the author for this guide, then built and tested).

BookController.java (v2)
@RestController @RequestMapping("/api/books") public class BookController { private final BookService service; public BookController(BookService service) { this.service = service; } @GetMapping public List<BookDto> list() { return service.findAll(); } @GetMapping("/{id}") public BookDto get(@PathVariable long id) { return service.findById(id); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public BookDto create(@Valid @RequestBody CreateBookRequest request) { return service.create(request); } }
Entities, DTOs, service, handler (expand)
CreateBookRequest.java
public record CreateBookRequest( @NotBlank String title, @NotBlank String author, @Min(1450) @Max(2100) int year) { }
Book.java (excerpt)
@Entity public class Book { @Id @GeneratedValue private Long id; private String title; @Column(name = "publication_year") private int year; @ManyToOne(fetch = FetchType.LAZY) private Author author; // constructor and getters omitted }
BookService.java
@Service public class BookService { private final BookRepository books; private final AuthorRepository authors; public BookService(BookRepository books, AuthorRepository authors) { this.books = books; this.authors = authors; } @Transactional(readOnly = true) public List<BookDto> findAll() { return books.findAllByOrderByTitle().stream().map(BookDto::from).toList(); } @Transactional(readOnly = true) public BookDto findById(long id) { return books.findById(id).map(BookDto::from).orElseThrow(() -> new BookNotFoundException(id)); } @Transactional public BookDto create(CreateBookRequest request) { Author author = authors.findByName(request.author()).orElseGet(() -> authors.save(new Author(request.author()))); return BookDto.from(books.save(new Book(request.title(), request.year(), author))); } }
ApiExceptionHandler.java
@RestControllerAdvice public class ApiExceptionHandler { @ExceptionHandler(BookNotFoundException.class) ProblemDetail notFound(BookNotFoundException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); } }
Prerequisites: the app running (./mvnw -B spring-boot:run). Four requests:
$ curl -i localhost:8080/api/books/99 $ curl -i -X POST localhost:8080/api/books -H 'Content-Type: application/json' -d '{"title":"","author":"X","year":2020}' $ curl -i -X POST localhost:8080/api/books -H 'Content-Type: application/json' -d '{"title":"Domain-Driven Design","author":"Eric Evans","year":2003}' $ curl localhost:8080/api/books | jq -c 'length'
Actual output (status lines and bodies) · executed 2026-10-30
HTTP/1.1 404 Content-Type: application/problem+json {"detail":"Book 99 not found","instance":"/api/books/99","status":404,"title":"Not Found"} HTTP/1.1 400 (blank title rejected) HTTP/1.1 201 {"id":7,"title":"Domain-Driven Design","author":"Eric Evans","year":2003} 7

Verify, beyond "it works": the 404 body is a standard ProblemDetail, the 400 came from validation (not a crash), the new book got id 7 after six seeded rows, and the list grew to seven. Read the diff for anything the task did not ask for.

03
Scenario 3 · Tests

Unit, Slice and Integration Tests

Levelbookshelf testWhat it provesCost
Web sliceBookControllerTest (@WebMvcTest, service mocked)Routing, validation, status codes, ProblemDetail shapeFast — loads only the web layer
IntegrationBookApiIntegrationTest (@SpringBootTest + MockMvc)Controller + service + JPA + H2 working togetherStarts the whole context
Behavioural guardBookQueryCountTestListing runs exactly one SQL statementFull context; catches a performance regression
BookControllerTest.java (excerpt)
@WebMvcTest(BookController.class) class BookControllerTest { @Autowired MockMvc mvc; @MockitoBean BookService service; @Test void unknownIdReturns404() throws Exception { given(service.findById(99L)).willThrow(new BookNotFoundException(99L)); mvc.perform(get("/api/books/99")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.detail").value("Book 99 not found")); } @Test void blankTitleIsRejected() throws Exception { mvc.perform(post("/api/books") .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"\",\"author\":\"Someone\",\"year\":2020}")) .andExpect(status().isBadRequest()); } }
Actual output (excerpt) · ./mvnw -B test, final state · Spring Boot 4.1.2, Java 21.0.2
SQL statements for findAll(): 1 [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.bookshelf.BookControllerTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.bookshelf.BookQueryCountTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.bookshelf.BookApiIntegrationTest [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS
A passing test is evidence only if it can fail. The strongest proof in this project is that BookQueryCountTest did fail on a real defect (next sections). When an agent writes tests for code it wrote, check that at least one fails when you break the behaviour on purpose.
04
Scenario 4 · Debug

A Production-Style Failure

A client reports "the API gives a 500 for a book that does not exist". The first prompt carries real evidence (Chapter 8): the request, the symptom and the stack. Here is that evidence, captured by running version 2 without the exception handler:

Actual output · GET /api/books/99 before ApiExceptionHandler existed
HTTP/1.1 500 {"timestamp":"2026-10-30T17:34:38.729Z","status":500,"error":"Internal Server Error","path":"/api/books/99"} com.example.bookshelf.BookNotFoundException: Book 99 not found at com.example.bookshelf.BookService.lambda$findById$0(BookService.java:25) at com.example.bookshelf.BookService.findById(BookService.java:25) at com.example.bookshelf.BookController.get(BookController.java:31)
The debugging loop, with evidence at each step
Reproducecurl → 500saved log Isolateread thestack trace Fixmap theexception Guardadd a testfor the 404 Verifysame curl →404, not 500 reproduce before you fix; re-run the same reproduction after an agent that skips "reproduce" can only guess
Prompt
GET /api/books/99 returns 500 instead of 404. Stack trace below. BookNotFoundException is thrown in BookService.findById but never mapped. Add the mapping in ApiExceptionHandler (ProblemDetail, no try/catch in the controller), add a regression test in BookControllerTest, and re-run the same request with curl to confirm 404. [paste the trace]

The fix is the ApiExceptionHandler shown in Scenario 2, and the "after" is the 404 application/problem+json response already shown. Notice the cause was not a bug in the lookup at all: the service correctly threw. The defect was a missing translation layer — a distinction an agent finds by reading the trace, and a reason to ask for the diagnosis before the fix.

A second real failure: the build itself

Earlier in the same work, version 2 would not even start under test. The Maven output pointed straight at the cause:

Actual output (excerpt) · first ./mvnw -B test of the JPA version
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "create table book ([*]year integer not null, author_id bigint, id bigint not null, title varchar(255), primary key (id))"; expected "identifier" [ERROR] Tests run: 5, Failures: 0, Errors: 2, Skipped: 0 [ERROR] BUILD FAILURE

The [*] marks the failing token: year, a reserved word in H2. The fix is a one-line column mapping, @Column(name = "publication_year"), now recorded in the project's CLAUDE.md (Chapter 10) so no future session repeats it. Two of five tests errored because both full-context tests failed to start the application — a reminder to read which tests failed and why before assuming the tests are the problem.

05
Scenario 5 · Refactor

Legacy Code Without Breaking Behaviour

The docs' refactoring recipe: find the legacy code, get recommendations, apply changes while maintaining the same behaviour, then run tests — in small, testable increments (Source 1). The risky part is "same behaviour". Here is a real example of why that sentence needs a check.

Legacy method: deeply nested ifs
static String eraLegacy(int year) { String result; if (year < 1900) { result = "classic"; } else { if (year < 2000) { result = "twentieth-century"; } else { if (year < 2010) { result = "2000s"; } else { if (year < 2020) { result = "2010s"; } else { result = "recent"; } } } } return result; }
Attempt 1 — a clever decision table
static String eraAttempt1(int year) { return switch (year / 10) { case 0, 1, 2, /* ... */ 18 -> "classic"; case 190, 191, /* ... */ 199 -> "twentieth-century"; case 200 -> "2000s"; case 201 -> "2010s"; default -> "recent"; }; }
Final — guard clauses
static String era(int year) { if (year < 1900) return "classic"; if (year < 2000) return "twentieth-century"; if (year < 2010) return "2000s"; if (year < 2020) return "2010s"; return "recent"; }
The safety net: compare against the original over many inputs
inputs 0..3000 legacy (oracle) refactored comparecount differences attempt 1: 1710 differences final: 0 differences
Prerequisites: JDK 21. Single-file program comparing all three versions
$ java LegacyLabel.java
Actual output · executed 2026-10-30 · OpenJDK 21.0.2
years 0..3000 where behaviour differs from the legacy method: attempt 1 (decision table): 1710 final (guard clauses): 0 first failing year for attempt 1: 190

What happened: the clever table looked right and read well, but it silently labelled every year from 190 to 1899 as "recent" (1710 years), because the table only covered quotients up to 18. A handful of spot checks would have missed it; comparing against the old implementation over a range found it at once. This is the pattern to ask an agent for: keep the old method as an oracle, compare over many inputs, then delete the oracle. And it is a live example of a confident-looking change that is wrong.

06
Scenario 6 · SQL

Finding and Fixing an N+1 Query

Listing books maps each Book to a DTO containing the author's name, but Book.author is lazy. Loading six books by three authors therefore runs one query for books and one per distinct author. Measured, not guessed — with Hibernate statistics in a test:

BookQueryCountTest.java
@SpringBootTest(properties = "spring.jpa.properties.hibernate.generate_statistics=true") class BookQueryCountTest { @Autowired BookService service; @Autowired EntityManagerFactory emf; @Test void listingBooksUsesASingleQuery() { Statistics stats = emf.unwrap(SessionFactory.class).getStatistics(); stats.clear(); service.findAll(); System.out.println("SQL statements for findAll(): " + stats.getPrepareStatementCount()); assertThat(stats.getPrepareStatementCount()).isEqualTo(1); } }
Actual output · before the fix
SQL statements for findAll(): 4 [ERROR] BookQueryCountTest.listingBooksUsesASingleQuery:29 expected: 1L but was: 4L [ERROR] Tests run: 5, Failures: 1, Errors: 0, Skipped: 0 [ERROR] BUILD FAILURE
Before and after: SQL statements per listing request
Before · 4 statements select books select author 1 select author 2 select author 3 After · 1 statement select books join authors no extra query per author

The fix is a single annotation: tell Spring Data to fetch the author together with the books.

BookRepository.java (after)
public interface BookRepository extends JpaRepository<Book, Long> { @EntityGraph(attributePaths = "author") List<Book> findAllByOrderByTitle(); }
Actual output · after the fix
SQL statements for findAll(): 1 [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS

What to ask an agent: measure first, change second, show before-and-after counts, keep the test as a permanent guard. Be wary of "optimisations" without a measurement: "faster" is unfalsifiable, "1 statement instead of 4" is not. This page does not claim wall-clock gains — the data set is six rows in memory. Index advice for real databases belongs with your actual query plans, which an agent can read through a database MCP server (Chapter 10) if you connect one.

07
Scenario 7 · Review

Code Review

Claude Code includes bundled review commands: /code-review is listed among its bundled skills (Source 3) and /security-review runs an on-demand security pass over the changes on your current branch (Source 4). Both were left unrun for this page. Prompt patterns that work with or without them:

Prompts
review my changes on this branch against main. focus on correctness and security; skip style. for each finding give the file and line, a failing scenario, and a minimal fix. check the diff for new endpoints without validation or without tests.
A reviewer that must find something will. Anthropic's advice is to tell a review prompt what to ignore — style, in its example — because otherwise you end up with extra abstraction and tests for cases that cannot happen (Source 5). Treat findings as hypotheses: ask for the scenario that would trigger each one, and reject any you cannot reproduce.
08
Scenario 8 · Documentation

Docs That Stay True

The docs' recipe for documentation: identify undocumented code, generate documentation, improve it, and verify it follows project standards (Source 1). The risk is documentation that describes what the code "should" do. Counter it with the same discipline as tests: make the documentation executable.

Prompt
Write README.md for bookshelf: what it is, how to build and test, and an API table (method, path, request body, responses). Every command and every curl example in it must be one you have actually run in this session; show the output of each. Do not describe endpoints that don't exist.

Verify: run the README's commands yourself on a clean checkout. The four curl examples and the ./mvnw -B test command in this chapter are the kind of content that belongs there, because each was run and its output is on this page.

09
Scenario 9 · Git

Git Operations

You can ask in plain language — "what files have I changed?", "commit my changes with a descriptive message", "create a new branch called feature/quickstart", "create a pr" (Source 1, Source 6). Whatever drives the Git commands, the resulting history is what you review. Here is the real history of this project's two steps:

Run in the bookshelf repository
$ git log --oneline --graph --all $ git diff --stat main..feature/jpa-books | tail -4
Actual output · executed 2026-10-30
* c12f41d Add JPA persistence, validation and 404 handling; fix N+1 on book listing * 7672f6d Initial bookshelf: in-memory list endpoint .../java/com/example/bookshelf/DataSeeder.java | 27 +++++++++++++++++ .../com/example/bookshelf/BookQueryCountTest.java | 31 +++++++++++++++++ ... 15 files changed, 317 insertions(+), 12 deletions(-)
  • Branch per task. The JPA work lives on feature/jpa-books, so main still builds the v1 app. A bad agent run costs you a branch, not the codebase.
  • Read git diff --stat first. 15 files for "add persistence" is plausible; 60 would be a warning sign.
  • Pull requests. Claude Code can open them (gh pr create) and links the session to the PR for later resumption with claude --from-pr NUMBER (Source 1).
  • Remote actions need the most care. Pushing, force-pushing and deleting branches affect others; Chapter 10 shows a permission rule and a hook that block them.
10
Scenario 10 · Build and Deployment

Troubleshooting a Broken Build

Build failures are the best agent tasks: the error is precise and the check is "does it build". This one is real — the first ./mvnw -B test of the freshly generated project failed like this (Chapter 7):

Actual output (excerpt) · first build of the generated project
[FATAL] Non-resolvable parent POM for com.example:bookshelf:0.0.1-SNAPSHOT: The following artifacts could not be resolved: org.springframework.boot:spring-boot-starter-parent:pom:4.1.2.RELEASE (absent): Could not find artifact org.springframework.boot:spring-boot-starter-parent:pom:4.1.2.RELEASE in central (https://repo.maven.apache.org/maven2)
Prompt
./mvnw -B test fails with "Non-resolvable parent POM ... spring-boot-starter-parent:pom:4.1.2.RELEASE". The project came from start.spring.io. Find out whether that version exists on Maven Central, fix pom.xml minimally, and re-run the build. Do not change dependencies.

What the agent needs to do: check Central (the published list had 4.1.2, not 4.1.2.RELEASE), change one line in pom.xml, rerun. The "check Central" step matters: an agent without network access would only be guessing. State what it may use.

Deployment problems: the same shape

For an AWS deployment the pattern is identical — paste the real failing signal, name the environment, forbid destructive actions, ask for a diagnosis first. This prompt is a pattern; the aws commands were not run for this page.

Illustrative prompt — AWS commands not executed here
The bookshelf container task keeps stopping on ECS Fargate (service bookshelf-api, region eu-west-1). Run only read-only commands: describe the service and the latest stopped task, then read the last 100 lines of its CloudWatch log group. Report the stop reason and the first error. Do not change anything and do not run update, delete or deploy commands.
Why the boundary matters: an agent's permission rules can allow read-only aws calls and deny the rest; Chapter 10 shows how rules like Bash(aws *) interact (a broad deny cannot be overridden by a narrower allow).
11
Common Mistakes

Working on Real Code

1 · Trusting generated tests. Passing is not proof; confirm a test can fail (BookQueryCountTest did).
2 · "Same behaviour" by eyeball. The decision-table refactor read perfectly and was wrong for 1710 inputs. Compare against the original.
3 · Fixing symptoms. The 500 was a missing exception mapping, not a bad lookup. Ask for the diagnosis first.
4 · Optimising without measuring. Count statements or time a query before and after; keep the measurement as a test.
5 · Letting generated config through unread. The Initializr pom needed a fix; the H2 mapping needed one too. Generated is not verified.
6 · A giant diff nobody reads. Work on a branch, in small steps, and read git diff --stat before the details.
7 · Unbounded autonomy near production. Deployment prompts should say what is allowed, in words, and permissions should back the words up.
12
Summary

Key Takeaways, Exercises and Sources

∑ Working with real projects — what to remember
  • Explore read-only first, then act; ask for the files behind every answer.
  • Give each task a check: a test, a curl, a statement count.
  • Reproduce before you fix, and re-run the same reproduction after.
  • Refactors need an oracle. A plausible rewrite can be wrong for most inputs.
  • Measure performance work with a number that can fail a test.
  • Branch, commit small, read the diff. Git is the durable undo.
Exercises

Exercise 1 Beginner

Run ./mvnw -B test on the v2 project, then change @Column(name = "publication_year") back to the default and run it again. Read the error: which token does [*] mark?

Learning outcome: read a build failure to its root cause.

Exercise 2 Intermediate

Remove the @EntityGraph from BookRepository, add a fourth author with books, and predict the statement count before you run BookQueryCountTest. Then run it. Was your prediction right, and why?

Learning outcome: understand N+1 as a function of distinct lazy parents.

Exercise 3 Advanced

Take a legacy method from your own codebase. Write a comparison harness against the original over a wide input range, then ask Claude to refactor it. Record whether its first attempt passed the harness, and how many inputs differed if it did not.

Learning outcome: treat refactoring as behaviour-preserving only when measured.

Sources (read on 2026-10-30; paraphrased, at most one short quote per source)
  1. Anthropic, Common workflows — codebase exploration, bug fixing, refactoring, tests, pull requests, documentation, Git prompts.
  2. Anthropic, Extend Claude Code and Best practices — code-intelligence plugins, subagent delegation.
  3. Anthropic, Extend Claude Code — bundled skills such as /code-review.
  4. Anthropic, Security — /security-review.
  5. Anthropic, Best practices — review against a spec; "skip style".
  6. Anthropic, Quickstart — Git prompts.

Executed on this machine, 2026-10-30: Maven builds and tests (Spring Boot 4.1.2, Hibernate statistics, H2), the running app with curl, Git history and diff, and the Java refactoring comparison (OpenJDK 21.0.2 Temurin; Maven 3.10.0 via wrapper). Not executed: any Claude Code session, /code-review, /security-review, and the AWS commands. All code was written by the guide's author; none is a recording of an agent session.