Documentation beats memory: building repos for humans and AI
1. Overview
Coding agents are slowly becoming “team members”: they read code, fix bugs, write tests, and open pull requests. The problem is agents forget everything between sessions. Some tools offer “memory” features, but that saved context is usually tied to one person, hard to review, and doesn’t track with your code versions.
This post explains why documentation in your repo is more reliable than agent memory. It also shows you how to structure a Spring Boot repo so that both newcomers and agents get it right from day one. After reading, you’ll know how to write an AGENTS.md guide file, consolidate checks into a single command, turn conventions into tests, and document design decisions.
2. Why Memory Falls Short
Consider two ways an agent might know that the domain layer shouldn’t depend on Spring:
- Memory: you told it before, and it saved a note somewhere.
- Repo: the rule lives in
AGENTS.mdand there’s a test that fails if anyone violates it.
The first approach has obvious weaknesses:
- The note lives on one person’s machine or account. A colleague using a different agent won’t have it.
- Nobody reviews the note. If the agent “remembered” wrong, it’s hard to spot.
- Notes don’t follow branches. When the rule changes on
main, the old memory persists.
The second approach is versioned, reviewed via pull request, and stays correct with each commit. More importantly, it helps humans too. What an agent needs to get it right overlaps almost exactly with what a new developer needs in their first week.
That’s why this matters now. Agents don’t expose a new problem. They expose the cost of poor documentation, because they can’t ask “hey, how does this work?” the way a new person can.
3. The Agent Guide File: AGENTS.md
Many agent tools read a Markdown file at the repo root for instructions. The filename varies by tool, but AGENTS.md is a common convention. If your tool uses a different name, you can symlink instead of duplicating.
3.1. What to Include
# AGENTS.md
## Project
Order service (Spring Boot 3, Java 21). Main modules: `order`, `payment`, `customer`.
## Commands
- Build + full checks: `./scripts/verify.sh`
- Run one test: `./mvnw -q test -Dtest=DiscountPolicyTest`
- Run app locally: `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`
## Mandatory conventions
- `..domain..` must not import `org.springframework..` (verified by `ArchitectureTest`).
- Amounts use `BigDecimal`, not `double`.
- Any change to business behavior must have a test in `src/test/java/.../domain`.
## Don't do this
- Never modify files in `src/main/resources/db/migration` after merge; create a new migration instead.
- Never commit `application-local.yml`.
## Further reading
- Architecture decisions: `docs/adr/`
- Payment flow: `docs/payment-flow.md`
Each section has its own purpose:
- Commands: agents run commands a lot. A wrong command wastes iteration cycles. Make commands exact and copy-paste ready.
- Mandatory conventions: each line should point to how to verify it (test name, linter rule). Conventions without checks get skipped.
- Don’t do this: list actions that are hard to undo.
- Further reading: point to detailed docs instead of repeating them here.
3.2. Keep It Short, Point to the Source of Truth
AGENTS.md should be short—readable in a minute. The agent loads this into context each session. The longer it is, the more context it uses, and the more likely it’s outdated.
General rule: don’t re-explain what code or build already states clearly. Don’t list each dependency—pom.xml has it. Don’t explain each endpoint—the OpenAPI spec does. Only write what can’t be inferred from code.
Warning: Never put secrets, tokens, or sensitive internal URLs in
AGENTS.md. It’s read by external tools and lives in git history forever.
4. A Single Verification Command
Agents work best with a clear feedback loop: edit, run one command, see pass or fail. If full validation means running four commands in sequence, the agent will skip steps. So do humans.
#!/usr/bin/env bash
# scripts/verify.sh — runs exactly what CI does. Non-zero exit means not ready to merge.
set -euo pipefail
echo ">> Compile + unit test + architecture test"
./mvnw -B -q verify
echo ">> Check for leftover TODO-AGENT markers"
if grep -rn "TODO-AGENT" src/; then
echo "Unresolved TODO-AGENT markers found" >&2
exit 1
fi
echo "OK: ready to create pull request"
Explanation:
set -euo pipefailstops the script immediately on error. This way, the agent sees a non-zero exit code instead of a tall log ending with misleading ‘OK’.-B(batch mode) disables colored output and progress bars. The log is cleaner, making errors easier to spot.- The
grepstep is an example of a team convention: agents are allowed to mark incomplete work withTODO-AGENT, but must resolve them before opening a PR.
On success, you see:
>> Compile + unit test + architecture test
>> Check for leftover TODO-AGENT markers
OK: ready to create pull request
The key point: CI must call this exact script. If CI runs a different set of commands, verify.sh will drift from reality and become outdated.
5. Turn Conventions Into Tests With ArchUnit
A line like “domain shouldn’t depend on Spring” in a document is just advice. Turn it into a test and it becomes law. ArchUnit lets you write architecture rules as JUnit tests.
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
package com.nonla.shop;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packages = "com.nonla.shop", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {
@ArchTest
static final ArchRule domainKhongPhuThuocSpring = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("org.springframework..")
.because("domain must be testable without Spring context, see docs/adr/0003-domain-pure.md");
@ArchTest
static final ArchRule webKhongGoiThangRepository = noClasses()
.that().resideInAPackage("..web..")
.should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
.because("controllers go through services to keep transactions in one place");
}
The most valuable part is .because(...). When the rule is violated, the error message includes the reason and a link to the ADR. Suppose someone adds @Component to DiscountPolicy in the domain package. The output looks like:
Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package
'..domain..' should depend on classes that reside in a package 'org.springframework..',
because domain must be testable without Spring context, see docs/adr/0003-domain-pure.md'
was violated (1 times):
Class <com.nonla.shop.order.domain.DiscountPolicy> is annotated with <org.springframework.stereotype.Component> ...
An agent reading this message knows exactly what to do: remove the annotation, then register the bean in a @Configuration class in the infra layer. A good error message is documentation showing up exactly when someone needs it.
6. Tests as Business Documentation
Business rules are what agents get wrong most easily. It can’t guess that “first-order discount caps at 50,000đ”. Let code and tests say it.
package com.nonla.shop.order.domain;
import java.math.BigDecimal;
import java.math.RoundingMode;
public record Order(String customerId, BigDecimal subtotal, boolean firstOrder) {}
class DiscountPolicy {
static final BigDecimal FIRST_ORDER_RATE = new BigDecimal("0.10");
static final BigDecimal FIRST_ORDER_CAP = new BigDecimal("50000");
BigDecimal discountFor(Order order) {
if (!order.firstOrder()) {
return BigDecimal.ZERO;
}
BigDecimal raw = order.subtotal().multiply(FIRST_ORDER_RATE)
.setScale(0, RoundingMode.DOWN);
return raw.min(FIRST_ORDER_CAP);
}
}
Test it with meaningful names:
package com.nonla.shop.order.domain;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Order discount policy")
class DiscountPolicyTest {
private final DiscountPolicy policy = new DiscountPolicy();
@Test
@DisplayName("First order gets 10% off total")
void firstOrderGetsTenPercent() {
var order = new Order("c-1", new BigDecimal("200000"), true);
assertThat(policy.discountFor(order)).isEqualByComparingTo("20000");
}
@Test
@DisplayName("First order discount is capped at 50,000đ")
void firstOrderDiscountIsCapped() {
var order = new Order("c-1", new BigDecimal("2000000"), true);
assertThat(policy.discountFor(order)).isEqualByComparingTo("50000");
}
@Test
@DisplayName("Repeat orders get no discount")
void repeatOrderGetsNoDiscount() {
var order = new Order("c-1", new BigDecimal("2000000"), false);
assertThat(policy.discountFor(order)).isZero();
}
}
Explanation:
@DisplayNameturns test output into readable specs. Product people understand it too.isEqualByComparingTocomparesBigDecimalby value, so20000and20000.00are equal. UsingisEqualTohere is a common mistake—it compares scale too.- If asked to bump the discount to 15%, an agent immediately sees which test to change. If it accidentally removes the cap, the second test fails.
When run, the output looks like:
Order discount policy
✔ First order gets 10% off total
✔ First order discount is capped at 50,000đ
✔ Repeat orders get no discount
7. Document the ‘Why’: ADRs and package-info
Code says what the system does. Tests say what it should do. But nothing explains why you chose that way. Without the ‘why’, agents tend to ‘clean up’ things that look redundant but have reasons.
An Architecture Decision Record (ADR) is a short file per decision:
# 0003. Domain Layer Must Not Depend on Spring
Status: Accepted
## Context
Domain tests used to require Spring context startup, which was slow and fragile.
## Decision
The `..domain..` package uses only JDK. Beans are declared in `..infra.config`.
## Consequences
- Domain tests run with plain JUnit.
- Extra configuration classes needed. Verified by `ArchitectureTest`.
For package-specific guidance, package-info.java is the right place—agents see it when they open the directory:
/**
* Order business rules. JDK only, no Spring (ADR 0003).
* All amounts are BigDecimal, in VND, rounded down to the nearest dong.
*/
package com.nonla.shop.order.domain;
8. Common Mistakes and When Not to Do This
Documentation duplication. The same rule appears in README.md, AGENTS.md, and a wiki. After a few months, all three say different things. Pick one source of truth and link to it from the others.
Guide contradicts CI. AGENTS.md says “run mvn test”, but CI also runs integration tests. Agent reports ‘done’ while the PR stays red.
Vague commands to the agent. ‘Write clean code’ helps nobody. ‘Public methods in ..service.. must have a test’ is useful, and even better if verifiable.
Dumping everything in one file. A multi-thousand-line AGENTS.md dilutes context. Important info gets buried.
Note: Not every repo needs all of this. A one-off script, a prototype you’re about to abandon, or a repo with a few hundred lines needs just a
README.mdwith run commands. ArchUnit only pays for itself if you have multiple layers or modules with real boundaries to maintain. Writing docs for things that don’t exist yet is just cruft.
That said, agent memory still has a place. It’s good for personal preferences—how you explain things or your preferred editor. Those aren’t repo concerns. But project rules should always live in the repo.
9. Conclusion
Agents don’t need better memory—they need repos that are easier to understand. You’ve seen how to:
- Write a short
AGENTS.mdwith exact commands and conventions you can verify. - Consolidate all checks in
scripts/verify.shand have CI use the same script. - Turn architecture conventions into ArchUnit tests, with error messages pointing to the why.
- Use tests with
@DisplayNameas business specs. - Document the ‘why’ with ADRs and
package-info.java.
All of this helps humans first. Agents are just the reason we can’t keep putting it off. Start small: write verify.sh and the first five lines of AGENTS.md this week.