Watch what happens when an engineering team introduces an AI coding agent to an existing Spring Boot codebase.
On day one, the agent writes a Fat Controller that injects a JpaRepository directly and returns a raw database @Entity to the frontend. A senior engineer catches it in code review and adds a rule to AGENTS.md: Never call repositories from controllers.
Next week, the agent puts 150 lines of Stripe billing logic inside a service method without a timeout. Another paragraph goes into AGENTS.md. Six months later, AGENTS.md is a 1,500-line encyclopedia of rules, and the agent is ignoring half of them.
The team assumes the model is not smart enough for enterprise code.
In February 2026, OpenAI published Harness Engineering: Leveraging Codex in an Agent-First World. Their team shipped a production system with roughly one million lines of code over five months without writing a single line of code by hand. They hit the exact same wall early on, and their takeaway changes how we should think about AI-assisted engineering:
You cannot fix an environment problem by writing a longer prompt.
When an agent makes a mistake, the instinct is to lecture it in markdown. Harness engineering flips this around. Instead of stuffing every rule into the prompt upfront, you build a harness: an environment where context is loaded only when needed, and architectural rules are enforced by the build tool at the exact moment the agent breaks them.
You do not need a greenfield repository to do this. Traditional frameworks like Spring Boot MVC are built on strict conventions. Once you wire those conventions into fast feedback loops, a standard MVC monolith becomes an ideal agent harness.
Shift 1: Treat Context Like RAM, Not a Hard Drive
Why does a 1,500-line AGENTS.md fail? Because an agent’s context window is working memory. Every line of upfront instructions competes for attention with the actual Java classes the agent needs to read and edit.
Even worse, agents are copycats. They follow the patterns they see in the code over the rules they read in a prompt. If your legacy codebase has 40 controllers from 2019 that return raw @Entity objects, the agent will copy those controllers even if AGENTS.md tells it not to.
OpenAI solved this by shrinking AGENTS.md to roughly 100 lines. It acts strictly as a routing map, delegating everything else to progressive disclosure:
AGENTS.md(Always in RAM): A 100-line map of build commands, folder locations, and core boundaries.- Golden Reference Package (Pattern to Copy): One small, pristine vertical slice in the codebase (
com.company.app.reference) that shows the exact modern standard for JavarecordDTOs,@RestControllerAdvice(RFC 7807), and slice tests. Instead of describing good code in prose, you tell the agent to copy the reference package. - Agent Skills (
SKILL.mdLoaded on Demand): Step-by-step playbooks stored in.agents/skills/. At startup, the agent only sees the skill’s name and one-line description. It reads the full playbook only when triggered by a matching task.
# AGENTS.md - Spring Boot MVC Routing Map
## Fast Verification (Run before finishing any task)
- Slice Test: `./mvnw test -Dtest=ClassName#methodName`
- Architecture & Style Check: `./mvnw test-compile checkstyle:check -Dtest=ArchitectureTest`
- Local Health Check: `curl -s http://localhost:8080/actuator/health`
## Where to Look
- Golden Reference Slice (Copy this pattern): `src/main/java/com/company/app/reference/`
- Active Execution Plans: `docs/exec-plans/active/`
- Architecture Rules: `docs/architecture/mvc-layers.md`
## Core MVC Invariants
1. Package by domain first (`com.company.app.<domain>`), then by layer (`controller`, `service`, `repository`, `entity`, `dto`, `gateway`).
2. `controller`: HTTP routing and `@Valid` input validation only. Max 40 lines per method.
3. `service`: `@Transactional` boundaries and business orchestration.
4. `entity`: JPA models. Never expose an `@Entity` in a Controller or View. Map to a `dto` record.
5. `gateway`: All external SDKs and HTTP clients must sit behind an interface in `gateway`.
Notice what happens here. Package-by-feature (com.company.app.billing) keeps all related files in one folder so the agent does not have to search across hundreds of unrelated classes. And the agent never pays the context cost of reading migration rules or UI testing guides until it actually needs them.
Shift 2: Your Linter Is No Longer for Humans. It Is a Prompt Generator.
This is the single biggest aha moment in harness engineering.
For twenty years, we wrote linter and test failure messages for human developers:
Rule 'controllers should not depend on repositories' was violated (1 times):
Method <BillingController.<init>(BillingRepository)> calls constructor
When a human sees that error, they know what to do. When an AI agent sees that error, it often tries a lazy workaround.
Now look at what happens when you realize that terminal output (stderr) is a prompt injection. In a Spring Boot project, you can use ArchUnit to enforce your MVC layers in milliseconds. By writing explicit refactoring instructions inside the .because(...) clause, the compiler teaches the architecture only when the agent makes a mistake.
package com.company.app;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import jakarta.persistence.Entity;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packages = "com.company.app")
public class ArchitectureTest {
@ArchTest
static final ArchRule controllers_must_not_access_repositories =
noClasses()
.that().resideInAPackage("..controller..")
.should().dependOnClassesThat().resideInAPackage("..repository..")
.because("REMEDIATION: Controllers must remain thin and delegate to Services. "
+ "Inject the domain @Service class into your Controller instead of the @Repository.");
@ArchTest
static final ArchRule controllers_must_not_expose_jpa_entities =
noClasses()
.that().resideInAPackage("..controller..")
.should().dependOnClassesThat().areAnnotatedWith(Entity.class)
.because("REMEDIATION: Never accept or return JPA @Entity classes in a Controller. "
+ "Create a Java record in the ..dto.. package and map the Entity inside the @Service layer.");
@ArchTest
static final ArchRule external_sdks_must_stay_in_gateways =
noClasses()
.that().resideInAnyPackage("..controller..", "..service..", "..repository..")
.should().dependOnClassesThat().resideInAnyPackage("com.stripe..", "software.amazon.awssdk..")
.because("REMEDIATION: External SDKs are isolated to ..gateway.. packages. "
+ "Define a gateway interface and inject it into your @Service.");
}
Think about the economics of this:
- When the agent writes valid code, these rules cost zero tokens in its context window.
- The moment the agent takes a shortcut and returns a
UserEntityfromUserController, running./mvnw test -Dtest=ArchitectureTestprints theREMEDIATION:prompt directly into the terminal. - The agent reads the failure, creates the
recordDTO, and fixes its own code before a human ever opens a pull request.
Pair this with a Checkstyle MethodLength rule capped at 40 lines on *Controller.java files, and Fat Controllers become mechanically impossible to merge.
Shift 3: Stop Making the Agent Code Blind
An agent cannot fix what it cannot see. In many traditional Spring Boot setups, verifying a change requires booting the entire monolith, waiting 45 seconds, and scrolling through a 200-line multi-line Java stack trace. That slow, noisy loop destroys agent reliability.
OpenAI made their runtime legible by giving agents direct access to Chrome DevTools, structured logs, and local metrics. In Spring Boot MVC, you already have these tools built in:
| Blind Agent Loop (Default) | Legible Agent Loop (Harness) | Why It Matters |
|---|---|---|
Booting the full @SpringBootTest context (45s) |
Running @WebMvcTest(BillingController.class) (2s) |
The agent tests HTTP routes, @Valid rules, and JSON serialization 20x faster. |
| 200-line raw Java stack traces in console | Single-line JSON logs in target/app-dev.log queried with jq |
One command gives the agent the exact root cause without flooding its context window. |
| Guessing if database migrations or endpoints work | Querying Spring Boot Actuator (/actuator/health, /actuator/flyway, /actuator/metrics) |
The agent verifies schema state and response latency via curl. |
| Guessing if Thymeleaf/JSP views render properly | Chrome DevTools MCP / Playwright snapshot | The agent loads localhost:8080, submits forms, and checks browser console errors. |
When an agent can run a @WebMvcTest slice in two seconds and check ArchitectureTest in another second, it can iterate five times in the time it used to take to boot the server once.
How to Apply This Tomorrow (New vs. Legacy Projects)
You can package this entire setup into a reusable Agent Skill directory so an agent can apply it to any repository on command:
.agents/skills/spring-mvc-harness/
├── SKILL.md # Playbook for Bootstrap vs. Legacy Refactor modes
├── resources/
│ ├── AGENTS.template.md # The 100-line routing map template
│ ├── ArchitectureTest.java # Drop-in ArchUnit rules with REMEDIATION prompts
│ ├── checkstyle-mvc.xml # 40-line Controller method limit
│ └── logback-dev-json.xml # Single-line JSON logging config
└── scripts/
└── check-actuator-health.sh # Helper script to query Actuator & parse JSON logs
Starting a New Project: Build the Harness First
Before asking the agent to build your first business feature, run the skill in Bootstrap Mode. The agent sets up AGENTS.md, drops in ArchitectureTest.java and checkstyle-mvc.xml, configures JSON logging, and creates the com.company.app.reference vertical slice. Every feature built afterward is automatically constrained by the harness.
Refactoring a Legacy Monolith: The FreezingArchRule Trick
What if you have a five-year-old Spring Boot app with 300 existing layer violations? If you drop ArchitectureTest.java into the repo today, the build will fail 300 times. Asking an agent to fix all 300 violations in one prompt will fail.
Instead, have the agent wrap your ArchUnit rules in FreezingArchRule.freeze(...):
@ArchTest
static final ArchRule no_entity_leaks =
FreezingArchRule.freeze(controllers_must_not_expose_jpa_entities);
On the first run, ArchUnit records all 300 existing violations into a plain-text store (archunit_store/) and passes the build. From that second forward:
- Zero new debt: Any new code the agent writes must follow the new rules, or the build fails with the
REMEDIATION:prompt. - Linter-driven refactoring: When you want to clean up the legacy
billingdomain, the agent creates a checklist indocs/exec-plans/active/refactor-billing.md, removes thebillingentries from the freeze store, and runs./mvnw test -Dtest=ArchitectureTest. ArchUnit outputs the exact list of violations inbillingwith remediation instructions, and the agent fixes them one by one until the test turns green.
Summary
Harness engineering is not about replacing Spring Boot MVC with an experimental AI framework. It is a shift in how you manage the agent’s attention and feedback:
- Keep
AGENTS.mdunder 100 lines and point the agent to a pristinereferencepackage to copy, loading detailed workflows on demand via Agent Skills. - Write linter errors as prompts. Put
REMEDIATION:instructions inside ArchUnit and Checkstyle so the build tool corrects the agent only when it crosses a boundary. - Give the agent a 2-second feedback loop using
@WebMvcTest, single-line JSON logs, and Spring Boot Actuator. - Use
FreezingArchRuleto lock down legacy repositories immediately and burn down old architectural debt one domain at a time.