When AI assistants write Java, they tend to default to the worst habits the language ever supported. They append methods to whatever class is open until it crosses a thousand lines. They generate POJOs with mutable getters and setters when a one-line record would do. They wrap everything in try { ... } catch (Exception e) {} because the model doesn't know which exception is actually possible. They stamp @Transactional on random methods, smear @Value("${...}") across the codebase, and mix Stream.forEach with side effects until reads and writes look the same. The result compiles. It even passes the happy path. Then it goes to production and quietly leaks threads, breaks lazy loading, and turns log aggregation into archaeology.
The fix isn't a smarter model. It's giving the model the same conventions every senior Java developer earned the hard way after their first three production incidents. A CLAUDE.md at the root of your repository (or .cursorrules, or AGENTS.md — same idea) teaches the AI your house rules before it writes a single line.
Below are the 13 rules I keep in mine. Each one closes a specific failure mode I've seen AI repeat across real Java 17 / Spring Boot 3 codebases.
Rule 1: One class, one responsibility — no God classes
If a class is over ~300 lines, has Manager/Util/Helper in its name, or fields that touch unrelated domains, split it. AI keeps appending methods to whatever file is open. Cohesion beats convenience. A class should have one reason to change.
Rule 2: Records for DTOs and value objects, classes for entities and services
Java 16+ records are the right shape for transport and value types. AI still generates POJOs with mutable getters and setters by default. JPA @Entity classes stay as class because Hibernate needs a no-arg constructor and proxying. Records are for immutable data; entities are for things with identity and a lifecycle.
Rule 3: Java naming conventions are non-negotiable
PascalCase for classes and interfaces. camelCase for methods, fields, and locals. UPPER_SNAKE_CASE for static final constants. Package names lowercase, no underscores. No I prefix on interfaces (UserRepository, not IUserRepository). No Impl suffix unless there are multiple implementations and one is the genuine default. AI loves invoice_service and IUserRepositoryImpl. Don't let it.
Rule 4: Never swallow exceptions — log, wrap, or rethrow
catch (Exception e) { return null; } is the single biggest source of "works on my machine" bugs in production Java. Catch the most specific exception you can. Always include the original as the cause. Prefer unchecked exceptions in service layers — checked exceptions force ceremony for problems the caller can't fix anyway.
Rule 5: No raw types, no unchecked casts, no @SuppressWarnings("unchecked") without a reason
If the compiler is warning, AI shouldn't tell it to shut up. Raw types defeat the entire point of generics. List items = repository.findAll() is a ClassCastException in waiting. If you must suppress, pin it to the smallest possible scope and leave a comment that explains why the cast is safe.
Rule 6: Lombok with intent — @Value/@Builder for DTOs, never @Data on entities
@Data on a JPA entity generates equals/hashCode over mutable fields and triggers full lazy loading whenever something hits a Set<Order>. AI slaps it on everything. Use @Getter/@Setter plus @EqualsAndHashCode(of = "id") on entities, and prefer record over @Value for new immutable types.
Rule 7: Stream API for transformation, loops for side effects
Streams are declarative for map/filter/reduce. They become a liability when they hide mutation, throw checked exceptions, or wrap a single forEach. Rule of thumb: if the stream ends in forEach with side effects, use a for loop. If it produces a value, stream is fine.
Rule 8: Optional<T> is a return type, not a field or parameter
Optional was designed for "this method might not return a value." It is not a substitute for nullable fields (it isn't Serializable), and putting it on a parameter is API noise — callers shouldn't have to wrap and unwrap on every call. If you want compile-time null-safety, use @Nullable/@NonNull from JSpecify.
Rule 9: Make concurrency intent explicit — @Immutable, @ThreadSafe, @GuardedBy
Threading bugs are silent until production. AI rarely annotates intent, which makes future maintainers (including AI) guess. Default to immutable. Reach for ConcurrentHashMap, AtomicLong, and CopyOnWriteArrayList instead of synchronized blocks unless you genuinely need the broader lock. Never use volatile to "make it thread-safe" — it fixes visibility, not atomicity.
Rule 10: Pin versions, use BOMs, fail the build on dependency drift
A pom.xml with <version>LATEST</version> or SNAPSHOT dependencies in main is a Heisenbug factory. Use the spring-boot-dependencies BOM (or platform constraints in Gradle) so transitive versions stay coherent. Run mvn dependency:analyze in CI to catch unused declarations and conflicts before they ship.
Rule 11: JUnit 5 + AssertJ + Mockito — descriptive names, AAA layout
AI writes test1() and assertEquals(true, x). The first time it fails in CI, nobody knows what the contract was supposed to be. Use @DisplayName, name tests methodUnderTest_condition_expectedBehavior, and structure them Arrange/Act/Assert. Use slice tests (@WebMvcTest, @DataJpaTest) instead of full @SpringBootTest for everything that isn't a true integration test.
Rule 12: Spring config goes through @ConfigurationProperties, not scattered @Value
@Value("${some.key}") sprinkled across components turns typos into runtime startup failures with no validation. A typed @ConfigurationProperties record with @Validated plus @NotBlank/@Min/@Max refuses to start the app when configuration is missing. One typed object beats scattered string keys, and the IDE can navigate to it.
Rule 13: SLF4J with placeholders — never string concatenation, never System.out.println
log.info("Created invoice " + id + " for " + customer) allocates String even when the level is disabled and turns structured data into unparseable text. log.info("Created invoice id={} customer={}", id, customer) skips the format step when the level is off and keeps fields parseable downstream. Use MDC for request-scoped context. Never log secrets, full request bodies, or PII.
Wrapping up
These 13 rules don't replace Effective Java — they encode the failure modes AI repeats most often in real Java codebases. Records over POJOs, structured exceptions, typed configuration, immutable concurrency, and SLF4J placeholders aren't style preferences; they're how Java services stay predictable under load and across team handoffs.
I keep a maintained version of this file as a public GitHub Gist — fork it, prune what doesn't fit your stack, and drop it next to your pom.xml:
→ https://gist.github.com/oliviacraft/8be751bd539c98faf9332b4b935b1a27
If you want the full CLAUDE.md Pack — covering Java/Spring Boot, C#/.NET, Go, Rust, TypeScript, Next.js, React, Node.js, Vue 3, Django, FastAPI, Postgres, Docker, Kubernetes, and more — it's $27 here:
Get the CLAUDE.md Pack — oliviacraftlat.gumroad.com/l/skdgt
One file. Thirteen rules per language. Production-grade AI output, every prompt.
Top comments (0)