Mastering the Explore, Plan, Execute methodology for AI-assisted development

Mastering the Explore, Plan, Execute methodology for AI-assisted development

October 3, 2025· Guillaume Moigneu
Guillaume Moigneu
·Reading time: 34 minutes

Working with AI coding assistants in 2025 feels like managing an exceptionally eager, overly confident junior developer who types at superhuman speed (thank you Paul for the metaphor). They’ll confidently write thousands of lines of code before you realize they’ve misunderstood the assignment. The difference between frustration and productivity lies not in the AI’s capabilities, but in how you guide it.

After months of refining workflows with Claude Code and other AI assistants, one methodology has consistently transformed these tools from unpredictable helpers into reliable development partners for my needs: Explore, Plan, Execute. This article breaks down each phase with practical examples, showing you how to build the right context, leverage advanced features, and maintain code quality throughout the process.

The fundamental challenge: Context vs. prompts

Most developers confuse prompts with context. A prompt tells the AI what to do right now. Context teaches it who you are, what your project does, and how you work. Without proper context, even the best prompts produce mediocre results.

Think of it this way: Would you hire a developer, point them at your codebase, and immediately ask them to implement a payment system? Of course not. You’d onboard them, explain the architecture, share coding standards, and review similar implementations. AI assistants need the same foundation.

Building your knowledge foundation

Before any coding session, your AI assistant needs two types of knowledge: project-specific information and team conventions. This isn’t about dumping your entire codebase into a context window, that’s like giving a new hire 10,000 pages of documentation on their first day. Instead, you need structured, purposeful context.

The README.md: Your project’s story

Your README should answer the questions a new developer would ask on day one. Here’s a template optimized for AI comprehension:

# Project Name

## What This Project Does
[2-3 sentences explaining the core purpose and value proposition]

## Architecture Overview
- **Frontend**: Next.js 14 with TypeScript, deployed on Vercel
- **Backend**: Node.js + Express, containerized with Docker
- **Database**: PostgreSQL with Prisma ORM
- **Infrastructure**: Upsun PaaS with preview environments

