Maintainability
Modularity, explicit technical debt, context size limits, and continuous refactoring — so what the agent builds today can be changed by anyone tomorrow.
Maintainability is the cost of future change. Code that’s hard to maintain isn’t just annoying — it slows every subsequent feature, accumulates risk in every deployment, and eventually becomes the primary drag on the team’s velocity.
AI-generated code has a specific maintainability risk: it optimises for the immediate task. An agent won’t refactor a class that grew too large, split a module that’s doing too much, or name a file precisely because precision wasn’t in the prompt.
Maintainability rules exist to prevent accumulation of those problems.
Context size limits
The single most effective maintainability constraint you can give an agent is a file size limit. Large files accumulate complexity, violate Single Responsibility, and become invisible to reviewers.
## Size limits (enforce as rules in AGENTS.md)
- Functions: max 20 lines before considering extraction
- Classes: max 200 lines before considering splitting
- Files: max 300 lines before considering modularisation
- Use cases: one use case per file — no exceptions
- Domain entities: one aggregate per file
These aren’t hard stops — they’re signals that the abstraction isn’t right. A use case growing past 50 lines is probably doing two things. A domain entity past 200 lines probably contains logic that belongs in a separate value object or service.
AGENTS.md rules for size
## Code size
- Functions: max 20 lines — extract if longer
- Classes: max 200 lines — split responsibilities if longer
- Files: max 300 lines — modularise if longer
- If a file needs to grow past these limits, create a new abstraction first
Modularity and bounded contexts
Each bounded context (Orders, Payments, Users, etc.) should be internally cohesive and externally minimal. What crosses context boundaries should be explicit — events, commands, shared value objects.
Signs the modularity is wrong:
- A use case in the Orders context imports directly from the Payments domain
- A controller takes data from two contexts and combines them before returning
- A change in the Payments context requires a change in the Orders context
- A single test needs to set up state across multiple contexts to pass
Enforce module boundaries in CI — the same eslint-plugin-boundaries configuration that enforces layers can enforce context boundaries:
// eslint.config.js
{
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
// Contexts communicate only through defined interfaces
{ from: "orders", allow: ["orders", "shared"] },
{ from: "payments", allow: ["payments", "shared"] },
{ from: "shared", allow: ["shared"] },
]
}]
}
}
Explicit technical debt
Technical debt is normal. Unacknowledged technical debt is the problem. Agents accumulate debt silently — they implement a shortcut without noting it, and two months later someone has to reconstruct why the code is the way it is.
Use a standard comment format to make debt explicit and trackable:
// TODO(tech-debt): This use case grew beyond single responsibility.
// It handles both order creation and inventory reservation.
// Separate into CreateOrderUseCase + ReserveInventoryUseCase
// after the current sprint. Ticket: ORD-142
// FIXME(tech-debt): This retry logic is duplicated from PaymentService.
// Extract to a shared RetryPolicy value object.
// Ticket: INFRA-89
Include in AGENTS.md:
## Technical debt
- When taking a shortcut, add a TODO(tech-debt) comment with:
- What the shortcut is
- What the correct implementation would be
- A ticket reference or why it's deferred
- Do not add shortcuts silently — unacknowledged debt is the most expensive kind
Continuous refactoring
Refactoring isn’t a separate phase — it’s part of every feature. The agent should clean up what it touches. Leave the code better than it found it.
What continuous refactoring looks like in practice:
- Every PR includes at least a pass to eliminate dead code in the touched files
- If a use case grew too large during implementation, split it before marking the task done
- If a naming inconsistency is discovered, fix it with a
refactor:commit - If a pattern appears three times, extract it before implementing the fourth instance
The rule of three:
Once: implement it inline
Twice: note the duplication
Three times: extract the abstraction
Agents tend to copy-paste. Catch it in review. Three copies of the same retry logic is never the right answer.
Naming as documentation
Names are the primary documentation for code. An agent given a vague task will produce vague names. An agent given precise requirements produces precise names.
Review for:
- Functions named after what they do, not how they do it (
processPaymentvschargeStripeCard) - Classes that could be described in one sentence without “and”
- Boolean parameters that force the reader to check the function signature (
sendEmail(user, true)→sendEmailWithConfirmation(user)) - Variables named after their type rather than their role (
userList→pendingApprovals)
AGENTS.md rule:
## Naming
- Classes and functions must be nameable in one sentence without "and"
- If "and" appears in the name, split the responsibility
- No boolean parameters — use named options objects or separate methods
- Variables named after their role, not their type
Maintainability review checklist
Part of the Definition of Done — check these before merging any PR:
## Maintainability Checklist
Size:
- [ ] No function exceeds 20 lines without a clear reason
- [ ] No class exceeds 200 lines — split if needed
- [ ] No file exceeds 300 lines — modularise if needed
Modularity:
- [ ] Context boundaries respected — no cross-context imports outside shared/
- [ ] New abstractions added only when a pattern appears 3+ times
Technical debt:
- [ ] All shortcuts documented with TODO(tech-debt) comments
- [ ] No silent workarounds — if it's wrong, name it
Naming:
- [ ] Every class name is a single responsibility
- [ ] No "and" in class or function names
- [ ] No boolean parameters on public methods
Refactoring:
- [ ] Dead code removed from touched files
- [ ] Duplication eliminated before merging