Writing Java Code with Claude Code: A Refactoring Tutorial

  • Last Updated: May 14, 2026
  • By: javahandson
  • Series
img

Writing Java Code with Claude Code: A Refactoring Tutorial

A practical tutorial on writing Java code with Claude Code — covering setup, effective prompts, refactoring workflows, generating clean methods and classes, fixing legacy code, and reviewing AI output. Includes real Java examples, comparison tables, interview tips, and patterns Java developers can adopt immediately to ship better code faster without losing control of their codebase.

Introduction

If you write Java for a living, you already know the rhythm of the day. You read a Jira ticket, open IntelliJ, scroll through three or four classes to remember how the existing code works, write a new method, run the build, fix a typo, run the build again, and finally commit. A lot of that time is not actually spent thinking about the business problem. It is spent typing boilerplate, jumping between files, and reformatting things that should already be clean.

Claude Code changes that rhythm. It is a command-line AI coding assistant from Anthropic that lives inside your project folder, reads your real source files, and writes or edits Java code with you. It is not a chat window for copying and pasting snippets. It opens files, applies changes, runs your Maven or Gradle build, reads the errors, and fixes them. For a Java developer, that is a meaningful shift in how day-to-day coding feels.

In this article, we will focus on two of the most common things Java developers actually do: writing new code and refactoring existing code. We will keep the language simple, use real Java examples, and walk through the workflow step by step so that even if you are completely new to Claude Code, you can sit down at your terminal afterwards and use it on your own project.

📌 How to use this article: Read each section in order. We start with why Claude Code matters, then move to writing fresh Java code, then to refactoring an existing class, and finally to the patterns and pitfalls you should know. If you have not installed Claude Code yet, the earlier articles in this series cover that setup in detail.

1. Why Claude Code Fits Java Development

Most AI coding tools you have probably tried so far are chat-based. You describe a problem, paste some code, and get a snippet back. That works, but it has friction. You must copy code out, paste code in, fix imports by hand, and remember which version of the snippet was the right one. For small Java tasks, this is fine. For real production codebases with dozens of packages, it gets tiring fast.

Claude Code is different because it works directly with your filesystem. You launch it from the root of your Java project, and it can list files, read classes, write new files, and edit existing ones. It understands package layouts, Maven and Gradle conventions, Spring annotations, and the kind of multi-file changes that real Java work demands. When you ask it to add a new REST endpoint, it does not just give you a controller method — it can also create the service, update the interface, add the DTO, and wire it into your configuration.

There is also a quieter advantage that matters more over time. Because Claude Code reads your existing code before writing new code, the style of what it produces tends to match your codebase. If your services use constructor injection, it will use constructor injection. If your DTOs are Java records, it will write records. This is the kind of consistency that is genuinely hard to get from a copy-paste chat workflow, and it is one of the strongest reasons to give Claude Code a real try in a Java environment.

Real-World Analogy: Think of a senior developer joining your team for a week. A chat-based AI is like that developer working in a separate room — you describe the problem, they describe the solution, and you copy it over. Claude Code is the same developer sitting next to you with your repository open. They can read the files you read, edit the files you edit, and run the same build you run. The output looks different because the workflow is different.

2. Setting the Stage: Project Layout and First Run

Before we write any code, it helps to picture the kind of project we are working with. For the rest of this article, imagine a typical Spring Boot service. It has a controller package, a service package, a repository package, a few entities, and the usual Maven structure. Nothing exotic — the same shape that thousands of Java services share.

To use Claude Code for this project, open a terminal in the project root and run the claude command. Claude Code starts an interactive session in that folder. From this point on, anything you ask it to do happens in the context of your real codebase. There is no copy-paste step, no separate editor window, and no need to describe your file layout to it — it can list directories and read files directly.

2.1 A Minimal Starting Project

Let us assume the project has a simple Customer entity, a CustomerRepository that extends JpaRepository, and a CustomerService that wraps a few basic operations. There is also a CustomerController with one GET endpoint. The build is Maven, the package root is com.example.shop, and the Java version is Java 21. This is the kind of base on which we will both add new functionality and clean up the rough edges.