## Quick Start
```bash
npm install
npm run dev
# API runs on http://localhost:3000
# Frontend runs on http://localhost:3001

## Core Concepts
- **User**: Authenticated entity with roles (admin, user, guest)
- **Workspace**: Isolated environment for team collaboration
- **Pipeline**: Automated workflow triggered by events

## Project Structure
src/
├── api/          # Express routes and middleware
├── services/     # Business logic layer
├── models/       # Database models (Prisma)
├── utils/        # Shared utilities
└── types/        # TypeScript definitions

## External Dependencies
- Stripe API for payments (test mode in development)
- SendGrid for transactional emails
- Redis for session management
- S3-compatible storage for file uploads

This structure gives AI assistants immediate understanding of your technology choices, project layout, and key abstractions. When you later ask “add user authentication,” the assistant knows you’re using Express, Prisma, and Redis for sessions.

The AGENTS.md: Your team’s DNA

While README describes what your project is, AGENTS.md defines how you build it. This file contains your team’s coding philosophy, standards, and preferences—essentially your team’s DNA that the AI assistant needs to internalize.

Comprehensive AGENTS.md template

Here’s a battle-tested AGENTS.md structure that covers all aspects of team conventions. This example uses specific technologies and stacks so it needs to be tailored to your needs!

# Development Guidelines for AI Assistants

## Team Philosophy
We prioritize code readability over cleverness. Our codebase should be maintainable by any team member, including juniors. We favor explicit over implicit, and simple over complex. When in doubt, choose the solution that's easier to debug at 3 AM during an incident.

## Code Style Preferences

### General Principles
- Functions should do one thing well (Single Responsibility)
- Prefer composition over inheritance
- Use early returns to reduce nesting
- Maximum line length: 100 characters
- Maximum file length: 300 lines (split larger files)
- Indent with 2 spaces (never tabs)

### Naming Conventions
- Variables: camelCase (userId, orderTotal)
- Constants: UPPER_SNAKE_CASE (MAX_RETRIES, API_TIMEOUT)
- Classes: PascalCase (UserService, OrderController)
- Interfaces: PascalCase with 'I' prefix (IUserService, IOrderData)
- Type aliases: PascalCase (UserRole, OrderStatus)
- Files: kebab-case (user-service.ts, order-controller.ts)
- Test files: *.spec.ts or *.test.ts adjacent to source
- React components: PascalCase files (UserProfile.tsx)
- Hooks: use prefix (useAuth, useLocalStorage)
- Boolean variables: is/has/should prefix (isLoading, hasError, shouldRefresh)
- Event handlers: on prefix (onSubmit, onClick)
- Private methods: underscore prefix (_validateInput, _processData)

### Comment Standards
- Use JSDoc for public APIs
- Comments explain "why" not "what"
- TODO comments must include author and date: // TODO(john, 2024-03): Refactor after v2 launch
- No commented-out code in main branch
- Document complex regex patterns
- Document magic numbers

Example:

/**
 * Calculates compound interest with monthly contributions
 * @param principal - Initial investment amount
 * @param rate - Annual interest rate as decimal (0.05 for 5%)
 * @param years - Investment period in years
 * @param monthlyContribution - Optional monthly addition
 * @returns Total value after compound interest
 */
function calculateCompoundInterest(
  principal: number,
  rate: number, 
  years: number,
  monthlyContribution = 0
): number {
  // Using monthly compounding for more accurate results
  const monthlyRate = rate / 12;
  const months = years * 12;
  
  // Formula: P(1 + r)^n + PMT × (((1 + r)^n - 1) / r)
  // where P = principal, r = rate, n = periods, PMT = payment
  const compoundPrincipal = principal * Math.pow(1 + monthlyRate, months);
  const contributionValue = monthlyContribution * 
    ((Math.pow(1 + monthlyRate, months) - 1) / monthlyRate);
  
  return compoundPrincipal + contributionValue;
}


## TypeScript Conventions

### Type Safety Rules
- Always define explicit return types for functions
- Use interfaces for object shapes, types for unions/primitives
- Prefer `unknown` over `any` when type is truly unknown
- Use strict mode with all checks enabled
- Define DTOs for all API boundaries
- Use branded types for domain primitives

### Type Examples

// Branded type for type safety
type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };

// Domain types with clear constraints
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

// Interface for data objects
interface IOrderData {
  id: OrderId;
  userId: UserId;
  items: IOrderItem[];
  status: OrderStatus;
  createdAt: Date;
  updatedAt: Date;
}

// Type for function options
type CreateOrderOptions = {
  skipInventoryCheck?: boolean;
  notifyUser?: boolean;
  priority?: 'normal' | 'high';
};

// Utility types we use frequently
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;


## Architecture Patterns

### Service Layer Pattern
All business logic lives in services, not controllers or models:

// ✅ Good: Business logic in service
class OrderService {
  async createOrder(data: CreateOrderDTO): Promise<Order> {
    // Validation
    await this.validateInventory(data.items);
    
    // Business logic
    const pricing = this.calculatePricing(data.items);
    
    // Persistence
    return this.orderRepository.create({
      ...data,
      ...pricing
    });
  }
}

// ❌ Bad: Business logic in controller
class OrderController {
  async create(req: Request, res: Response) {
    // Don't put business logic here!
    const pricing = calculatePricing(req.body.items); // Wrong!
  }
}


### Dependency Injection
Use constructor injection with interfaces:

class OrderService {
  constructor(
    private readonly orderRepo: IOrderRepository,
    private readonly inventoryService: IInventoryService,
    private readonly emailService: IEmailService
  ) {}
}


### Repository Pattern
Abstract data access behind repositories:

interface IUserRepository {
  findById(id: UserId): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  create(data: CreateUserDTO): Promise<User>;
  update(id: UserId, data: UpdateUserDTO): Promise<User>;
  softDelete(id: UserId): Promise<void>;
}


## Error Handling

### Error Classes
Define specific error classes for different scenarios:

// Base error class
abstract class ApplicationError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public code: string
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class ValidationError extends ApplicationError {
  constructor(message: string, public fields: Record<string, string>) {
    super(message, 400, 'VALIDATION_ERROR');
  }
}

class NotFoundError extends ApplicationError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, 404, 'NOT_FOUND');
  }
}

class UnauthorizedError extends ApplicationError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED');
  }
}


### Error Handling Rules
- Never use `console.log` in production code
- Use structured logging with context
- Always handle promise rejections
- Throw custom error classes, not strings
- Don't expose internal errors to users
- Log full error details, return safe messages

Example:

async function processPayment(orderId: OrderId): Promise<void> {
  try {
    const order = await orderRepo.findById(orderId);
    if (!order) {
      throw new NotFoundError('Order', orderId);
    }
    
    await paymentGateway.charge(order.total);
    
  } catch (error) {
    logger.error('Payment processing failed', {
      orderId,
      error: error instanceof Error ? error.message : 'Unknown error',
      stack: error instanceof Error ? error.stack : undefined,
      timestamp: new Date().toISOString()
    });
    
    if (error instanceof ApplicationError) {
      throw error; // Re-throw known errors
    }
    
    // Don't expose internal errors
    throw new ApplicationError('Payment processing failed', 500, 'PAYMENT_ERROR');
  }
}


## Testing Philosophy

### Testing Principles
- Write tests first (TDD approach)
- Aim for 80% code coverage minimum
- Focus on behavior, not implementation
- Test public interfaces, not private methods
- Each test should test one thing
- Tests should be independent and idempotent

### Test Structure
Follow AAA pattern (Arrange, Act, Assert):

describe('OrderService', () => {
  describe('createOrder', () => {
    it('should calculate correct total with tax and shipping', async () => {
      // Arrange
      const mockItems = [
        { productId: '1', quantity: 2, price: 10.00 },
        { productId: '2', quantity: 1, price: 20.00 }
      ];
      const orderData = { items: mockItems, userId: 'user123' };
      
      // Act
      const result = await orderService.createOrder(orderData);
      
      // Assert
      expect(result.subtotal).toBe(40.00);
      expect(result.tax).toBe(4.00); // 10% tax
      expect(result.shipping).toBe(5.00);
      expect(result.total).toBe(49.00);
    });
    
    it('should throw ValidationError when items array is empty', async () => {
      // Arrange
      const orderData = { items: [], userId: 'user123' };
      
      // Act & Assert
      await expect(orderService.createOrder(orderData))
        .rejects
        .toThrow(ValidationError);
    });
  });
});


### Mocking Strategy
- Mock external services (APIs, databases)
- Don't mock internal modules
- Use factories for test data
- Reset mocks between tests

## Database Patterns

### Query Patterns
- Use transactions for multi-table operations
- Always use parameterized queries
- Implement soft delete with `deletedAt` timestamps
- Include audit fields on all tables
- Use optimistic locking for concurrent updates

### Standard Fields
Every table must include:

interface IAuditFields {
  id: string; // UUID
  createdAt: Date;
  createdBy: UserId;
  updatedAt: Date;
  updatedBy: UserId;
  deletedAt: Date | null;
  deletedBy: UserId | null;
  version: number; // For optimistic locking
}


### Migration Rules
- Never modify existing migrations
- Always provide rollback logic
- Test migrations on copy of production data
- Name migrations descriptively: `001_add_user_email_verification.ts`

## API Design

### RESTful Conventions

GET    /api/v1/orders          # List with pagination
GET    /api/v1/orders/:id      # Get single resource
POST   /api/v1/orders          # Create new resource
PUT    /api/v1/orders/:id      # Full update
PATCH  /api/v1/orders/:id      # Partial update
DELETE /api/v1/orders/:id      # Soft delete


### Response Formats
Success response:

{
  "success": true,
  "data": {
    "id": "123",
    "name": "Example"
  },
  "meta": {
    "timestamp": "2024-01-01T00:00:00Z",
    "version": "1.0.0"
  }
}


Error response:

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      }
    ]
  },
  "meta": {
    "timestamp": "2024-01-01T00:00:00Z",
    "requestId": "req_123456"
  }
}


### Pagination Standard

{
  "data": [...],
  "pagination": {
    "page": 1,
    "perPage": 20,
    "total": 100,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  }
}


## Security Requirements

### Authentication & Authorization
- Use JWT with refresh tokens
- Implement RBAC (Role-Based Access Control)
- Session timeout after 30 minutes of inactivity
- Require MFA for admin accounts
- Log all authentication events

### Input Validation
- Validate all input with Zod schemas
- Sanitize HTML content with DOMPurify
- Reject requests with unexpected fields
- Implement request size limits

### Security Headers

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));


### Rate Limiting
- Public endpoints: 100 requests/minute
- Authenticated endpoints: 1000 requests/minute  
- Login endpoints: 5 attempts/15 minutes
- Use sliding window algorithm

## Performance Standards

### Response Time Targets
- API endpoints: < 200ms (p95)
- Database queries: < 50ms
- External API calls: implement circuit breakers
- Page load time: < 3 seconds

### Optimization Rules
- Implement pagination for lists (max 100 items)
- Use database indexes on foreign keys and frequently queried fields
- Cache expensive computations with Redis
- Implement database connection pooling
- Use CDN for static assets

## Git Workflow

### Branch Naming
- Features: `feature/JIRA-123-user-authentication`
- Bugfixes: `bugfix/JIRA-456-payment-calculation`
- Hotfixes: `hotfix/JIRA-789-critical-security-fix`
- Releases: `release/v1.2.0`

### Commit Message Format
Follow conventional commits:

type(scope): subject

body (optional)

footer (optional)


Types:
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- style: Code style changes
- refactor: Code refactoring
- perf: Performance improvements
- test: Test additions or corrections
- chore: Build process or auxiliary tool changes

Example:

feat(auth): implement OAuth2 login with Google

- Added Google OAuth2 strategy
- Created callback endpoint for token exchange
- Updated user model to store OAuth profiles

Closes JIRA-123


### PR Rules
- PRs must pass all CI checks
- Require at least 2 approvals
- Must include tests for new features
- Update documentation for API changes
- Link to JIRA ticket in description

## Documentation Standards

### Code Documentation
- Document all public APIs with JSDoc
- Include examples in documentation
- Keep documentation next to code
- Update documentation with code changes

### API Documentation
- Use OpenAPI 3.0 specification
- Include request/response examples
- Document all error codes
- Provide curl examples

### README Files
Each module should have a README explaining:
- Purpose and responsibilities
- Public interface
- Usage examples
- Dependencies
- Configuration options

## Monitoring and Logging

### Logging Levels
- ERROR: System errors requiring immediate attention
- WARN: Issues that should be investigated
- INFO: Important business events
- DEBUG: Detailed diagnostic information

### What to Log
- All API requests (method, path, duration, status)
- Authentication events (login, logout, failed attempts)
- Database query times > 100ms
- External API calls and responses
- Business events (order created, payment processed)
- Errors with full context

### Metrics to Track
- Request rate and latency (by endpoint)
- Error rate (4xx vs 5xx)
- Database connection pool usage
- Queue lengths and processing times
- Business metrics (orders/hour, conversion rate)

## Development Environment

### Required Tools
- Node.js 20.x LTS
- TypeScript 5.x
- Docker and Docker Compose
- Git 2.x
- VS Code with team settings

### VS Code Extensions
- ESLint
- Prettier
- GitLens
- Docker
- Thunder Client (API testing)
- Error Lens
- TODO Highlight

### Environment Variables
Never commit .env files. Use .env.example:

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/dbname

# Redis
REDIS_URL=redis://localhost:6379

# Auth
JWT_SECRET=your-secret-here
JWT_REFRESH_SECRET=your-refresh-secret

# External APIs
STRIPE_API_KEY=sk_test_...
SENDGRID_API_KEY=SG...

# Feature Flags
ENABLE_NEW_PAYMENT_FLOW=false


## Definition of Done

A task is only "done" when:
- [ ] Code is written and follows team conventions
- [ ] Unit tests written and passing (>80% coverage)
- [ ] Integration tests for API endpoints
- [ ] Code reviewed and approved by 2 team members
- [ ] Documentation updated (code, API, README)
- [ ] No linting or TypeScript errors
- [ ] Security scan passed
- [ ] Performance requirements met
- [ ] Deployed to staging and tested
- [ ] Monitoring and alerts configured
- [ ] Feature flag configured (if applicable)
- [ ] JIRA ticket updated and moved to Done

## AI-Specific Instructions

When generating code:
1. Always follow these conventions without exception
2. If unsure about a convention, ask for clarification
3. Generate complete code, not snippets
4. Include error handling in all examples
5. Write tests alongside implementation
6. Add detailed comments for complex logic
7. Use our established patterns, don't introduce new ones
8. Validate all inputs according to our standards
9. Follow our Git commit message format
10. Alert us to any security concerns

Remember: Consistency is more important than perfection. Follow team patterns even if you know a "better" way.

Team-specific customization

Different teams have different needs. Here are specialized sections you might add:

For Frontend Teams

## React/Frontend Conventions

### Component Structure
- One component per file
- Co-locate styles, tests, and stories
- Use function components with hooks
- Props interface named `I{ComponentName}Props`

### State Management
- Use Zustand for global state
- React Query for server state
- Local state with useState/useReducer
- No Redux (legacy pattern for us)

### Styling
- CSS Modules for component styles
- Tailwind for utility classes
- No inline styles except dynamic values
- Mobile-first responsive design

For Microservices Teams

## Microservices Patterns

### Service Communication
- REST for synchronous communication
- RabbitMQ for async messaging
- gRPC for internal service-to-service
- GraphQL federation for client-facing API

### Service Boundaries
- Each service owns its database
- No shared database access
- Data duplication is acceptable
- Event sourcing for data sync

For DevOps-Heavy Teams

## Infrastructure as Code

### Container Standards
- Multi-stage Docker builds
- Non-root user in containers
- Health checks required
- Resource limits defined

### CI/CD Pipeline
- Build -> Test -> Security Scan -> Deploy
- Automatic rollback on metric degradation
- Blue-green deployments for production
- Canary releases for high-risk changes

For Claude Code users, create a symlink from CLAUDE.md to AGENTS.md:

ln -s AGENTS.md CLAUDE.md

Organizing knowledge for larger projects

As projects grow, flat files become unwieldy. You can organize your context hierarchically:

.ai/
├── knowledge/
│   ├── architecture/
│   │   ├── database-schema.md
│   │   ├── api-design.md
│   │   └── deployment.md
│   ├── decisions/
│   │   ├── ADR-001-chose-postgresql.md
│   │   ├── ADR-002-event-driven-architecture.md
│   │   └── ADR-003-monorepo-structure.md
│   └── examples/
│       ├── service-pattern.ts
│       ├── test-structure.spec.ts
│       └── error-handling.ts
└── rules/
    ├── code-style.md
    ├── security.md
    └── performance.md

But beware of context rot—too much information degrades performance. Research shows that beyond 10K tokens, AI assistants struggle to find relevant information. Keep your context focused and relevant to current work.

Phase 1: Explore - Teaching your AI to understand

The Explore phase transforms your AI from an outsider to an informed team member. This isn’t about writing code, it’s about building understanding. Most developers skip this phase, jumping straight to implementation, then wonder why the AI misses obvious patterns or violates architectural principles.

The exploration mindset

When you explore, you’re explicitly preventing the AI from coding. This counterintuitive approach forces deeper analysis and prevents premature implementation based on incomplete understanding.

Example: Exploring a payment service

claude
We're going to work on the payment processing module. I need you to understand how it currently works before we make changes.

Read through:
- src/services/payment/
- src/api/routes/payment.ts
- tests/payment.test.ts
- docs/payment-flow.md

DO NOT write any code yet. After reading, explain:
1. How does the current payment flow work?
2. What external services are involved?
3. What are the main data models?
4. What error cases are handled?
5. What patterns do you notice in the implementation?

Focus on understanding the business logic and architectural decisions.

The AI will analyze your codebase and provide insights like:

claude
After analyzing the payment module, here's my understanding:

1. **Payment Flow**:
   - User initiates payment through POST /api/payment/create
   - System creates a pending PaymentIntent with Stripe
   - Frontend confirms payment using Stripe Elements
   - Webhook receives confirmation from Stripe
   - System updates order status and sends confirmation email

2. **External Services**:
   - Stripe for payment processing (using SDK v12.5)
   - SendGrid for email notifications
   - Redis for idempotency keys (preventing duplicate charges)

3. **Data Models**:
   - Payment: Core record with amount, status, metadata
   - PaymentMethod: Stored customer payment methods
   - Transaction: Audit log of all payment events

4. **Error Handling**:
   - Retry logic for network failures (3 attempts with exponential backoff)
   - Webhook signature validation to prevent fraud
   - Graceful degradation if email service is down

5. **Patterns Observed**:
   - Service layer pattern isolates business logic from routes
   - All monetary amounts stored as integers (cents)
   - Event sourcing pattern for payment state changes
   - Dependency injection for testability

Advanced exploration with Claude Code

Claude Code’s exploration capabilities extend beyond simple file reading. Use these commands to build comprehensive understanding:

claude
# Initialize Claude with exploration mode
claude --explore "src/services/" --depth=3 --prepare-discuss

# For complex features, use think modes during exploration
claude --think-hard
You can use Shift + Tab in claude to switch from plan mode to classic.

Then provide exploration prompts that leverage Claude’s thinking capabilities:

claude
Think hard about the authentication system. 

Read through:
- All files in src/auth/
- Middleware implementations in src/middleware/
- Session management in src/services/session/
- Related tests

Create a mental model of:
- How tokens are generated and validated
- Session lifecycle from creation to expiration  
- Permission checking flow
- Refresh token rotation strategy

After exploring, prepare to discuss potential security improvements.

Multi-file exploration patterns

For features spanning multiple modules, structure your exploration systematically:

claude
We need to understand the document processing pipeline. This touches multiple systems.

Phase 1 - Input Layer:
- Read src/api/upload/ to understand file ingestion
- Review src/validators/document.ts for validation rules

Phase 2 - Processing Core:
- Examine src/workers/document-processor.ts
- Understand src/services/ocr/ for text extraction
- Review src/services/nlp/ for entity recognition

Phase 3 - Storage and Retrieval:
- Check src/models/document.ts for data structure
- Review src/services/search/ for indexing logic

DO NOT suggest improvements yet. First, map out:
1. The complete data flow from upload to search
2. All transformation steps
3. Error handling at each stage
4. Performance bottlenecks you observe

We'll discuss optimizations after you fully understand the current implementation.

Context preservation between sessions

One challenge with AI assistants is maintaining context across sessions. Document your exploration findings:

claude
After exploring the codebase, write your understanding to knowledge/payment-system-analysis.md

Include:
- High-level architecture diagram (as ASCII art)
- Key components and their responsibilities
- Data flow for main use cases
- Integration points with external services
- Current limitations or technical debt observed

This will help future sessions start with context.

Phase 2: Plan - Architecting before implementing

Planning transforms vague requirements into concrete, testable specifications. This phase leverages AI’s ability to think systematically while keeping you in control of architectural decisions.

Strategic thinking modes in Claude Code

Claude Code offers multiple thinking modes, each consuming different token budgets:

  • Think (4,000 tokens): Quick planning for simple features
  • Think hard (10,000 tokens): Standard planning for most tasks
  • Think hardest (32,000 tokens): Complex architectural decisions
  • Ultrathink (customizable): Comprehensive system design

Choose your mode based on task complexity:

claude
Think hard about implementing real-time notifications.

Consider:
- WebSocket vs Server-Sent Events vs polling
- Scalability with multiple server instances
- Message delivery guarantees
- Offline message queueing
- Battery optimization for mobile clients

Create a detailed implementation plan including:
1. Technology choice with justification
2. Server architecture changes needed
3. Client integration approach
4. Database schema for message storage
5. Testing strategy for real-time features

The planning template

Effective plans follow consistent structure. Here’s a template that works across different feature types:

claude
Create a detailed plan for [FEATURE]. Think hardest about this.

Requirements:
- [Specific requirement 1]
- [Specific requirement 2]
- [Performance requirement]
- [Security requirement]

Constraints:
- Must work with existing [system/API]
- Cannot modify [protected component]
- Must maintain backwards compatibility
- Deploy to Upsun with preview environments

Deliverables needed:
1. List all files that need changes
2. New files to create with purpose
3. Database migrations required
4. API endpoint changes
5. Test scenarios to cover
6. Rollback strategy if deployment fails

For each component, specify:
- Function/class names to create
- 2-3 sentences about implementation approach
- Dependencies on other components
- Estimated complexity (simple/medium/complex)

Focus on implementing only what we need now. Don't include "nice to have" features or future extensibility unless specifically required.

Example: Planning a multi-tenant feature

Let’s walk through planning a real feature—adding multi-tenancy to an existing application:

claude
Think hardest about adding multi-tenant support to our application.

Current state:
- Single-tenant PostgreSQL database
- User authentication with JWT
- Express API with Prisma ORM

Requirements:
- Separate data between organizations
- Users can belong to multiple organizations
- Organization admins can invite users
- Data isolation must be bulletproof
- Gradual rollout capability

Create a plan that:
1. Identifies the isolation strategy (schema-per-tenant vs row-level security)
2. Lists all models needing tenant_id
3. Describes middleware for tenant context injection
4. Outlines migration strategy for existing data
5. Specifies test scenarios for isolation verification

Be specific about implementation details.

The AI might respond with:

claude
## Multi-Tenant Implementation Plan

### Isolation Strategy: Row-Level Security (RLS)
**Justification**: Simpler operations, better resource utilization, easier backup/restore per tenant

### Database Changes

#### New Tables
1. **organizations**
   - id, name, slug (unique), created_at, settings (JSONB)
   - Purpose: Root tenant entity

2. **organization_users**
   - organization_id, user_id, role, joined_at
   - Purpose: Many-to-many relationship with roles

#### Modified Tables (add tenant_id)
- projects → add organization_id (FK, NOT NULL after migration)
- documents → add organization_id (via projects relation)
- api_keys → add organization_id

#### Migrations Required

// 001_add_organizations.ts
- Create organizations table
- Create organization_users table
- Add organization_id to affected tables (nullable initially)

// 002_populate_default_org.ts
- Create default organization for existing data
- Update all records with default org
- Make organization_id NOT NULL

// 003_add_rls_policies.ts
- Enable RLS on all tenant tables
- Create policies for tenant isolation


### API Changes

#### New Middleware: src/middleware/tenantContext.ts

// Extracts organization from subdomain or header
// Verifies user has access to organization
// Injects organizationId into req.context
// All queries automatically filtered by tenant


#### Modified Services
1. **src/services/auth.service.ts**
   - Add organization selection to login response
   - Include available organizations in JWT

2. **src/services/prisma.service.ts**
   - Extend Prisma client with automatic tenant filtering
   - Use middleware to inject WHERE clauses

### Test Scenarios

1. **Isolation Tests** (src/tests/multitenancy/isolation.spec.ts)
   - User A in Org1 cannot see Org2 data
   - API calls without org context fail
   - Direct database queries respect RLS

2. **Migration Tests** (src/tests/multitenancy/migration.spec.ts)
   - Existing data accessible after migration
   - Rollback preserves data integrity

3. **Performance Tests**
   - Query performance with tenant filtering
   - Index effectiveness on organization_id

### Rollout Strategy
1. Deploy with feature flag disabled
2. Run migrations in maintenance window
3. Enable for internal testing organization
4. Gradual percentage rollout
5. Full enablement after 1 week stability

### Rollback Plan
- Feature flag disables tenant filtering
- Keep organization_id columns (harmless if unused)
- Revert RLS policies if issues arise

The “My Developer” review technique

After generating a plan, use this psychological trick to get better critique:

claude
My developer created this plan for multi-tenancy. Review it critically—both high-level architecture and implementation details.

What are the potential issues? 
Where might this fail in production?
What security concerns do you see?
How could we simplify this approach?
Are there any race conditions or edge cases not considered?

Be harsh but constructive. I need to know the problems before we build this.

The AI responds more critically when reviewing “someone else’s” work, often catching issues it wouldn’t identify in its own output.

Risk-based planning strategies

Not all features require the same planning depth:

Low risk (simple CRUD, UI changes)

Quick plan to add a "favorite" feature to projects. Users should be able to star/unstar projects. Keep it simple.

Medium risk (integrations, data migrations)

Think hard about integrating with Slack for notifications. Plan the implementation including webhook handling, retry logic, and user configuration.

High risk (security, payments, data privacy)

Think hardest about implementing end-to-end encryption for sensitive documents. 

Generate three different implementation approaches:
1. Client-side encryption with key management service
2. Proxy re-encryption for sharing
3. Hardware security module integration

Compare the approaches across:
- Security strength
- Implementation complexity  
- Performance impact
- User experience
- Maintenance burden

Recommend the best approach with detailed justification.

Storing the specifications

When developing a new feature, you may find useful to actually keep the results from the Explore and Plan steps for future use. One way to achieve this easily is to ask your AI assistant to write the specifications for that feature:

claude
Write the specifications for the feature discussed in `features/NEW_FEATURE.md`.

This is especially handy when your organization needs to develop multiple clients for the same project. You can for example ask the assistant to produce the iOS specification of the app based on the work you are doing on the web version. Another AI assistant can then pick up the spec and will have everything it needs to produce the new feature in native Swift.

Phase 3: Execute - From plan to production code

Execution transforms your carefully crafted plan into working code. This phase requires different strategies depending on your confidence level and the code’s impact radius.

The execution spectrum

Execution isn’t 1 or 0, it’s a spectrum from full automation to careful supervision:

Watched execution (high-risk changes)

claude
Now implement the authentication middleware based on our plan.

After each code block:
1. Run the linter: npm run lint
2. Run the specific tests: npm test -- auth.middleware.spec.ts
3. Show me any errors before proceeding
4. Wait for my confirmation before the next file

Start with src/middleware/auth.ts

Guided execution (medium-risk)

claude
Implement the planned notification system.

Rules:
- Write tests FIRST for each component
- Run tests to confirm they fail
- Then implement the feature
- Run tests again to confirm they pass
- Use our established error handling patterns
- Follow the service layer architecture

Start with the WebSocket manager service.

Autonomous execution (low-risk)

claude
Now think hard and write elegant code that completes the favorite projects feature.

After implementation:
- Run the full test suite
- Check TypeScript compilation
- Verify no linting errors
- Create the migration file

Don't add any features we didn't plan. Focus on clean, minimal implementation.

Test-driven development with AI

TDD with AI assistants requires explicit instructions to avoid the common trap of mock implementations:

claude
We're implementing the payment webhook handler using TDD.

Step 1: Write comprehensive tests for handleStripeWebhook function
- Test successful payment confirmation
- Test signature validation failure  
- Test duplicate event handling (idempotency)
- Test database transaction rollback on error
- Test notification sending

Write REAL tests, not placeholders. Use our existing test patterns from tests/services/

Step 2: Run the tests and show me they fail

Step 3: Implement the minimal code to make tests pass

Step 4: Refactor if needed while keeping tests green

Remember: No implementation until tests are written and failing.

Example: Implementing a complex service

Let’s implement a document processing service with proper error handling and testing:

claude
Based on our plan, implement the document processing service.

Requirements reminder:
- Accept PDF, DOCX, and TXT files
- Extract text using appropriate libraries
- Run NLP for entity extraction
- Store results in PostgreSQL
- Index in Elasticsearch for search

Implementation approach:
1. First, write the TypeScript interfaces
2. Create the service class with stubbed methods
3. Implement one method at a time with tests
4. Add comprehensive error handling
5. Include logging at key points

Start with src/services/document/processor.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ElasticsearchService } from '../search/elasticsearch.service';
import { NlpService } from '../nlp/nlp.service';
import { Logger } from 'winston';

interface ProcessingResult {
  documentId: string;
  extractedText: string;
  entities: Entity[];
  metadata: DocumentMetadata;
}

interface Entity {
  type: 'PERSON' | 'ORGANIZATION' | 'LOCATION';
  value: string;
  confidence: number;
  positions: Array<{ start: number; end: number }>;
}

// Continue with the implementation...

Show me each method as you implement it. Include error handling and logging.

Handling large file modifications

Claude Code excels at modifying large files without breaking existing functionality:

We need to refactor the monolithic user.service.ts file (currently 3000+ lines).

Approach:
1. Extract authentication logic to auth-user.service.ts
2. Extract profile management to profile-user.service.ts  
3. Extract notification logic to notification-user.service.ts
4. Keep core user CRUD in user.service.ts

Rules:
- Maintain all existing public interfaces
- Update dependency injection properly
- Keep all tests passing
- Use careful imports to avoid circular dependencies

Show me the extraction plan first, then proceed with implementation after I approve.

Incremental execution with checkpoints

For complex features, build incrementally with validation checkpoints:

Implement the real-time collaboration feature in stages.

Stage 1: WebSocket connection management
- Implement connection handling
- Add authentication
- Test with multiple clients
CHECKPOINT: Show me manual test results

Stage 2: Operation transformation  
- Implement OT algorithm
- Add conflict resolution
- Test concurrent edits
CHECKPOINT: Run integration tests

Stage 3: Persistence layer
- Add operation history storage
- Implement replay functionality
- Test recovery scenarios
CHECKPOINT: Full system test

Stage 4: UI integration
- Add collaboration indicators
- Implement cursor positions
- Test user experience
CHECKPOINT: End-to-end test

Stop at each checkpoint for review before proceeding.

Quality assurance: Catching AI hallucinations and anti-patterns

AI-generated code requires specialized quality assurance beyond traditional linting. The challenge isn’t only syntax, it’s semantic correctness, security vulnerabilities, and architectural consistency.

The following examples are based on a specific node.js project. While they give you a good understanding as to what should be present, you should build your own templates for this!

Essential TypeScript/Node.js toolchain

Configure your project with AI-specific quality gates:

ESLint configuration for AI code review

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:security/recommended',
    'plugin:node/recommended'
  ],
  plugins: [
    '@typescript-eslint',
    'security',
    'no-secrets',
    'promise',
    'unicorn'
  ],
  rules: {
    // Catch common AI mistakes
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/explicit-function-return-type': 'error',
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-floating-promises': 'error',
    
    // Security rules for AI-generated code
    'security/detect-object-injection': 'error',
    'security/detect-non-literal-regexp': 'error',
    'security/detect-eval-with-expression': 'error',
    'no-secrets/no-secrets': 'error',
    
    // Prevent hallucinated imports
    'node/no-missing-import': 'error',
    'node/no-unpublished-import': 'error',
    
    // Code quality
    'complexity': ['error', { max: 10 }],
    'max-lines-per-function': ['error', { max: 50 }],
    'max-depth': ['error', { max: 3 }]
  },
  parserOptions: {
    project: './tsconfig.json'
  }
};

Automated testing with Vitest

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'c8',
      reporter: ['text', 'json', 'html'],
      lines: 80,
      functions: 80,
      branches: 80,
      statements: 80,
      checkCoverage: true,
      excludeNodeModules: true
    },
    testTimeout: 10000,
    hookTimeout: 10000,
    teardownTimeout: 10000,
    isolate: true,
    threads: true,
    mockReset: true
  }
});

AI-specific code review checklist

Train your AI to self-review with your tailored checklist:

Before finalizing the implementation, review your code against this checklist:

Security:
□ All user input is validated
□ SQL queries use parameterization (or Prisma)
□ Secrets are in environment variables
□ Authentication checks on all protected routes
□ Rate limiting on public endpoints

Performance:
□ Database queries are optimized (no N+1)
□ Async operations are parallelized where possible
□ Large datasets use pagination/streaming
□ Caching implemented where appropriate
□ No synchronous file I/O in request handlers

Error Handling:
□ All promises have .catch() or try/catch
□ Custom error classes used (not string throws)
□ Errors logged with context
□ User-facing errors don't leak sensitive info
□ Database transactions rollback on error

Testing:
□ Unit tests for business logic
□ Integration tests for API endpoints
□ Edge cases covered (null, empty, overflow)
□ Error scenarios tested
□ Mocks used appropriately

Code Quality:
□ Functions do one thing well
□ No code duplication
□ Clear variable and function names
□ Comments explain "why" not "what"
□ TypeScript types are specific (no `any`)

Run through this checklist and fix any issues before marking complete.

Continuous integration for AI workflows

Set up GitHub Actions to catch issues before merge:

# .github/workflows/ai-code-review.yml
name: AI Code Quality Check

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  quality-check:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: TypeScript compile check
        run: npm run build
        
      - name: ESLint security audit
        run: npm run lint:security
        
      - name: Run tests with coverage
        run: npm run test:coverage
        
      - name: Check for common AI anti-patterns
        run: |
          # Check for console.log in production code
          if grep -r "console\.log" --include="*.ts" --exclude-dir="test" src/; then
            echo "❌ Found console.log in production code"
            exit 1
          fi
          
          # Check for hardcoded secrets
          if grep -rE "(api[_-]?key|secret|password|token)\s*=\s*[\"'][^\"']+[\"']" --include="*.ts" src/; then
            echo "❌ Possible hardcoded secrets found"
            exit 1
          fi
          
          # Check for TODO comments (incomplete AI implementation)
          if grep -r "TODO\|FIXME\|XXX" --include="*.ts" src/; then
            echo "⚠️ Found TODO comments - ensure implementation is complete"
          fi          
      
      - name: Comment PR with results
        uses: actions/github-script@v6
        if: always()
        with:
          script: |
            const fs = require('fs');
            const coverage = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
            const total = coverage.total;
            
            const comment = `## AI Code Review Results
            
            ### Coverage Report
            - Lines: ${total.lines.pct}%
            - Functions: ${total.functions.pct}%
            - Branches: ${total.branches.pct}%
            - Statements: ${total.statements.pct}%
            
            ${total.lines.pct < 80 ? '⚠️ Coverage below 80% threshold' : '✅ Coverage meets requirements'}
            `;
            
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            });            

Claude Code tips and tricks

After months of daily use, these techniques consistently improve productivity and output quality:

The resume pattern for long sessions

claude
/resume

Continue implementing the payment service. We were working on webhook handling and got through signature validation. Next, we need to implement the event processing logic.

Plan mode for research without coding

Activate Plan Mode with Shift+Tab twice, then:

claude
Research how other projects handle distributed transactions. Look at:
- Saga pattern implementations
- Two-phase commit examples
- Event sourcing approaches

Find examples in our codebase and well-known open source projects. Don't implement anything yet—just gather information for discussion.

Using MCP servers for enhanced capabilities

Configure Model Context Protocol servers in your claude_mcp_config.json:

{
  "servers": {
    "upsun": {
      "type": "http", 
      "url": "https://mcp.upsun.com",
      "headers": {
        "upsun-api-token": "---",
        "enable-write": "true"
      } 
    },
    "postgres": {
      "command": "npx",
      "args": ["mcp-server-postgres", "postgresql://localhost/mydb"]
    },
    "github": {
      "command": "npx",
      "args": ["mcp-server-github"],
      "env": {
        "GITHUB_TOKEN": "${GITHUB_TOKEN}"
      }
    },
    "playwright": {
      "command": "npx",
      "args": ["mcp-server-playwright"]
    }
  }
}

Then leverage these in your workflow:

claude
Using the Upsun MCP, create a new environment "test-qa" for my project "Test"

Using the postgres MCP server, analyze our database for:
- Tables without indexes on foreign keys
- Unused indexes taking space
- Slow query patterns from pg_stat_statements

Then use the playwright server to:
- Test our checkout flow end-to-end
- Capture screenshots at each step
- Identify any console errors or failed network requests

Message queueing for complex tasks

Queue multiple prompts for intelligent processing:

claude
[First message]
Analyze the performance bottlenecks in our API using the APM data.

[Second message]
Based on the bottlenecks found, create optimization plans for the top 3 issues.

[Third message]
Implement the highest-priority optimization with tests.

Process these in order, but pause if you need clarification on any step.

The “think harder” escalation pattern

Start simple and escalate complexity as needed:

claude
# First attempt - quick solution
Create a function to validate email addresses.

# If insufficient, escalate
Think harder about email validation. Consider international domains, subdomains, and new TLDs. Create a comprehensive solution.

# Maximum analysis if critical
Ultrathink: Design a complete email validation system including syntax checking, DNS verification, and disposable email detection. Consider performance, security, and user experience trade-offs.

Platform-specific considerations: Upsun and modern PaaS

When developing with AI assistants for Cloud Application Platform environments like Upsun, include platform-specific context:

# Deployment Context (add to AGENTS.md)

## Platform: Upsun
- Git-driven deployments from main branch
- Preview environments for each pull request
- Environment variables managed via platform
- Build hooks run during deployment
- Services configured in .upsun/config.yaml

## Build Process

# .upsun/config.yaml snippet
applications:
  app:
    build:
      flavor: none
    hooks:
      build: |
        npm ci --production=false
        npm run build
        npm prune --production
    deploy: |
        npm run migrate

## Available Services
- PostgreSQL 15 (primary database)
- Redis 7 (caching and sessions)
- Elasticsearch 8 (search)
- RabbitMQ (message queue)

## Environment-Specific Behavior
- Production: main branch
- Staging: staging branch  
- Preview: PR-* branches (data cloned from staging)
- Local: Use docker-compose for service parity

This context helps AI assistants generate deployment-aware code:

claude
Implement the transactions display feature.

Requirements:
- Create the migrations on the backend
- Add the endpoints on the API
- Create the transactions screen on the frontend
- Deploy and monitor the build and deploy processes to check for errors

Remember we're using Upsun's built-in environment variables for service connections.

Common pitfalls and how to avoid them

Pitfall 1: Context overload

Problem: Including entire codebase in context Symptom: AI gives generic, inconsistent responses Solution: Selective context loading

claude
# Bad
Here's our entire repository. Implement user authentication.

# Good
For implementing JWT authentication, review:
- Current auth middleware in src/middleware/auth.ts
- User model in src/models/user.ts
- Existing login endpoint in src/api/auth.ts

Focus only on adding refresh token functionality.

Pitfall 2: Ambiguous requirements

Problem: Vague instructions produce vague code Symptom: AI implements features you didn’t want Solution: Specific, measurable requirements

claude
# Bad
Make the search better.

# Good
Improve search with these specific changes:
1. Add fuzzy matching with max edit distance of 2
2. Implement "did you mean?" suggestions
3. Return results in <100ms for 95th percentile
4. Support filtering by date range and document type
5. Highlight matched terms in results

Pitfall 3: Ignoring failed tests

Problem: AI continues despite test failures Symptom: Broken code that “looks right” Solution: Explicit test gates

claude
After each implementation:
1. Run: npm test -- --watch=false
2. If ANY test fails, stop immediately
3. Show me the error
4. Fix the issue before continuing
5. Only proceed when all tests pass

This is non-negotiable. Failed tests = full stop.

Pitfall 4: Missing error handling

Problem: AI writes happy-path code only Symptom: Production crashes on edge cases Solution: Explicit error scenarios

claude
For each function you write, also implement:
1. What happens when inputs are null/undefined?
2. How do we handle network timeouts?
3. What if the database is unavailable?
4. How do we recover from partial failures?
5. What gets logged for debugging?

Example error handling pattern:

try {
  const result = await riskyOperation();
  return result;
} catch (error) {
  logger.error('Operation failed', {
    error: error.message,
    stack: error.stack,
    context: { userId, operationType }
  });
  
  if (error instanceof NetworkError) {
    throw new RetryableError('Network issue, please retry');
  }
  
  throw new ApplicationError('Operation failed', error);
}

Pitfall 5: Security shortcuts

Problem: AI takes unsafe shortcuts Symptom: SQL injection, XSS vulnerabilities Solution: Security-first instructions

claude
Security requirements for ALL code:
1. NEVER concatenate strings for SQL (use Prisma/parameters)
2. ALWAYS validate input with Zod schemas
3. Sanitize user content before rendering
4. Use crypto.randomBytes() for tokens, not Math.random()
5. Hash passwords with bcrypt, minimum 10 rounds
6. Check authorization, not just authentication

If you're unsure about security implications, flag it for review.

Conclusion: From chaos to productivity

The Explore, Plan, Execute methodology transforms AI coding assistants from unpredictable tools into reliable development partners. Success doesn’t come from better prompts alone. It comes from systematic context building, structured planning, and disciplined execution.

Remember these key principles:

  1. Context is king: Invest time in README.md and AGENTS.md. They pay dividends in every coding session.

  2. Exploration prevents pain: Understanding existing code before changing it catches issues that would take hours to debug later.

  3. Plans are contracts: Detailed plans become specifications that keep AI on track during implementation.

  4. Execution needs supervision: Match your supervision level to risk. Watch high-impact changes closely; let low-risk code flow.

  5. Quality gates catch hallucinations: Automated testing, linting, and security scanning catch AI mistakes before they reach production.

The AI assistant remains that eager junior developer, but with proper guidance, it becomes a force multiplier for your productivity. The methodology works because it mirrors how you’d manage a human developer—clear requirements, thorough onboarding, structured planning, and quality review.

Start with small features to build confidence in the workflow. As you refine your context documents and develop prompt patterns that work for your team, you’ll find yourself shipping quality code faster than ever before. The future of development isn’t AI replacing developers, it’s developers wielding AI tools with discipline and expertise.

Last updated on