Chapter 12: Constraint Types and Architecture Constraints
Learning Objectives
By the end of this chapter, you will be able to:
- Define the five constraint categories (Architecture, Security, Performance, Data, Compliance) and explain when each applies
- Articulate why constraints exist: protecting against AI over-generation and implicit architectural decisions
- Write architecture constraints with concrete examples: controllers without business logic, stateless services, repository-only database access, project count limits
- Apply layered architecture constraints and dependency direction rules
- Express constraints in specification documents using the constraint document format
- Define architecture constraints for a running project through a hands-on tutorial
Why Constraints Exist
Specifications tell AI what to build. Constraints tell AI what not to build—even when the specification might seem to permit it.
Consider a real-world analogy: a building specification might say "construct a residential tower with 50 units." Without constraints, a contractor might interpret this as permission to use experimental materials, deviate from fire codes, or add a penthouse "because it would be nice." Building codes—the constraints—say: "Regardless of the specification, you must never do X, Y, or Z." The specification defines the desired outcome. The constraints define the inviolable boundaries.
In software, AI agents exhibit predictable failure modes when left unconstrained:
Over-generation: AI tends to add more than requested. Ask for "user registration" and you get user registration plus a generic repository pattern, plus a service layer abstraction, plus DTOs and mappers—none of which you asked for. Constraints like "maximum 3 projects for initial implementation" or "no speculative abstractions" prevent this.
Implicit architectural decisions: AI makes choices the specification doesn't address. Should database access go through repositories? Should controllers be thin? The AI guesses—and different sessions guess differently. Constraints make these decisions explicit and consistent.
Speculative features: "While I'm here, I'll add pagination" or "I'll include a caching layer for future scalability." Constraints like "no future-proofing" or "no features without acceptance criteria" prevent scope creep.
Framework violations: AI might wrap framework features in custom abstractions ("we might switch frameworks someday") or ignore framework conventions. Constraints like "use framework directly" or "follow framework conventions" prevent this.
Constraints transform implicit assumptions into explicit, enforceable rules. They ensure that every AI-generated implementation—whether from you, a teammate, or a different model—respects the same boundaries.
The Five Constraint Categories
Constraints fall into five categories. Each addresses a different dimension of system integrity:
| Category | Purpose | Example |
|---|---|---|
| Architecture | Govern structure, layering, and dependencies | Controllers cannot contain business logic |
| Security | Protect against vulnerabilities and misuse | All endpoints require authentication by default |
| Performance | Enforce latency, throughput, and resource limits | API p95 latency < 300ms |
| Data | Govern schema, storage, and data handling | PII encrypted at rest |
| Compliance | Enforce regulatory and organizational rules | GDPR: data retention limits, right to deletion |
This chapter focuses on Architecture constraints. Chapters 13 and 14 address Security and Performance in depth, and the constitutional foundation that ties all constraints together.
Architecture Constraints: The Structural Guardrails
Architecture constraints define how the system is organized. They answer: What layers exist? What may depend on what? What patterns are mandatory or forbidden?
Real-World Analogy: The Kitchen Layout
Imagine a restaurant kitchen. The specification might say "serve 200 meals per night." But the constraints govern the layout: the grill station cannot access the walk-in cooler directly (food safety); prep must happen before service (workflow); the dishwasher must be near the dish drop (efficiency). These constraints ensure that regardless of which chef works the line, the kitchen operates correctly. Architecture constraints do the same for software: they ensure that regardless of which AI generates the code, the structure remains coherent.
Constraint 1: Controllers Cannot Contain Business Logic
The rule: Controllers (or API handlers, route handlers, etc.) may only orchestrate requests and responses. They must not contain business rules, calculations, or domain logic.
Why it matters: AI often generates "fat controllers"—endpoints that validate input, apply business rules, query the database, and format responses all in one place. This creates untestable code, violates single responsibility, and makes refactoring dangerous. The constraint forces a clear boundary: controllers delegate to services.
Bad (AI without constraint):
// UserController.ts — VIOLATION: business logic in controller
app.post('/api/users', async (req, res) => {
const { email, password } = req.body;
// Business logic in controller — FORBIDDEN
if (password.length < 12) {
return res.status(400).json({ error: 'Password too short' });
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await db.users.create({ email, password: hashedPassword });
const token = jwt.sign({ userId: user.id }, process.env.SECRET);
return res.json({ user, token });
});
Good (with constraint):
// UserController.ts — CORRECT: orchestration only
app.post('/api/users', async (req, res) => {
try {
const result = await userService.register(req.body);
return res.status(201).json(result);
} catch (err) {
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message });
}
throw err;
}
});
// UserService.ts — business logic lives here
export async function register(input: RegisterInput) {
if (input.password.length < 12) {
throw new ValidationError('Password must be at least 12 characters');
}
const hashedPassword = await bcrypt.hash(input.password, 12);
const user = await userRepository.create({ email: input.email, password: hashedPassword });
const token = authService.createToken(user.id);
return { user, token };
}
Expressing the constraint:
## Architecture Constraint: Thin Controllers
- Controllers MUST NOT contain business logic, validation rules, or domain calculations
- Controllers MAY: parse request, call service, format response, handle HTTP status
- All business logic MUST reside in services or domain modules
- Violation: Build fails; linter rule `no-business-logic-in-controllers`
Constraint 2: Services Must Be Stateless
The rule: Services do not maintain request-scoped or session-scoped state. Every method receives all required data as parameters and returns results. No instance variables that change between requests.
Why it matters: Stateless services are horizontally scalable, easier to test, and avoid subtle concurrency bugs. AI sometimes generates services that cache results, hold request context, or use mutable instance state—especially when "optimizing" for performance. The constraint prevents this.
Bad:
// VIOLATION: stateful service
class SearchService {
private cache: Map<string, SearchResult> = new Map(); // instance state
async search(query: string): Promise<SearchResult> {
if (this.cache.has(query)) return this.cache.get(query)!;
const result = await this.doSearch(query);
this.cache.set(query, result); // mutable state
return result;
}
}
Good:
// CORRECT: stateless — cache is external, injected
class SearchService {
constructor(private readonly cache: Cache, private readonly searchClient: SearchClient) {}
async search(query: string): Promise<SearchResult> {
const cached = await this.cache.get(query);
if (cached) return cached;
const result = await this.searchClient.search(query);
await this.cache.set(query, result);
return result;
}
}
Expressing the constraint:
## Architecture Constraint: Stateless Services
- Services MUST NOT maintain mutable instance state between requests
- All request-scoped data MUST be passed as method parameters
- Caching, if used, MUST be via injected external store (Redis, in-memory cache service)
- Violation: Code review rejection; static analysis flag
Constraint 3: Database Access Only Through Repositories
The rule: No direct database access from services, controllers, or domain logic. All data access goes through repository interfaces. Repositories encapsulate SQL, ORM usage, and data mapping.
Why it matters: Direct database access scattered across the codebase creates tight coupling, makes testing difficult (you must mock the database), and prevents swapping storage implementations. AI often generates services that import the ORM directly and run queries inline. The constraint enforces a clean data access boundary.
Bad:
// VIOLATION: service directly using database
import { db } from './database';
export async function getOrder(id: string) {
const order = await db.query('SELECT * FROM orders WHERE id = $1', [id]);
const items = await db.query('SELECT * FROM order_items WHERE order_id = $1', [id]);
return { ...order, items };
}
Good:
// CORRECT: repository encapsulates data access
// OrderService.ts
export async function getOrder(id: string) {
return orderRepository.findById(id);
}
// OrderRepository.ts
export async function findById(id: string): Promise<Order> {
const order = await db.query('SELECT * FROM orders WHERE id = $1', [id]);
const items = await db.query('SELECT * FROM order_items WHERE order_id = $1', [id]);
return mapToOrder(order, items);
}
Expressing the constraint:
## Architecture Constraint: Repository-Only Data Access
- Services, controllers, and domain logic MUST NOT import database clients, ORMs, or execute raw queries
- All data access MUST go through repository interfaces in the `repositories/` layer
- Repositories MAY use ORM, raw SQL, or external APIs
- Violation: Import analysis; build fails on direct db/orm imports outside repositories
Constraint 4: Maximum 3 Projects for Initial Implementation
The rule: The initial implementation of a feature or system must not exceed three projects (or modules, packages, depending on your ecosystem). Additional projects require documented justification and approval.
Why it matters: AI tends to over-modularize. It creates core, domain, application, infrastructure, api, shared, common—each a separate project. This creates unnecessary complexity, slow builds, and circular dependency risks. The constraint forces simplicity: start with the minimum structure that works.
Example project structure (3 projects):
my-app/
├── src/ # Project 1: Application + API
├── lib/ # Project 2: Shared library (if needed)
└── tests/ # Project 3: Test project
Justification for a 4th project (must be documented):
## Complexity Tracking: Additional Project Justification
**Request**: Add `packages/contracts` as 4th project
**Rationale**: API contracts (OpenAPI, Protobuf) are shared between frontend and backend.
Keeping them in a separate package enables:
- Independent versioning for contract consumers
- Contract-first development without circular dependencies
**Approved by**: [Architecture review]
Expressing the constraint:
## Architecture Constraint: Project Count Limit
- Initial implementation MUST use ≤3 projects
- Each additional project REQUIRES:
- Documented rationale in `COMPLEXITY.md`
- Explicit approval from project maintainer
- "Project" = buildable unit (npm package, C# project, Go module, etc.)
- Violation: CI check fails if project count exceeds limit without approval
Layered Architecture Constraints
Layered architecture constrains direction of dependencies. The classic layers:
┌─────────────────────────────────────┐
│ Presentation (Controllers, API) │ ← Top
├─────────────────────────────────────┤
│ Application (Services, Use Cases) │
├─────────────────────────────────────┤
│ Domain (Entities, Business Rules) │
├── ───────────────────────────────────┤
│ Infrastructure (Repos, DB, External)│ ← Bottom
└─────────────────────────────────────┘
The rule: Dependencies flow downward only. Presentation may depend on Application. Application may depend on Domain and Infrastructure. Domain must not depend on anything. Infrastructure implements interfaces defined by Domain or Application.
Expressing layered constraints:
## Architecture Constraint: Layered Dependencies
- Layer dependency direction: Presentation → Application → Domain ← Infrastructure
- Domain layer MUST have zero outgoing dependencies (no imports from other layers)
- Infrastructure MUST implement interfaces defined in Application or Domain
- Presentation MUST NOT import from Infrastructure directly
- Violation: Dependency analysis tool (e.g., ArchUnit, dependency-cruiser) fails build
Dependency Direction Constraints
Beyond layers, you may constrain which modules can depend on which:
## Architecture Constraint: Dependency Rules
- `api/` may import from `services/` and `repositories/` (through interfaces)
- `api/` MUST NOT import from `database/` or `config/`
- `services/` may import from `repositories/` and `domain/`
- `services/` MUST NOT import from `api/`
- `repositories/` may import from `database/` and `domain/`
Tools like dependency-cruiser (JavaScript), ArchUnit (Java), or NetArchTest (C#) can enforce these rules in CI.
The Constraint Document Format
Constraints should be expressed in a consistent, machine-parseable format. This enables both human review and automated enforcement.
Standard Structure
# [Project Name] Constraints
## Metadata
- Version: 1.0
- Last Updated: 2026-03-09
- Scope: [global | feature-name | module-name]
## Architecture Constraints
### AC-001: Thin Controllers
- **Rule**: Controllers MUST NOT contain business logic
- **Rationale**: Ensures testability and single responsibility
- **Enforcement**: Linter rule `no-business-logic-in-controllers`
- **Violation**: Build failure
### AC-002: Stateless Services
- **Rule**: Services MUST NOT maintain mutable instance state
- **Rationale**: Enables horizontal scaling and predictable behavior
- **Enforcement**: Code review checklist; static analysis
- **Violation**: PR rejection
### AC-003: Repository-Only Data Access
- **Rule**: Database access ONLY through repository layer
- **Rationale**: Decouples business logic from storage implementation
- **Enforcement**: Import analysis in CI
- **Violation**: Build failure
### AC-004: Project Count Limit
- **Rule**: ≤3 projects for initial implementation
- **Rationale**: Prevents over-engineering and premature abstraction
- **Enforcement**: CI script counts projects; checks COMPLEXITY.md for exceptions
- **Violation**: Build failure without documented exception
## Constraint Index
| ID | Category | Rule Summary | Enforcement |
|----|----------|--------------|-------------|
| AC-001 | Architecture | Thin controllers | Linter |
| AC-002 | Architecture | Stateless services | Review |
| AC-003 | Architecture | Repository-only DB | CI |
| AC-004 | Architecture | Project limit | CI |
Placement in Repository
repo/
├── specs/
│ ├── global/
│ │ └── constraints.md # Global constraints
│ ├── constraints/
│ │ ├── architecture.md # Architecture constraints (detailed)
│ │ ├── security.md # Security constraints
│ │ └── performance.md # Performance constraints
│ └── features/
│ └── 001-user-auth/
│ └── constraints.md # Feature-specific constraints (overrides/additions)
How to Express Constraints in Specification Documents
Constraints can appear in three places:
1. Global Constraint Document
The canonical source: specs/global/constraints.md or specs/constraints/architecture.md. Loaded by AI at the start of every implementation session. Referenced in project rules (.cursor/rules/).
2. Feature Specification
Include a constraints section in each feature spec when the feature has specific requirements:
## Feature: User Registration
### Functional Requirements
...
### Constraints (Feature-Specific)
- This feature MUST use the existing UserRepository interface
- Registration endpoint MUST NOT expose internal user IDs in error messages
- Password validation rules: see AC-SEC-002 in global constraints
3. Implementation Plan
The implementation plan should reference constraints explicitly:
## Phase -1: Pre-Implementation Gates
### Architecture Compliance
- [ ] AC-001: Controller will delegate to UserService only
- [ ] AC-002: UserService will be stateless
- [ ] AC-003: Data access via UserRepository
- [ ] AC-004: No new projects (staying within existing 3)
Tutorial: Define Architecture Constraints for the Running Project
This tutorial assumes you have a running project—a collaboration platform, API, or similar. If not, use a hypothetical "Task Management API" as the target.
Step 1: Inventory Current Structure
List your current layers, modules, and dependencies:
Current structure:
- api/ (Express routes)
- services/ (business logic)
- models/ (data models)
- db/ (database client, migrations)
Identify violations of the constraints we've discussed:
- Do controllers contain business logic?
- Are services stateless?
- Do services access the database directly?
- How many projects/packages exist?
Step 2: Draft Architecture Constraints
Create specs/constraints/architecture.md:
# Architecture Constraints
## AC-001: Thin Controllers
Controllers MUST NOT contain business logic. They orchestrate only.
## AC-002: Stateless Services
Services MUST NOT maintain mutable instance state.
## AC-003: Repository-Only Data Access
All database access MUST go through repositories. No direct db/ imports in services or api.
## AC-004: Project Limit
Maximum 3 projects. Current: 1 (monolith). No new projects without COMPLEXITY.md justification.
## AC-005: Dependency Direction
- api/ → services/ (allowed)
- api/ → db/ (FORBIDDEN)
- services/ → repositories/ (allowed)
- services/ → db/ (FORBIDDEN)
Step 3: Add Enforcement Mechanisms
For AC-001 (thin controllers), add a simple rule or checklist. For AC-003, add a CI script:
# scripts/check-constraints.sh
# Fail if services or api import from db directly
if grep -r "from ['\"].*db" src/services src/api --include="*.ts"; then
echo "VIOLATION: Direct db import in services or api. Use repositories."
exit 1
fi
Step 4: Integrate with AI Context
Add to .cursor/rules/constraints.mdc:
---
description: Architecture constraints - always apply
globs: ["src/**"]
---
When implementing features, you MUST comply with specs/constraints/architecture.md:
- Controllers: orchestration only, no business logic
- Services: stateless
- Data access: repositories only
- Project count: ≤3
Before generating code, verify your plan passes all architecture constraints.
Step 5: Validate
Ask AI to implement a small feature (e.g., "add GET /api/users/:id"). Review the output against your constraints. Did the AI respect them? If not, refine your constraint document and rules.
Constraint Anti-Patterns: What to Avoid
When writing architecture constraints, avoid these common mistakes:
Anti-Pattern 1: Vague Constraints
Bad: "Keep the code clean." Good: "Controllers MUST NOT exceed 50 lines and MUST NOT contain business logic."
Vague constraints cannot be enforced. AI cannot comply with "clean" because it has no measurable definition. Every constraint should be verifiable: a linter, script, or human reviewer can determine compliance.
Anti-Pattern 2: Constraints Without Rationale
Bad: "Use repositories." (Why?) Good: "All database access MUST go through repositories. Rationale: Decouples business logic from storage; enables testing with mocks; allows swapping implementations."
Rationale helps AI understand why the constraint exists. When the AI encounters an edge case, it can reason about the spirit of the rule rather than blindly following or breaking it.
Anti-Pattern 3: Unenforceable Constraints
Bad: "Code should be maintainable." (No automated check exists.) Good: "Functions MUST NOT exceed 30 lines. Enforcement: ESLint max-lines-per-function."
If you cannot enforce a constraint, it will be violated. Prefer constraints that map to linters, static analysis, or CI scripts.
Anti-Pattern 4: Conflicting Constraints
Bad: "Use the framework's built-in validation" AND "All validation must go through a custom ValidationService." Good: Choose one. Document the decision.
Conflicting constraints confuse AI and humans. Resolve conflicts explicitly.
Anti-Pattern 5: Constraint Proliferation
Bad: 50 architecture constraints for a 3-person project. Good: Start with 5–7 high-impact constraints. Add more only when violations occur.
Too many constraints create noise. Focus on the constraints that prevent the failures you actually see.
Integrating Constraints with Implementation Plans
The implementation plan template (from Part IV and the Spec Kit workflow) should include a Pre-Implementation Gates section that checks constraints before any code is generated:
## Phase -1: Pre-Implementation Gates
### Architecture Compliance Checklist
Before generating implementation code, verify:
- [ ] **AC-001**: Controller design delegates all business logic to services
- [ ] **AC-002**: No stateful services; all data passed as parameters
- [ ] **AC-003**: Data access design uses repository pattern only
- [ ] **AC-004**: No new projects; staying within existing structure
- [ ] **AC-005**: Dependency direction respected (no api → db imports)
### Complexity Tracking
If any gate cannot be passed, document here:
| Gate | Status | Justification |
|------|--------|---------------|
| AC-004 | Exception | Adding contracts package for OpenAPI sharing |
This gates section forces the AI (or human) to explicitly confirm constraint compliance before proceeding. It creates a checkpoint: "Have I violated any constraints?"—before the first line of code is written.
Constraint Evolution: When to Change Constraints
Constraints are not immutable. They should evolve when:
-
The constraint causes more harm than good: e.g., "maximum 3 projects" blocks a legitimate need for a shared contracts package. Document the exception, or amend the constraint.
-
Technology changes: A new framework might make a constraint obsolete. "Use framework X's validation" may need updating when you migrate to framework Y.
-
Team learning: The team has internalized the constraint; it's now "obvious." You might relax it or remove it—but document why.
-
New failure modes emerge: A new type of violation appears (e.g., AI starts generating circular dependencies). Add a constraint to prevent it.
Amendment process: When changing a constraint, document:
- What changed
- Why
- When (date)
- Who approved
This creates an audit trail and prevents silent drift.
Try With AI
Prompt 1: Constraint Discovery
"I'm defining architecture constraints for my project. Analyze my codebase structure: [paste or describe your src/ layout]. Identify where we might have violations of (1) thin controllers, (2) stateless services, (3) repository-only data access. List specific files and line ranges that violate these principles, and suggest how to fix them."
Prompt 2: Constraint Document Generation
"Using the constraint document format from this chapter, generate a complete architecture constraints document for a [describe your project: e.g., REST API for X, React SPA for Y]. Include at least 5 architecture constraints with rule, rationale, enforcement mechanism, and violation consequence. Make them specific to my stack: [e.g., Node/Express, Python/FastAPI]."
Prompt 3: Violation Audit
"I have this architecture constraint: 'Controllers cannot contain business logic.' Here's my controller code: [paste code]. Does it violate the constraint? If yes, extract the business logic and show me the refactored controller and the new service where the logic should live."
Prompt 4: Dependency Analysis
"Map the dependency graph of my project: [list main directories like api/, services/, db/]. For each module, list what it imports from. Then check: does any high-level module (api, services) import from low-level modules (db, config) directly? If so, suggest the dependency direction constraints we should add and how to refactor to comply."
Practice Exercises
Exercise 1: Constraint Extraction from Legacy Code
Take an existing codebase (or a sample from GitHub) with fat controllers. Extract 5 architecture constraints that, if applied from the start, would have prevented the current structure. Write them in the constraint document format. For each, propose an enforcement mechanism (linter, CI script, or review checklist).
Expected outcome: A constraints document with 5 rules, each with rationale and enforcement strategy.
Exercise 2: Layered Architecture Design
Design a 4-layer architecture (Presentation, Application, Domain, Infrastructure) for a "Book Library API" with endpoints: list books, add book, borrow book, return book. Write dependency direction constraints: which layer may import from which? Draw the dependency graph. Add a constraint that Domain has zero outgoing dependencies. Explain how you would enforce this with a tool.
Expected outcome: A dependency graph and a constraints section that would prevent circular or inverted dependencies.
Exercise 3: AI Constraint Compliance Test
Use AI to generate a "user profile update" endpoint. Give it only a brief spec: "PATCH /api/users/:id updates name and email." Do NOT give it your constraints. Then, review the generated code against these constraints: thin controller, stateless service, repository-only DB access. Document every violation. Next, give the same request to AI WITH the constraints in context. Compare the two outputs. How much did the constraints improve the result?
Expected outcome: A before/after comparison showing that constrained generation produces more compliant code.
Key Takeaways
-
Constraints define what must never happen—they complement specifications that define what should happen. Without constraints, AI over-generates and makes implicit architectural decisions.
-
Five constraint categories exist: Architecture, Security, Performance, Data, Compliance. Architecture constraints govern structure, layering, and dependencies.
-
Four essential architecture constraints: thin controllers (no business logic), stateless services, repository-only database access, and project count limits (≤3 for initial implementation).
-
Layered architecture constraints enforce dependency direction: Presentation → Application → Domain ← Infrastructure. Domain has zero outgoing dependencies.
-
The constraint document format includes rule, rationale, enforcement mechanism, and violation consequence. Use consistent IDs (AC-001, etc.) for traceability.
-
Constraints belong in three places: global constraint documents, feature specifications (when feature-specific), and implementation plan gates. Integrate with AI context via project rules.
Chapter Quiz
-
What is the difference between a specification and a constraint? Give an example of each for "user registration."
-
Why do AI agents tend to over-generate when left unconstrained? Name three failure modes.
-
Write the "thin controller" constraint in one sentence. What enforcement mechanism would you use?
-
Why must services be stateless? What problems does stateful services cause?
-
Explain the "repository-only data access" constraint. Why not allow services to query the database directly?
-
What is the purpose of the "maximum 3 projects" constraint? When might you justify a 4th project?
-
Draw the dependency direction for a 4-layer architecture. Which layer must have zero outgoing dependencies?
-
Where should constraints appear in an SDD repository? How do you ensure AI loads them during implementation?
Appendix: Constraint Document Template
Use this template when creating new constraint documents:
# [Category] Constraints — [Project Name]
## Metadata
- Version: 1.0
- Last Updated: YYYY-MM-DD
- Scope: [global | feature-name]
- Owner: [team or person]
## Constraint Index
| ID | Rule Summary | Enforcement | Violation |
|----|--------------|--------------|-----------|
| XXX-001 | ... | ... | ... |
## Detailed Constraints
### XXX-001: [Constraint Name]
- **Rule**: [One-sentence rule]
- **Rationale**: [Why this constraint exists]
- **Enforcement**: [How we verify compliance]
- **Violation**: [What happens when violated]
- **Examples**: [Good/bad code snippets if helpful]
## Exceptions
| Constraint | Exception | Justification | Approved |
|------------|-----------|---------------|----------|
| (none) | | | |
## Changelog
| Date | Change |
|------|--------|
| YYYY-MM-DD | Initial version |
Cross-References: Constraints and Other Artifacts
Constraints interact with other SDD artifacts:
| Artifact | Relationship to Constraints |
|---|---|
| Specification | Spec may reference constraints; constraints limit what spec can require |
| Implementation Plan | Plan must pass constraint gates; documents compliance |
| Tests | Tests may verify constraint compliance (e.g., no N+1) |
| Constitution | Constitution governs constraint philosophy; constraints operationalize it |
| CI/CD | Pipeline enforces constraints as gates |
When updating a constraint, consider impact on: existing specs, implementation plans, tests, and CI configuration.
Tools for Architecture Constraint Enforcement
Several tools can automate architecture constraint enforcement:
| Tool | Language/Ecosystem | What It Enforces |
|---|---|---|
| dependency-cruiser | JavaScript/TypeScript | Dependency direction, circular deps |
| ArchUnit | Java | Layer rules, naming, dependency direction |
| NetArchTest | C# | Architecture rules, dependency constraints |
| Sculptor | .NET | Bounded context, dependency rules |
| Custom grep/scripts | Any | Pattern-based (e.g., no db import in services) |
Example: dependency-cruiser (Node.js):
// .dependency-cruiser.json
{
"forbidden": [
{
"name": "no-db-in-api",
"from": { "path": "src/api" },
"to": { "path": "src/db" }
},
{
"name": "no-db-in-services",
"from": { "path": "src/services" },
"to": { "path": "src/db" }
}
]
}
Run with: npx dependency-cruiser src/. Build fails if violations exist.
Constraint Naming Conventions
Use consistent IDs for traceability and automation:
- Architecture:
AC-001,AC-002, ... - Security:
SEC-001,SEC-002, ... - Performance:
PERF-001,PERF-002, ... - Data:
DATA-001,DATA-002, ... - Compliance:
COMP-001,COMP-002, ...
When referencing constraints in specs or plans, use the ID: "This design complies with AC-001, AC-002, AC-003." This enables grep-based audits and automated compliance reporting.
Checklist: Architecture Constraint Readiness
Before considering your architecture constraints complete, verify:
- Each constraint has a unique ID
- Each constraint has rationale and enforcement mechanism
- Violation consequence is defined (build fail, PR rejection, etc.)
- Constraints are integrated with AI context (rules, templates)
- At least one constraint has automated enforcement (CI, linter)
- Constraint document is versioned and referenced in specs
- Team has reviewed and agreed on constraints
From Constraints to Culture
Architecture constraints are most effective when they become team culture. New members learn them during onboarding. Code reviews reference them. When someone asks "why can't we put business logic in the controller?" the answer is "AC-001—it's in our constraints."
Over time, constraints that are consistently enforced become internalized. The team stops violating them because the rules feel natural. That's the goal: constraints that protect the system until the protection becomes second nature.