// src/main/java/com/example/shop/customer/Customer.java
package com.example.shop.customer;
 
import jakarta.persistence.*;
 
@Entity
@Table(name = "customers")
public class Customer {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String fullName;
    private String email;
 
    // getters and setters omitted for brevity
}

This is the kind of class every Java developer has written a hundred times. It is fine, but it is also a great example of code that benefits from both new feature work and refactoring. We will do both, with Claude Code, in the rest of this article.

3. Writing New Java Code with Claude Code

Writing new code with Claude Code feels less like prompting and more like delegating. You describe the outcome you want, give it any constraints that matter, and let it propose the implementation. The most important habit to build is being clear about the contract — what the code should do, what it should return, and what conventions it should follow.

Let us walk through a concrete example. Suppose we want to add a new endpoint that lets a client search for customers by partial name. The search should be case-insensitive, return a list of matching customers, and follow the same patterns as the rest of the service. With Claude Code, you would just describe that in plain English at the prompt.

3.1 A First Prompt That Actually Works

There is a difference between a prompt that gives you something usable and one that gives you a generic snippet. The good prompts are specific about file locations, naming, and conventions. They tell Claude Code to read the existing files first so the new code blends in rather than clashing with the rest of the project.

# Example prompt you would type into Claude Code
 
Read CustomerController, CustomerService, and CustomerRepository.
Then add a new endpoint:
 
  GET /api/customers/search?name={query}
 
It should:
  - Search customers by partial, case-insensitive match on fullName
  - Return a List<CustomerDto> with id, fullName, email
  - Follow the same exception and response style as the existing endpoints
  - Add a unit test for the service layer using JUnit 5 and Mockito
 
Use constructor injection. Do not change unrelated code.

Notice what this prompt does. It points to the files Claude Code should review, provides a clear functional description, specifies the response shape, and forbids unrelated changes. That last line matters. Without it, an AI assistant might helpfully reformat your imports across three other files. With it, you keep the diff small and reviewable.

3.2 What Claude Code Actually Does Behind the Scenes

After you submit that prompt, Claude Code will typically open the three files you mentioned, read them, and then announce what it plans to change. You will see a list like: edit CustomerRepository to add a query method, edit CustomerService to add a search method, edit CustomerController to add the endpoint, create a new CustomerDto record, and create a new test class. Before applying changes, it shows you the diff. You approve, reject, or ask for adjustments.

This explicit plan-then-apply loop is one of the most underrated features of Claude Code. It keeps you in control. You are not blindly accepting AI output; you are reviewing a focused diff that you understand. For Java teams that take code review seriously, this maps very naturally onto the discipline you already have.

// CustomerRepository — generated method (Claude Code adds this)
public interface CustomerRepository extends JpaRepository<Customer, Long> {
 
    List<Customer> findByFullNameContainingIgnoreCase(String fullName);
}
 
// CustomerService — generated method
public List<CustomerDto> searchByName(String query) {
    return customerRepository
            .findByFullNameContainingIgnoreCase(query)
            .stream()
            .map(c -> new CustomerDto(c.getId(), c.getFullName(), c.getEmail()))
            .toList();
}
 
// CustomerController — generated endpoint
@GetMapping("/search")
public List<CustomerDto> search(@RequestParam String name) {
    return customerService.searchByName(name);
}

The output is short, idiomatic, and uses the same Spring Data JPA query pattern your codebase likely already uses elsewhere. There is no magic here — Claude Code looked at your existing repository to see what style you use, and copied that style for the new method. That is what makes the result feel like it was written by a teammate rather than dropped in from outside.

4. Iterating: Asking for Changes Without Starting Over

Real coding is rarely one-shot. You write something, look at it, and want to change it. Claude Code is designed for that loop. After it makes a change, you can describe what you want differently in plain language, and it will edit the same files again rather than starting from scratch. This is much faster than re-prompting from zero.

In our search example, you might say: rename the endpoint to /api/customers and use a ‘q’ query parameter, return a Page<CustomerDto> instead of a List, and add pagination support with a default page size of 20. Claude Code will read the current state of the files, apply only the necessary changes, and show you the new diff. You stay in control of every step.

