Architecture Rules
The structural rules that make AI-generated code predictable, maintainable, and coherent — defined once, enforced always.
Without structural rules, an agent takes the path of least resistance. That tends to produce business logic in controllers, dependencies pointing the wrong direction, anemic models with no behaviour, and monolithic classes that do too much. These aren’t AI-specific failures — they’re common in any codebase without constraints. But AI produces them faster, at greater scale, across the entire codebase at once.
The speed multiplier problem
Without architecture rules, the agent doesn’t write bad code slowly — it writes it at 10× the speed of a human. Problematic patterns spread everywhere before anyone notices. Architecture rules are what stops that.
The rules on this page exist for one purpose: to narrow the agent’s solution space until its structural decisions are predictable. They belong in your AGENTS.md, in your design documents, and in your CI pipeline.
File structure
The canonical structure enforced across all Aircury projects:
src/
domain/ # Business logic — no external dependencies
application/ # Use cases and ports (interfaces)
ports/ # Interfaces the domain depends on
infrastructure/ # Adapters implementing ports (DB, HTTP, queues)
adapters/
Every file the agent creates must land in one of these three layers. If a file doesn’t fit, the structure is wrong before a line is written.
The dependency rule
Dependencies always point inward:
infrastructure → application → domain
domain/imports nothing fromapplication/orinfrastructure/application/imports fromdomain/onlyinfrastructure/imports fromapplication/anddomain/
Violating this rule is the most common agent failure. It’s also the easiest to detect — any infrastructure import inside domain/ is a violation.
Layer responsibilities
Domain layer — business logic and invariants only. Entities, value objects, aggregates. No ORMs, no HTTP clients, no framework imports. SOLID applies here strictly.
Application layer — orchestration. Use cases call domain logic and coordinate through Port interfaces. Use cases know what to do; they don’t know how infrastructure does it.
Infrastructure layer — implements ports. Postgres repositories, Stripe adapters, S3 clients. Knows about the outside world so the domain doesn’t have to.
Ports and adapters
All external dependencies (databases, APIs, queues, file systems) are accessed through Port interfaces defined in application/ports/. Infrastructure provides the implementations (Adapters).
// Port — defined in application/ports/, owned by the domain
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
}
// Adapter — defined in infrastructure/adapters/, implements the port
class PostgresOrderRepository implements OrderRepository {
async findById(id: OrderId): Promise<Order | null> { ... }
async save(order: Order): Promise<void> { ... }
}
The agent must never instantiate database clients or HTTP clients directly inside domain/ or application/. All such dependencies are constructor-injected as interfaces.
Language and naming
Use the same terms the business uses. No translation layer between “what the business calls it” and “what the code calls it.” When specs use a term, the code uses the same term. This produces code that stakeholders can read, and specs that map directly to implementations.
// ❌ Generic — says nothing about the business
class Record {
process(data: object): void { ... }
}
// ✓ Ubiquitous language — mirrors business concepts
class InsuranceClaim {
approve(reviewer: Underwriter): void { ... }
reject(reason: RejectionReason): void { ... }
}
Architecture rules for AGENTS.md
Include this block verbatim:
## Architecture Rules
### File structure
- Domain: src/domain/ — business logic, no external imports
- Application: src/application/ — use cases and port interfaces
- Infrastructure: src/infrastructure/ — port implementations
### Dependency direction
- Dependencies always point inward: infrastructure → application → domain
- NEVER import from infrastructure/ or application/ inside domain/
- NEVER import from infrastructure/ inside application/
### Ports and adapters
- All external dependencies (DB, HTTP, queues) go through Port interfaces
- Ports in: src/application/ports/
- Adapters in: src/infrastructure/adapters/
- NEVER instantiate database clients or HTTP clients in domain or application
### Domain rules
- Domain entities must contain business logic, not just getters/setters
- Use constructor injection for all dependencies
- Use the business's language for class and method names
### Naming
- Classes: PascalCase | Interfaces: PascalCase, no I prefix
- Files: kebab-case | Domain folders: kebab-case, plural
- Events: past tense (OrderCreated) | Commands: imperative (CreateOrder)
- Constants: SCREAMING_SNAKE_CASE
Specifying architecture in design documents
Every design document must define:
- Bounded Context — what domain are we in? What are the explicit boundaries?
- Aggregates and their Roots — what are the main domain objects?
- Primary Ports (Use Cases) — what actions can the outside call?
- Secondary Ports — what infrastructure does the domain need? (Repository interfaces, Gateway interfaces)
- Adapter locations — where in the file structure do adapters live?
When the agent reads a design document with this level of clarity, it implements the structure you’ve defined rather than inventing its own. The result is code any team member can navigate immediately.
Enforcement with linting
Turn architecture rules into CI failures with eslint-plugin-boundaries:
// eslint.config.js
{
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{ from: "infrastructure", allow: ["application", "domain"] },
{ from: "application", allow: ["domain"] },
{ from: "domain", allow: [] },
]
}]
}
}
This makes architecture violations impossible to merge silently. The agent can’t break layer boundaries without the CI pipeline catching it.