AGENTS.md Makes Your Java Codebase AI-Agent Ready AGENTS.md Makes Your Java Codebase AI-Agent Ready

I keep seeing teams hand AI agents a Java codebase and hope for the best. AGENTS.md is the missing piece, a simple contract that tells agents how your project actually works. Here is why I think every repo needs one.

I added an AGENTS.md file to the ITJobOpportunities backend a while ago, mostly out of curiosity. By then, I was wondering why I hadn't done it two years earlier. It's not a magic incantation — it's a plain markdown file. But it changed how fast Cursor and Claude Code could actually work inside jobs-posting , and that's the part nobody warns you about until you try it. Here's the situation I kept running into. I'd open Cursor, ask it to add a new endpoint to JobApplicationController , and it would spend the first thirty seconds guessing at conventions. Does this project use constructor injection or field injection? Are we on Lombok or writing getters by hand? Is the test folder mirroring src/main/java ? It would usually guess right, because Spring Boot projects tend to look alike, but "usually right" isn't "right," and I was tired of re-reviewing agent output line by line just to catch a Liquibase changeset in the wrong format. So I wrote an AGENTS.md . Fifteen, maybe twenty minutes. The difference in agent output afterward was not subtle. What AGENTS.md actually is It's not a new framework and it's not tied to one vendor. AGENTS.md is becoming a convention — some people call it the "README for robots" — that AI coding agents look for at the repo root, the same way they've always looked for README.md . Cursor picks it up. Claude Code reads it as project context. Copilot's agent mode respects it too, at least in the workspaces I've tested. There's no strict schema, which annoyed me at first because I wanted a spec to follow. But the lack of rigidity is the point: you write what actually matters for your codebase, not what a template thinks matters. For a Java shop, that usually means answering questions an agent can't infer just by reading build.gradle : Which Java version, and are we actually using its features (I'm on Java 25 for jobs-posting , and I don't want an agent writing code like it's targeting Java 8) Which Spring Boot version, because 3.5.x and 4.x have real behavioral differences in auto-configuration and observability defaults How we structure packages ( com.josalero.posting.core.job.jobfit , not a flat com.company.controllers ) Where tests live and what testing library we prefer (JUnit 5, Testcontainers for anything touching Postgres) What NOT to touch — this one matters more than people think The Java-specific stuff that trips agents up Java accumulates conventions that live in developers' heads and nowhere in the code. Lombok is a good example. If an agent doesn't know your team uses @Data and @Builder , it'll write explicit getters, setters, and a full constructor for every new entity, and now half your codebase has Lombok and half doesn't. That's a mess to clean up later. I put this near the top of my AGENTS.md for jobs-posting : ## Code style conventions - Use Lombok annotations (`@Data`, `@Builder`, `@RequiredArgsConstructor`) for all entity and DTO classes. Do not write explicit getters/setters. - Constructor injection only. No `@Autowired` on fields. - Package structure follows domain, not layer: `core.job.<feature>`, not `controllers` / `services` / `repositories` as top-level packages. - Liquibase changesets go in `changeLog-2.0.json`, one changeset per logical schema change, never batched. That last bullet alone saved me a genuinely annoying cleanup. Before I had this documented, an agent bundled three unrelated schema changes into a single Liquibase changeset because that's what it inferred from a slightly ambiguous example file. Technically it worked. But it broke our "one changeset, one rollback" discipline, and untangling that after the fact took longer than it should have. Testing conventions matter more than you'd guess Here's a section I wish I'd written on day one instead of week three: ## Testing - Unit tests: JUnit 5 + Mockito, colocated in `src/test/java` mirroring the main package structure. - Integration tests: Testcontainers with Postgres 16, tagged `@Tag("integration")`, run separately via `./gradlew integrationTest`. - Do not mock the database in integration tests. Use a real Testcontainers instance. - Every new controller endpoint needs at least one integration test hitting the real Spring context (`@SpringBootTest`). Without this, agents default to over-mocking. They'll mock a repository, mock a service, mock the whole world, and you end up with tests that pass but don't verify anything real touches the database correctly. I've seen this happen on human-written code too, so it's not purely an AI problem — but agents do it more often because mocking is the "safe" default when the convention isn't spelled out. Where this connects to what I'm building Running ITJobOpportunities as a founder, mostly solo with contractors rotating in, means I don't have a five-person team enforcing conventions through code review osmosis. If I'm not careful, every AI-assisted commit drifts slightly from the last one, and six months later the codebase looks like it was written by a committee that never talked to each other. AGENTS.md became my stand-in for that missing team consensus. When I'm working on Job Fit — the "check your fit" card on a published job listing that scores a resume against the posting — I need an agent to understand that fit-check results are ephemeral unless the resume has an email address, and that fit-check-only leads should never leak into the recruiter-facing candidate list. That's not something you can infer from the code alone unless you trace through JobFitController , CandidateService , and half of JobApplicationService . It's the kind of business rule that lives in a spec doc and never makes it into code comments. So a chunk of my AGENTS.md for that repo looks less like code style and more like domain rules: ## Domain boundaries (read before touching job fit or candidate code) - Job Fit is a public, anonymous-friendly feature. It must NOT create a full Candidate record unless an email address is extracted from the uploaded resume. - Job Fit leads (candidates with only an email, no application) are NOT visible in the ATS candidate pipeline. Do not join them into `CandidateController` list queries. - AI-generated candidate assessments are manually triggered by a recruiter, never automatic on apply. Do not wire this into `JobApplicationService.submit()`. That third bullet exists because I almost automated it myself, before I stopped and thought about what "automatic AI assessment on every application" actually means for a recruiter who wants to skim ten resumes, not read ten AI essays they didn't ask for. Keeping that boundary explicit in AGENTS.md means an agent won't "helpfully" wire it up for me while I'm asking for something unrelated, like a bug fix in the email service. A quick comparison: AGENTS.md vs. just writing a good README I went back and forth on whether a thorough README.md would do the same job. Short answer: no. A README is written for humans skimming it once, usually to figure out how to run the project locally. An AGENTS.md is written for something that reads it fresh, every single session, with no memory of the last conversation. That difference in audience changes what you write. A README says "run docker-compose up to start Postgres." An AGENTS.md says "the local Postgres runs on port 5433, not 5432, and the schema is named job , not public ." A README explains what the product does for a new hire. An AGENTS.md explains which files to never touch without asking — looking at you, application-production.yml . A README can afford to be a little stale. An AGENTS.md going stale means an agent confidently writing code against conventions you dropped three sprints ago, which is worse than no guidance at all. I keep both, but I stopped trying to make the README do double duty. It was making the README bloated and the agent guidance buried in prose nobody, human or otherwise, was going to read carefully. What I actually put in mine (the short...