# Follow-up prompt
 
Update the search endpoint to:
  - Move it to GET /api/customers and use 'q' as the query parameter
  - Return Page<CustomerDto> instead of List
  - Default to page=0 and size=20 when not provided
  - Update the test to cover both a paged result and an empty result

This is also where Claude Code shines compared to copy-paste chat workflows. There is no risk of the second answer drifting from the first because Claude Code is reading the actual files on disk, not a stale snippet of code from earlier in the conversation. The single source of truth is your repository.

5. Refactoring Java Code: Where Claude Code Really Pays Off

Refactoring is changing how code is structured without changing what it does. Most Java developers know they should refactor more, but in practice, it gets pushed down the priority list because it is risky, time-consuming, and easy to do badly. Claude Code does not eliminate the risk, but it makes refactoring far less tedious and error-prone in common cases.

The kinds of refactoring that benefit most from Claude Code are the boring ones. Renaming a field everywhere it appears. Splitting a fat service into smaller ones. Extracting a method out of a long block. Converting old-style POJOs into Java records. These tasks are individually simple but collectively take hours, and they are exactly the tasks where careful, mechanical edits across many files make up the whole job.

5.1 Refactor 1: Extracting a Method From a Long Service Method

Imagine your CustomerService has a registerCustomer method that has grown to 80 lines. It does input validation, deduplication checking, password hashing, the actual save, an event publish, and a welcome email. Each block is logically separate, but they all live in one method because nobody wanted to break things by splitting them. This is the classic ‘method that does too much’ smell.

# Refactoring prompt
 
Read CustomerService.registerCustomer.
 
Refactor it by extracting these private methods:
  - validateRegistrationRequest(RegistrationRequest req)
  - assertEmailNotTaken(String email)
  - persistCustomer(RegistrationRequest req)
  - publishCustomerCreatedEvent(Customer saved)
  - sendWelcomeEmail(Customer saved)
 
Do not change behavior. Keep all existing exceptions and messages identical.
Update the existing unit tests if their internal mock setup needs to change,
but do not change their assertions.

The instruction to keep behavior and assertions identical is the heart of safe refactoring. Claude Code will read the current method, identify the logical blocks, extract them into private methods, and update the original method to call them in order. The public contract does not change, the tests still pass, and the next developer who reads registerCustomer sees five named steps instead of 80 lines of mixed concerns.

5.2 Refactor 2: Replacing Constructor Boilerplate With Records

Java records, available since Java 16, eliminate a huge amount of DTO boilerplate. Yet many older codebases still have hand-written immutable classes with manual constructors, equals, hashCode, and toString. Migrating these classes is mechanical work — the kind of work that is perfect for Claude Code because the rules are simple, but the volume is high.

# Refactor prompt
 
Find all DTO classes in com.example.shop.dto.
 
For each class that:
  - Has only final fields with a single constructor that assigns them
  - Has only getters (no setters)
  - Overrides equals, hashCode, and toString in the standard way
 
...convert it to a Java record with the same field names and order.
Update all callers (constructors and getter calls — record accessors
have no 'get' prefix).
 
Do not touch classes that have additional behavior beyond simple data holding.

This is genuinely tedious work to do by hand because each conversion touches the class itself, every place the class is constructed, and every place a getter is called. Claude Code will perform all three steps in a single pass and show you a single, coherent diff. You will still want to read it carefully — that is the whole point of the diff — but the typing is gone.

Interview Insight: A common Java interview question is ‘when would you use a record versus a class?’ The clean answer: use records when the class is purely data with no internal mutable state and no behavior beyond simple accessors. Records are perfect for DTOs, value objects, and event payloads. Use a regular class when the type owns identity, has lifecycle logic, or needs inheritance. Knowing this distinction is exactly the judgement call you make every time you decide what to convert and what to leave alone during refactoring.

5.3 Refactor 3: Renaming a Field Across the Codebase

Renaming feels like a small thing. You change a name, save the file, and move on. Inside one file, that is exactly how it works. But a real project is not one file. There are many files that all talk to each other. One name can show up in hundreds of places. Some are easy to spot. Many are not.

