Architecture as Code: How to Tame AI with Systematic Thinking
๐ง The Ultimate Dilemma of AI Programming: Are You "Using AI" or "Being Used by AI"?
Most people fall into a paradox in AI programming: originally wanting AI to improve efficiency, but ending up spending a lot of time "correcting AI" and "fixing problems generated by AI."
The root cause lies in: lack of systematic architectural thinking. Without good architecture, AI is like a wild horse - capable but uncontrollable.
During Tidepool Notes development, I summarized a set of "Architecture as Code" methodology that transforms AI from an "uncontrollable genius" into an "obedient expert."
๐ฏ Core Cognition: Architecture Before Code, Rules Before Features
Misconceptions in Traditional AI Programming
โ Typical Wrong Workflow:
- Have an idea
- Directly ask AI to write code
- AI generates complex solutions
- Find problems, ask AI to modify
- More and more problems, project out of control
Where's the problem?
- No clear architectural boundaries
- No predefined rules
- No systematic constraint mechanisms
- AI is "freely expressing," deviating from actual needs
Correct Workflow for Architecture as Code
โ Systematic Correct Workflow:
- Define architectural principles and boundaries
- Create rule constraints and templates
- Let AI work within the framework
- Control output quality through architecture
- Continuously optimize the architecture itself
๐๏ธ First Layer: Project-Level Architecture Constraints
1. Mandatory Constraints on Technology Stack
Problem: Why does AI recommend unsuitable technology stacks?
- AI tends to recommend "latest and hottest" technologies
- Lacks understanding of actual project needs
- Overconsiders "possible future expansion needs"
Solution: Lock down technology stack in Claude.md
## Mandatory Technology Stack Constraints (Unchangeable)
### Frontend Technology Stack (Locked)
- Framework: Taro 4.1.7 + React 18 (No Vue, Next.js, etc.)
- Language: TypeScript 5.4.5 (Strict mode, no any types)
- State Management: React Context + useReducer (No Redux, Zustand, etc.)
- Styling: SCSS + CSS Variables (No Tailwind, Styled Components, etc.)
- Charts: ECharts (No Chart.js, D3, etc.)
### Backend Technology Stack (Locked)
- Runtime: Node.js 18+ (No Python, Java, etc.)
- Framework: Fastify (No Express, Koa, etc.)
- Database: PostgreSQL 14+ (No MySQL, MongoDB, etc.)
- ORM: Prisma (No TypeORM, Sequelize, etc.)
- Authentication: JWT (No Session, OAuth, etc.)
### Prohibited Technologies
- โ Any form of microservices architecture
- โ Message queues (Redis Queue, RabbitMQ, etc.)
- โ Complex caching strategies
- โ Multi-database design
Effect: AI can no longer recommend "show-off" solutions, can only solve problems within given framework
2. Mandatory File Structure Specifications
Problem: Why does AI like to create complex file structures?
- Influenced by "best practices" articles
- Tends to over-layer and over-abstract
- Doesn't understand actual needs of small projects
Solution: Define precise file structure templates
## Mandatory File Structure (Unbreakable)
project/
โโโ frontend/
โ โโโ tidepool/
โ โโโ src/
โ โ โโโ app.tsx # App entry, not exceeding 100 lines
โ โ โโโ context/ # Global state, each file not exceeding 200 lines
โ โ โ โโโ AppContext.tsx
โ โ โโโ pages/ # Page components, each file not exceeding 300 lines
โ โ โ โโโ index/
โ โ โ โโโ record/
โ โ โ โโโ chat/
โ โ โ โโโ insights/
โ โ โ โโโ settings/
โ โ โโโ components/ # Common components, each file not exceeding 200 lines
โ โ โ โโโ PageWrapper.tsx
โ โ โ โโโ CustomTabBar.tsx
โ โ โ โโโ SketchIcon.tsx
โ โ โโโ services/ # API services, each file not exceeding 300 lines
โ โ โ โโโ api.ts
โ โ โโโ types/ # Type definitions, each file not exceeding 150 lines
โ โ โโโ index.ts
โ โโโ config/ # Configuration files, unchangeable
โ โโโ apiConfig.js
โโโ backend/
โโโ src/
โโโ routes/ # API routes, each file not exceeding 200 lines
โ โโโ auth.routes.ts
โ โโโ users.routes.ts
โ โโโ sessions.routes.ts
โโโ services/ # Business logic, each file not exceeding 400 lines
โ โโโ auth.service.ts
โ โโโ users.service.ts
โ โโโ llm.service.ts
โโโ repositories/ # Data access, each file not exceeding 300 lines
โ โโโ users.repository.ts
โ โโโ sessions.repository.ts
โโโ types/ # Type definitions, each file not exceeding 150 lines
โโโ users.types.ts
โโโ sessions.types.ts
**Key Constraints:**
- Single file not exceeding 1500 lines (AI context window limit)
- Directory structure unchangeable
- File naming must follow specifications
- Prohibited from creating additional utils, helpers, etc. directories
3. Mandatory Rules for Dependency Management
Problem: Why does AI like to add unnecessary dependencies?
- Tends to use "specialized libraries" to solve simple problems
- Influenced by GitHub stars count
- Doesn't understand dependency maintenance costs
Solution: Establish strict dependency management rules
## Mandatory Dependency Management Rules
### Prohibited Dependency Types
- โ Any form of utility libraries (lodash, date-fns, classnames, etc.)
- โ Any UI component libraries (Ant Design, Material-UI, etc.)
- โ Any form validation libraries (yup, zod, joi, etc.)
- โ Any HTTP client libraries (axios, got, etc., use native fetch)
- โ Any state management libraries (redux, mobx, zustand, etc.)
### Scenarios Requiring Native Implementation
```typescript
// โ Prohibit using lodash
import { map, filter, find } from 'lodash'
// โ
Use native JavaScript
const filtered = users.filter(user => user.age > 18)
const found = users.find(user => user.id === targetId)
// โ Prohibit using date-fns
import { format, addDays } from 'date-fns'
format(new Date(), 'yyyy-MM-dd')
// โ
Use native Date
const formatDate = (date: Date) => {
return date.toISOString().split('T')[0]
}
// โ Prohibit using axios
import axios from 'axios'
const response = await axios.get('/api/users')
// โ
Use native fetch
const response = await fetch('/api/users')
const data = await response.json()
Dependency Decision Tree
Need functionality F?
โโโ Can it be implemented with native JavaScript APIs?
โ โโโ Yes โ Use native implementation
โ โโโ No โ Check if it's a core project requirement
โ โโโ Yes โ Consider adding dependency (needs sufficient reason)
โ โโโ No โ Redesign requirements to avoid dependency
๐งฉ Second Layer: Code-Level Architecture Constraints
1. Mandatory Specifications for Function Design
Problem: Why does AI write complex functions?
- Tends to show "comprehensive consideration"
- Overly handles edge cases
- Likes to use complex design patterns
Solution: Triple Constraints for Function Design
// โ
Function template: Force simplicity
export class TemplateService {
// Constraint 1: Single function not exceeding 50 lines
async create(data: CreateData): Promise<ReturnData> {
// Constraint 2: Only necessary error handling
try {
const result = await repository.create(data)
logger.info(`Entity created: ${result.id}`)
return result
} catch (error) {
logger.exception('Create failed', error)
throw error
}
}
// Constraint 3: Single responsibility, don't do extra things
private validate(data: CreateData): void {
if (!data.requiredField) {
throw new Error('Required field missing')
}
}
}
// โ Complex function that AI might generate
export class ComplexService {
async createComplexEntity(data: ComplexInput): Promise<ComplexResult> {
// Excessive input validation (50 lines)
this.validateInputFormat(data)
this.validateBusinessRules(data)
this.validatePermissions(data)
this.validateDependencies(data)
// Complex retry logic (30 lines)
const result = await this.withRetry(async () => {
return await this.performComplexCreation(data)
}, { retries: 3, backoff: 'exponential' })
// Excessive logging (20 lines)
logger.debug('Starting creation process', { dataId: data.id })
logger.info('Validation passed', { validationResults: this.lastValidation })
logger.info('Creation completed', { resultId: result.id })
// Unnecessary caching handling (15 lines)
await this.updateCache(result)
await this.invalidateRelatedCache(result.relatedEntities)
return result
}
}
2. Unified Standards for Error Handling
Problem: Why does AI design complex error handling?
- Tends to "accommodate all situations"
- Likes to create complex error classification systems
- Overconsiders user experience
Solution: Let it Crash + Unified Error Format
// โ
Unified error handling template
export class StandardService {
async operation(data: InputData): Promise<OutputData> {
try {
const result = await repository.operation(data)
return result
} catch (error) {
// Unified format: log + throw
logger.exception('Operation failed', error)
throw new Error(`Operation failed: ${error.message}`)
}
}
}
// โ Complex error handling that AI might generate
export class ComplexErrorService {
async operation(data: InputData): Promise<OutputData | null> {
try {
const result = await repository.operation(data)
return result
} catch (error) {
// Excessive error classification
if (error instanceof ValidationError) {
return { error: 'VALIDATION_ERROR', details: error.details }
} else if (error instanceof PermissionError) {
return { error: 'PERMISSION_DENIED' }
} else if (error instanceof NetworkError) {
// Complex retry logic
return await this.retryWithBackoff(() => repository.operation(data))
} else {
// Return null instead of throwing error
return null
}
}
}
}
3. Simplicity Principles for Class Design
Problem: Why does AI like over-engineering class structures?
- Influenced by design pattern books
- Tends to "prepare for future expansion"
- Likes to use inheritance and abstraction
Solution: Prohibit over-abstraction, force composition
// โ
Simple composition design
export class UserService {
constructor(
private userRepository: UserRepository,
private logger: Logger
) {}
async create(userData: CreateUserInput): Promise<User> {
const user = await this.userRepository.create(userData)
this.logger.info(`User created: ${user.id}`)
return user
}
}
// โ Complex inheritance structure that AI might generate
abstract class BaseService<T, CreateInput, UpdateInput> {
protected abstract repository: Repository<T, CreateInput, UpdateInput>
protected abstract logger: Logger
protected abstract validator: Validator<T>
async create(data: CreateInput): Promise<T> {
await this.validator.validate(data)
const result = await this.repository.create(data)
this.logger.info(`${this.getEntityName()} created: ${result.id}`)
return result
}
protected abstract getEntityName(): string
}
class UserService extends BaseService<User, CreateUserInput, UpdateInput> {
// Complex template method pattern
}
๐ง Third Layer: Process-Level Architecture Constraints
1. Standardized Development Process
Problem: Why do AI programming projects easily get out of control?
- Lack of clear development rhythm
- No clear completion standards
- Lack of continuous quality control
Solution: Standardized Development Process
## Mandatory Development Process
### Phase 1: Architecture Definition (Cannot be skipped)
1. Create/update Claude.md documentation
2. Define clear requirement boundaries
3. Determine technology stack constraints
4. Create file structure templates
### Phase 2: Skeleton Setup (Must be done first)
1. Create all necessary directory structures
2. Define TypeScript interfaces
3. Create basic CRUD operations
4. Set up basic error handling
### Phase 3: Feature Implementation (Strict order)
1. First implement data layer (repositories)
2. Then implement business layer (services)
3. Finally implement interface layer (routes)
4. Each layer must pass tests after completion
### Phase 4: Quality Check (Must be executed)
1. File line count check (not exceeding 1500 lines)
2. Type check (noImplicitAny: true)
3. Code style check (ESLint)
4. Unit test coverage check
### Daily Quality Gates
- [ ] All files not exceeding 1500 lines
- [ ] No any types used
- [ ] All errors correctly thrown
- [ ] Dependencies comply with constraint rules
- [ ] Functions have single responsibility
2. Mandatory Requirements for Code Review
Problem: How to ensure AI-generated code meets architectural requirements?
## Mandatory Code Review Checklist
### Architecture Consistency Check
- [ ] File structure complies with specifications
- [ ] Dependency relationships are correct
- [ ] Interface design is consistent
- [ ] Error handling is unified
### Quality Standard Check
- [ ] Function length not exceeding 50 lines
- [ ] Class has single responsibility
- [ ] No unnecessary complexity
- [ ] Follows KISS principle
### Constraint Rule Check
- [ ] No prohibited dependencies used
- [ ] No any types used
- [ ] Errors correctly thrown
- [ ] Log format is unified
### Performance and Security Check
- [ ] No unnecessary database queries
- [ ] Input validation is sufficient
- [ ] Sensitive information properly handled
- [ ] API responses are reasonable
3. Automated Mechanism for Refactoring
Solution: Automated Architecture Guardian
// scripts/archural-guard.ts
import fs from 'fs'
import path from 'path'
interface ArchitecturalRule {
name: string
check: (filePath: string, content: string) => boolean
message: string
}
const rules: ArchitecturalRule[] = [
{
name: 'file-size-limit',
check: (filePath, content) => {
const lines = content.split('\n').length
return lines <= 1500
},
message: 'File exceeds 1500 lines limit'
},
{
name: 'no-any-types',
check: (filePath, content) => {
return !content.includes(': any') && !content.includes('as any')
},
message: 'Found usage of any type'
},
{
name: 'required-imports',
check: (filePath, content) => {
if (filePath.endsWith('.ts')) {
return content.includes('import traceback')
}
return true
},
message: 'Missing import traceback statement'
},
{
name: 'error-handling',
check: (filePath, content) => {
if (filePath.includes('service')) {
return content.includes('logger.exception')
}
return true
},
message: 'Service files must use logger.exception for error handling'
}
]
function checkArchitecture() {
const srcDir = './src'
const files = getAllTypeScriptFiles(srcDir)
let hasViolations = false
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8')
rules.forEach(rule => {
if (!rule.check(file, content)) {
console.error(`โ [${rule.name}] ${file}: ${rule.message}`)
hasViolations = true
}
})
})
if (hasViolations) {
console.error('\nโ Architecture check failed!')
process.exit(1)
} else {
console.log('โ
Architecture check passed!')
}
}
checkArchitecture()
๐ฏ Practical Cases: Architecture Evolution in Tidepool Notes
Case 1: Message System Architecture Refactoring
Initial State (Result of AI Free Expression):
// Complex message system generated by AI
class MessageManager {
// Support multiple message types
async createTextMessage(data: TextMessageData): Promise<TextMessage>
async createImageMessage(data: ImageMessageData): Promise<ImageMessage>
async createVoiceMessage(data: VoiceMessageData): Promise<VoiceMessage>
async createVideoMessage(data: VideoMessageData): Promise<VideoMessage>
// Complex message processing flow
async processMessage(message: Message): Promise<ProcessedMessage> {
await this.validateMessage(message)
await this.transformMessage(message)
await this.enrichMessage(message)
await this.routeMessage(message)
await this.indexMessage(message)
await this.cacheMessage(message)
return this.formatMessage(message)
}
// Excessive error handling
async handleError(error: Error, context: ErrorContext): Promise<ErrorResult> {
// 50 lines of complex error classification and handling logic
}
}
Simplification after Applying Architecture Constraints:
// Constrained message system
export class MessageService {
constructor(
private messageRepository: MessageRepository,
private logger: Logger
) {}
async create(data: CreateMessageData): Promise<Message> {
try {
const message = await this.messageRepository.create(data)
this.logger.info(`Message created: ${message.id}`)
return message
} catch (error) {
this.logger.exception('Failed to create message', error)
throw error
}
}
async findBySessionId(sessionId: string): Promise<Message[]> {
return await this.messageRepository.findBySessionId(sessionId)
}
}
Effects of Architecture Constraints:
- Code lines reduced from 800 to 150 lines
- Maintenance cost reduced by 70%
- AI generation consistency improved by 90%
- New feature development speed increased by 50%
Case 2: Standardization of AI Integration
AI Free Expression's Complex Solution:
// AI tends to over-engineer LLM integration
class LLMOrchestrator {
constructor() {
this.promptManager = new ComplexPromptManager()
this.conversationManager = new ConversationStateManager()
this.responseParser = new MultiFormatResponseParser()
this.errorHandler = new RetryWithBackoffHandler()
this.cacheManager = new MultiLevelCacheManager()
}
async generateResponse(input: UserInput): Promise<AIResponse> {
// Complex multi-layer processing logic
const context = await this.conversationManager.getContext(input)
const prompt = await this.promptManager.buildPrompt(context, input)
const rawResponse = await this.callLLMWithRetry(prompt)
const parsedResponse = await this.responseParser.parse(rawResponse)
const enrichedResponse = await this.enrichResponse(parsedResponse)
await this.cacheManager.store(input, enrichedResponse)
return enrichedResponse
}
}
Standardization after Architecture Constraints:
// Standardized LLM integration
export class LLMService {
constructor(
private openaiClient: OpenAIClient,
private logger: Logger
) {}
async generateSuggestion(sessionData: SessionData): Promise<string> {
try {
const prompt = this.buildPrompt(sessionData)
const response = await this.openaiClient.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1000
})
return response.choices[0].message.content
} catch (error) {
this.logger.exception('Failed to generate suggestion', error)
throw error
}
}
private buildPrompt(sessionData: SessionData): string {
return `Based on user session data, provide emotional suggestions: ${JSON.stringify(sessionData)}`
}
}
Case 3: Automation of Architecture Guardian
Implemented Automated Checks:
// Automatically executed before each commit
{
"scripts": {
"pre-commit": "npm run type-check && npm run lint && npm run arch-check",
"arch-check": "ts-node scripts/architectural-guard.ts",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext .ts"
}
}
// Architecture checks in CI/CD pipeline
name: Architecture Check
on: [push, pull_request]
jobs:
check-architecture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install
- run: npm run arch-check
- run: npm run test
๐ Summary: Core Principles of Architecture as Code
Three Levels of Architectural Thinking
1. Project-Level Architecture (Top-Level Design)
- Clear technology stack constraints
- Standardized file structure
- Strict dependency management rules
- Unified development process
2. Code-Level Architecture (Middle-Level Control)
- Triple constraints for function design
- Unified standards for error handling
- Simplicity principles for class design
- Consistency of interface design
3. Process-Level Architecture (Bottom-Level Guarantee)
- Standardized development process
- Mandatory code reviews
- Automated architecture guardians
- Continuous quality control
Steps to Implement Architecture as Code
First Step: Establish Architecture Documentation
- Create detailed Claude.md
- Define clear constraint rules
- Provide specific code templates
- Create quality check checklists
Second Step: Implement Automated Guardians
- Create architecture check scripts
- Integrate into CI/CD processes
- Set up code quality gates
- Establish continuous monitoring mechanisms
Third Step: Continuously Optimize Architecture
- Regularly review architecture effectiveness
- Adjust rules based on actual problems
- Optimize automated check mechanisms
- Share architectural practice experiences
Key Success Factors
1. Clarity of Constraints
- Every rule must be specific and executable
- Provide positive and negative examples
- Regularly check rule effectiveness
- Update outdated constraints promptly
2. Automation of Tools
- Reduce reliance on manual checks
- Integrate into development workflow
- Provide clear error feedback
- Ensure fast execution of checks
3. Sustainability of Culture
- Team consensus on architectural constraints
- Continuous architectural thinking training
- Regular architecture review meetings
- Open problem feedback mechanisms
Remember: The core of AI programming is not "what to let AI do," but "what not to let AI do." Good architecture is to draw clear boundaries for AI, allowing it to create value within controllable ranges.
Next Article Preview: Prompt Engineering as Architecture: How to Make AI Understand Your Real Needs
Search "ๅๅฅ่ฟไผ่AI" on WeChat to see how these architecture principles create value in actual projects