Over the past few months, I wrote a four-article series on Java patterns for working with LLM agents, primarily Claude Code. However, everything here applies to any agent that reads a repository and edits it.
The series targets Java/Spring developers who already apply Claude Code (or something similar) to a real repository, not those interested in LLMs in theory.
By «agent» I mean not the model itself but a bundle of four components: LLM + repository access + command execution + file edits. The overview’s claims apply to any agent environment with this setup, but the empirical evidence comes from working with Claude Code.
What began as a short note on typing habits grew into four sizeable articles. Almost every aspect of a Java project (individual stack layers, cross-cutting engineering practices) turned out to affect how readable the code is for an agent.
This is the overview I wish I had when starting with agents. It summarizes the series' argument and maps what each part covers, so you can decide which parts are worth reading in full.
A short map, so you don’t read everything front to back:
If time is very short, skip straight to the «Where to start» section: the first three changes fit into one evening, the rest can wait until you get to them. One formulation runs through the entire series:
Agent-friendly code is code where an error is either immediately visible to the compiler or instantly reproducible in a test.
The formulation is deliberately strict. In practice, some errors (races, transaction isolation, performance degradations, production-specific configuration, network failures) will never be instant or fully reproducible. For such cases, the series adds a third option: make the error diagnosable through a structured log, a semantic exception, or an explicit state signal. The thesis in pure form works for most everyday bugs an agent hits on routine edits; for the rest, you live with a weaker form, «diagnosable» rather than «caught».
Until you try writing code by this rule, it looks like a tautology. Most Java codebases do not meet it. Errors live in the configuration that usually does not reach the agent’s context alongside the code being changed. They live in transactions that break only under load. They live in Spring scaffolding, where understanding the dependencies first requires bringing up the context, and in commits that record what changed without recording the reason for the change.
None of these failures is tied to one layer. They arise at the seams between levels, which is why the series is laid out by stack layers rather than by error types. A human works around these gaps through memory, past conversations, and a chance Slack exchange. The agent cannot operate that way: it relies on what it has pulled into context, which means files, command output, tests and a slice of git history. Anything the agent cannot cheaply find and pull into the current session barely figures in its reasoning.
Once you accept this constraint, many design decisions get easier. The question at each fork stops being «which looks nicer» and becomes a question about four buckets:
The patterns I collected in the series are the ones that move work out of the fourth bucket and closer to the first three.
A simple example of what this looks like in practice:
Each of these pairs works only insofar as the type is not diluted back through String, JSON, ORM mapping, or undisciplined code in neighboring layers. But where the discipline holds, whole categories of bugs end up in the fourth bucket.
Part 1. Conceptual foundations. The first article describes how the agent actually reads a repository (through grep, glob, and a token-bounded context window, not by scrolling top-to-bottom), why naming and file size carry more weight for it than for a human, and what typing discipline looks like when the goal is to translate runtime errors into compilation errors.
Part 2. Early error detection patterns. The second article walks through the stack layer by layer and at each level asks where the error surfaces and how to shift it earlier.
Part 3. Logging and git history. The third article is about two channels through which a running program and its history report themselves to the agent: the log and the commit history. For the agent, that is the main source of facts, since it has no debugger and no informal Slack conversation with a colleague to fall back on.
Part 4. Dependencies, architecture, testability. The last article is organized not by stack layer but around a cross-cutting goal: change locality. Its three sections (Spring dependencies, architectural boundaries, and testability) are independent locality mechanisms, each of which prevents a change from spreading beyond a single class or module.
On re-reading (yes, it can be useful to reread your own writing, especially when half of it was produced with an LLM), three ideas recur across all the articles.
The first idea: over the long run, prompts matter less than a checkable environment for shaping an agent’s behavior.
The second idea: the junior-developer analogy is more useful than it sounds. Everyone I talk to about agents eventually arrives at this analogy, usually with mild condescension. There is a real design constraint behind it, and it goes beyond mere convenience.
The third idea is a phrase I kept coming back to across the series: agents do not change what counts as «good»; they only raise the price of «bad».
If the basic layers are not on fire right now (logs are readable, persistence does not fall apart at every step), I would start with three changes from the first part: fix a naming convention in CLAUDE.md and lock in that rule with a dedicated ArchUnit test; introduce typed IDs for the two or three most frequently passed identifiers; move the DTOs the agent most often produces from Map<String, Object> to record. That cuts down the «mixed-up arguments» and «missed field» error categories. Many such bugs move from unstructured runtime to an explicit contract: the signature, deserialization, Bean Validation, and contract tests. In the second part, the biggest payoff comes from configuring maven-enforcer: dependency convergence, the requirePluginVersions rule, and a list of forbidden coordinates that the agent tends to hallucinate (the split of roles between dependencyManagement, pluginManagement, and the enforcer itself is described in part 2 above).
From the third, adopt structured logging with a stable event vocabulary and a requestId that runs across service boundaries. From the fourth, use Clock as a Spring bean instead of Instant.now() in your business logic, and run a single mutation-testing run against your domain model. The PIT report is not a ready-made TODO list but a list of hypotheses: some surviving mutants the agent will close on its own, others will require engineering judgment (equivalent mutations, debatable checks, tests that should not be strengthened mechanically). An agent’s capabilities are changed by swapping models, tools and retrieval and by skill configuration, but that is no longer about code structure. Code structure does not raise the agent’s intelligence. It reduces the volume of implicit context the agent has to infer, and closes the gap between what a human reader can infer and what a machine reader can verify.
In the codebases I have worked with, most of the disappointment with agent-driven development lives in that gap. The model’s capabilities are usually fine; the trouble is the assumption that context can be recovered from the team’s informal knowledge. Once you drop that assumption, the agent becomes more useful, and it stops being separate from the rest of the project’s engineering work.
Designing Java code for the agentic development was originally published in Dev Genius on Medium, where people are continuing the conversation by highlighting and responding to this story.