A field named userName might appear in entities, DTOs, mappers, controllers, tests, JSON property annotations, and SQL queries. Modern IDEs like IntelliJ handle most of these, but they sometimes miss string-based references, such as JSON paths in test fixtures or column names in native SQL.

With Claude Code, you can describe the rename in detail and ask it to find every reference, including those that IDEs might miss. You can say: rename the field ‘userName’ to ‘username’ on the Customer entity and all related DTOs, including JSON property names and any references in native SQL or Flyway migration files. The model will scan, list every occurrence, and update them while you watch.

6. Reading Code Before Writing It: The Habit That Matters Most

If there is one habit that separates good Claude Code use from poor Claude Code use, it is the habit of asking it to read first and write second. When you say ‘read X and Y, then do Z’, the resulting code matches your project. When you skip the reading step, the resulting code matches some generic Spring Boot tutorial.

This is especially important on legacy projects with non-obvious conventions. Maybe your services throw a custom DomainException instead of a RuntimeException. Maybe your controllers all return ResponseEntity instead of bare types. Maybe your tests use a custom @SpringBootTest base class. Claude Code cannot guess these things, but it can absolutely match them after reading two or three example files.

# A high-quality "read first" prompt
 
Read OrderController, OrderService, and OrderControllerTest as reference.
 
Following the EXACT same patterns:
  - Add a new endpoint POST /api/orders/{id}/cancel
  - Add a corresponding service method
  - Add a unit test
 
Use the same exception types, same response wrapper, same logging style,
same test base class as those reference files.

This kind of prompt produces code that genuinely fits in. The first time you try it on a real codebase, the diff feels almost suspiciously normal — there is nothing in it that screams ‘AI generated’. That is the point.

7. Running and Verifying: Closing the Loop

Writing or refactoring code is only half the job. The other half is verifying that it actually works. Claude Code can run your build, your tests, and your linters, then read the output and fix what fails. This closed loop is what turns it from a clever code generator into a useful coding partner.

In a Maven project, you can simply tell it: run mvn clean verify, and if anything fails, fix it. It will run the build, parse the output, identify the failing tests or compile errors, and propose changes. You review each change, just as before. For most small features and refactors, the loop converges in one or two iterations.

# Verification prompt after a refactor
 
Run: mvn -q -DskipITs verify
 
If there are compile errors, fix them.
If there are unit test failures, read the failure output, find the cause,
and fix the production code (not the test) unless the test assertion
is genuinely wrong.
 
Show me the final diff once everything is green.

There is something important in that prompt: ‘fix the production code, not the test’. AI assistants left to themselves will sometimes ‘fix’ a failing test by weakening the assertion, which is the opposite of what you want. Explicitly telling it to fix the production code instead is a small step that prevents a real failure mode. Treat your tests as the contract, and Claude Code will respect them.

8. Patterns That Work and Anti-Patterns to Avoid

After using Claude Code on real Java projects for a while, a clear set of habits emerges that separates productive sessions from frustrating ones. None of these is unique to Java, but they all have very Java-flavoured consequences.

8.1 Patterns That Work

Be specific about the scope. Tell Claude Code which files to read and which files to change. Tight scope means small, reviewable diffs. Always ask for the diff before applying. Reading the diff is the single most valuable five seconds you will spend, and it catches subtle issues immediately. Iterate in small steps. Three small prompts almost always beat one giant prompt, because each step gives you a checkpoint to verify.

Lean on your tests. If your project has a strong test suite, Claude Code becomes dramatically more reliable because the build itself catches mistakes. If your project does not, asking Claude Code to add tests as it goes is a good way to gradually build the safety net.

8.2 Anti-Patterns to Avoid

Vague prompts produce vague code. ‘Make this better’ is not a prompt — it is a wish. Tell the model what ‘better’ means: shorter methods, fewer dependencies, no static state, no checked exceptions, whatever you actually want. Avoid letting it touch unrelated files unless you ask. If you do not say ‘do not change unrelated code’, it sometimes will, and the diff explodes. Do not skip code review just because the AI looks confident. The model is helpful, but it is not infallible. The diff is your last line of defence; read it.

PatternWhy It Matters
Read first, write secondGenerated code matches your project conventions instead of a generic tutorial.
Specify files to touchKeeps the diff focused and easy to review.
Each step is a checkpoint you can cheaply roll back to.You build the safety net while you build the feature.
Run mvn verify after every changeThe build is the source of truth, not the model’s confidence.
Iterate in small stepsEach step is a checkpoint you can roll back from cheaply.
Avoid ‘just make it better’Vague prompts produce vague, sometimes regressive, edits.

9. A Realistic End-to-End Example

Let us tie everything together with a single, realistic mini-task. Suppose your team is working on the same Spring Boot shop service from earlier, and you have been asked to add a ‘soft delete’ capability for customers. Instead of removing rows, you mark them as deleted and exclude them from normal queries. This involves an entity change, a repository change, a service change, a controller change, a database migration, and updated tests.

Without Claude Code, this is a one-day task on a real codebase, mostly because of the surface area. With Claude Code, broken into a few clear prompts, it is closer to an hour. Here is what the prompt sequence might look like, in order, with verification between each step.

# Step 1 — Schema and entity
Read Customer.java and the Flyway migrations folder.
Add a 'deleted_at' nullable timestamp column via a new Flyway migration.
Add a corresponding 'deletedAt' field on Customer with the right JPA annotation.
Do not change any other code yet.
 
# Step 2 — Repository and queries
Update CustomerRepository so default finder methods exclude soft-deleted rows.
Add explicit findIncludingDeleted variants for admin use.
 
# Step 3 — Service and controller
Add CustomerService.softDelete(Long id) that sets deletedAt = now() and saves.
Add DELETE /api/customers/{id} that calls softDelete.
Existing GET endpoints must not return soft-deleted customers.
 
# Step 4 — Tests
Add unit tests for softDelete and integration tests confirming that
soft-deleted customers do not appear in list/search endpoints.
 
# Step 5 — Verify
Run: mvn -q verify
Fix any failures in the production code (not in the tests).
Show me the full final diff.

Each prompt is small, scoped, and verifiable. Each one builds on the last. By the end of step 5, you have a complete, tested, migration-backed feature, and every change has gone through your eyes as a diff. That is the workflow. It is not magic — it is just a tighter version of the discipline a good Java developer already has.

10. When Not to Use Claude Code

It is fair to ask: are there times when you should not reach for Claude Code? Yes, and being honest about that makes the rest of the workflow more credible. Claude Code is a tool, not a religion.

For tiny, one-line edits — fix a typo, change a constant — opening Claude Code is overkill. Your IDE is faster. For exploratory thinking — should we use Kafka or RabbitMQ? — The answer is a design conversation, not a code edit, and a different tool fits better. For code that touches sensitive cryptographic or security-critical paths, AI suggestions should be treated as drafts to be reviewed by a human security expert, not as ready-to-merge changes. And for areas where you simply do not yet understand the existing code, slow down and read it yourself first; otherwise, you will accept changes you cannot evaluate.

None of these is a reason to avoid Claude Code in general. There are reasons to use it where it shines and to set it aside where another tool — your eyes, your IDE, or your senior teammate — is the better choice.

11. Conclusion

Writing and refactoring Java code with Claude Code is not about replacing the developer. It is about removing the tedious parts of the job so the developer can spend more time on what actually matters — design, judgment, and code review. The model writes the boilerplate, you steer the direction, and the build verifies the result.

In this article, we covered why Claude Code fits the Java workflow especially well, how to set the stage for a productive session, how to write new code with focused prompts, how to iterate without losing your place, and how to refactor with confidence on real codebases. We also looked at the patterns that consistently work and the anti-patterns that consistently waste time.

The most useful thing you can do after reading this is to try it on a real project. Pick one small feature or one small refactor that has been sitting on your to-do list. Run Claude in the project root. Read first, write second, verify the build, review the diff. The first time the loop closes cleanly, the value of this workflow becomes obvious in a way no article can fully capture.

 

Leave a Comment