First time here? Start with Accuracy vs Latency to decide if sub-agents are right for your use case.
Summary
Replace monolithic single-agent workflows with specialized sub-agents that mirror real development teams. Backend engineers handle API development, frontend engineers build UI components, QA engineers write comprehensive tests, and code reviewers ensure quality—each with distinct context, behaviors, and restricted tool access. Division of labor yields substantial quality improvements for large codebases.
The Problem
Single AI agents handling all development aspects (backend, frontend, testing, review) produce lower quality outputs because they lack focused expertise and clear role boundaries. Like a developer trying to be backend engineer, frontend specialist, QA tester, and code reviewer simultaneously, context switching and jack-of-all-trades approach reduces effectiveness in each domain.
The Solution
Implement specialized sub-agents with distinct roles, contexts, and tool permissions orchestrated by a main agent. Each sub-agent focuses on its expertise domain with tailored prompts, behavioral flows, and restricted capabilities. Architecture uses Root CLAUDE.md for shared patterns, .claude/agents/agent.md for behavioral flows (100-200 lines), and package-specific Claude.md for local context.
The Problem
When a single AI agent handles every aspect of development—backend logic, frontend components, database migrations, testing, code review—quality suffers. This is the generalist trap.
The Generalist Trap
Scenario: You ask an AI agent to “Add user authentication to the application.”
What happens with a single generalist agent:
// The agent tries to do everything:
// 1. Backend API (switches to backend context)
app.post('/api/auth/login', async (req, res) => {
// Generic implementation, misses domain patterns
const user = await db.findUser(req.body.email);
if (user && user.password === req.body.password) { // ❌ Plain text comparison!
res.json({ token: 'abc123' }); // ❌ Hardcoded token!
}
});
// 2. Frontend component (switches to frontend context)
function LoginForm() {
// Generic React, doesn't match project patterns
const [email, setEmail] = useState('');
return <form>...</form>; // ❌ Missing validation, error handling
}
// 3. Tests (switches to testing context)
test('login works', () => {
// Superficial test, misses edge cases
expect(login('[email protected]', 'password')).toBeTruthy(); // ❌ No setup, no assertions
});
// 4. Code review (switches to review context)
// "Looks good!" // ❌ Misses security issues, pattern violations
Problems:
- Context switching: Agent loses focus switching between domains
- Generic patterns: Doesn’t apply domain-specific best practices
- Shallow expertise: Surface-level implementation in each area
- Missed issues: Security flaws, pattern violations go unnoticed
- No specialization: Jack-of-all-trades, master of none
Real-World Impact
For a large codebase (50K+ LOC):
Single generalist agent:
- Backend code quality: 6/10 (misses domain patterns)
- Frontend code quality: 5/10 (doesn't match component library)
- Test coverage: 40% (superficial tests)
- Code review effectiveness: 3/10 (misses critical issues)
- Overall velocity: Fast but requires 3-4 revision cycles
Specialized sub-agents:
- Backend code quality: 9/10 (follows domain patterns)
- Frontend code quality: 8/10 (matches component library)
- Test coverage: 85% (comprehensive edge cases)
- Code review effectiveness: 8/10 (catches security, patterns)
- Overall velocity: Slower initially but 1-2 revision cycles
The tradeoff: Higher quality at the cost of orchestration complexity and latency.
The Solution
Create specialized sub-agents that mirror real development team structure:
Agent Team Structure
┌─────────────────────────────────────────────────────────────────┐
│ ORCHESTRATOR AGENT │
│ (Main agent, like "Augster") │
│ │
│ Responsibilities: │
│ • Understands task requirements │
│ • Distributes work to specialized sub-agents │
│ • Coordinates handoffs between agents │
│ • Aggregates results │
└────────────────────┬────────────────────────────────────────────┘
│
┌───────────┼───────────┬───────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────┐ ┌────────┐ ┌──────────┐
│ BACKEND │ │FRONTEND │ │ QA │ │ REVIEWER │
│ ENGINEER │ │ENGINEER │ │ENGINEER│ │ │
└─────────────┘ └─────────┘ └────────┘ └──────────┘
Backend: Frontend: QA: Reviewer:
• API endpoints • Components • Test cases • Quality check
• Business logic • State mgmt • Edge cases • Pattern match
• DB schemas • Styling • Integration • Security scan
• Validation • Routing • E2E tests • Read-only
Specialization Benefits
1. Focused Context
Each agent has domain-specific context:
# Backend Engineer Context (.claude/agents/backend-engineer.md)
You are a Backend Engineer specializing in Node.js/TypeScript APIs.
Your expertise:
- RESTful API design following our tRPC patterns
- Domain-driven design with bounded contexts
- Database schema design (Postgres + Prisma)
- Authentication using our JWT strategy
- Validation using Zod schemas
Your workflow:
1. Read domain CLAUDE.md for business rules
2. Check existing API patterns in /packages/api
3. Follow our layered architecture (routes → services → repositories)
4. Always write Zod validation schemas
5. Include error handling with our Result pattern
6. Write integration tests
You NEVER:
- Write frontend code (delegate to Frontend Engineer)
- Write tests (delegate to QA Engineer)
- Modify code during review (you're a code generator, not reviewer)
2. Behavioral Flows
Each agent follows role-specific workflows:
# Frontend Engineer Behavioral Flow
When asked to create a component:
1. Check component library: /packages/ui/src/components
- What patterns exist?
- What variants are available?
- What props are standard?
2. Review design system: /packages/ui/theme
- What colors are defined?
- What spacing scale is used?
- What typography is available?
3. Implement component:
- Use existing component as template
- Follow naming conventions (PascalCase)
- Use theme tokens, not hardcoded values
- Include prop types with TypeScript
- Add JSDoc comments
4. Create Storybook story:
- Show all variants
- Include interactive controls
- Document props
5. Hand off to QA Engineer for testing
3. Tool Access Control
Restrict agent capabilities based on role:
// Agent tool permissions
const agentPermissions = {
backendEngineer: {
canRead: true,
canWrite: true,
canDelete: false,
allowedPaths: ['packages/api/**', 'packages/domain/**', 'packages/db/**'],
tools: ['Read', 'Write', 'Edit', 'Bash'],
},
frontendEngineer: {
canRead: true,
canWrite: true,
canDelete: false,
allowedPaths: ['packages/ui/**', 'apps/web/**'],
tools: ['Read', 'Write', 'Edit'],
},
qaEngineer: {
canRead: true,
canWrite: true, // Can write test files
canDelete: false,
allowedPaths: ['**/*.test.ts', '**/*.spec.ts', 'tests/**'],
tools: ['Read', 'Write', 'Edit', 'Bash'], // Bash for running tests
},
codeReviewer: {
canRead: true,
canWrite: false, // READ-ONLY - cannot modify code
canDelete: false,
allowedPaths: ['**/*'],
tools: ['Read', 'Grep', 'Glob'], // Only reading/searching tools
},
};
Key insight: Code Reviewer is read-only—it can identify issues but cannot fix them. This prevents the reviewer from introducing new bugs while “fixing” problems.
Implementation
Architecture Pattern
Three-layer context hierarchy:
project/
├── CLAUDE.md # Root: Shared patterns, style guide
├── .claude/
│ └── agents/
│ ├── backend-engineer.md # Behavioral flow (100-200 lines)
│ ├── frontend-engineer.md
│ ├── qa-engineer.md
│ └── code-reviewer.md
└── packages/
├── api/
│ └── CLAUDE.md # Package-specific: API patterns
├── ui/
│ └── CLAUDE.md # Package-specific: Component patterns
└── domain/
└── CLAUDE.md # Package-specific: Business rules
Layer 1: Root CLAUDE.md (Shared Patterns)
# Project Coding Standards
All agents must follow these patterns:
## Architecture
- Monorepo structure with packages
- Layered architecture: Domain → Application → Infrastructure
- Dependency rule: Inner layers don't know about outer layers
## TypeScript Standards
- Strict mode enabled
- No `any` types (use `unknown` + type guards)
- Explicit function return types
- Prefer interfaces over types for objects
## Error Handling
- Use Result pattern, never throw exceptions in business logic
- Return `{ success: boolean, data?: T, error?: string }`
## Testing
- Integration tests over unit tests
- Test behavior, not implementation
- AAA pattern: Arrange, Act, Assert
## Naming Conventions
- Files: kebab-case (user-service.ts)
- Functions: camelCase (createUser)
- Classes: PascalCase (UserService)
- Constants: UPPER_SNAKE_CASE (MAX_RETRIES)
Layer 2: Agent Behavioral Flows
# .claude/agents/backend-engineer.md
You are a Backend Engineer. Your role is to implement server-side logic.
## Your Workflow
### When implementing an API endpoint:
1. **Understand requirements**
- Read the task description
- Identify inputs, outputs, business rules
- Check if similar endpoints exist
2. **Design the endpoint**
- Choose HTTP method (GET/POST/PUT/DELETE)
- Design URL structure following REST conventions
- Define request/response schemas using Zod
3. **Implement layers** (follow existing patterns):
**Route layer** (`packages/api/src/routes/users.ts`):
```typescript
router.post('/users', validateSchema(createUserSchema), createUserHandler);
Handler layer (packages/api/src/handlers/users.ts):
async function createUserHandler(req: Request, res: Response) {
const result = await userService.createUser(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
return res.status(201).json(result.data);
}
Service layer (packages/application/src/services/user-service.ts):
async function createUser(data: CreateUserDTO): Promise<Result<User>> {
// Business logic here
const validation = validateUserData(data);
if (!validation.success) {
return { success: false, error: validation.error };
}
const user = await userRepository.create(data);
return { success: true, data: user };
}
-
Add validation
- Define Zod schema in
packages/api/src/schemas/users.ts - Validate at API boundary (route layer)
- Define Zod schema in
-
Handle errors
- Use Result pattern, no exceptions
- Return descriptive error messages
- Log errors for debugging
-
Hand off to QA Engineer
- Code complete, ready for testing
- Provide: endpoint URL, expected inputs/outputs, edge cases to test
What you DON’T do:
- Don’t write frontend code (Frontend Engineer’s job)
- Don’t write tests (QA Engineer’s job)
- Don’t review your own code (Code Reviewer’s job)
Tools you use:
- Read: Study existing code
- Write: Create new files
- Edit: Modify existing code
- Bash: Run database migrations, check schemas
### Layer 3: Package-Specific Context
```markdown
# packages/api/CLAUDE.md
API Package - RESTful endpoints using Express + tRPC
## Patterns in this package
### Route structure
All routes follow this pattern:
```typescript
// packages/api/src/routes/[resource].ts
import { Router } from 'express';
import { validateSchema } from '../middleware/validation';
import { authenticate } from '../middleware/auth';
import * as handlers from '../handlers/[resource]';
import * as schemas from '../schemas/[resource]';
const router = Router();
// Public routes
router.post('/[resource]', validateSchema(schemas.create), handlers.create);
// Protected routes
router.get('/[resource]/:id', authenticate, handlers.getById);
router.put('/[resource]/:id', authenticate, validateSchema(schemas.update), handlers.update);
router.delete('/[resource]/:id', authenticate, handlers.delete);
export default router;
Authentication
Use JWT tokens with our custom middleware:
import { authenticate } from '../middleware/auth';
router.get('/protected', authenticate, handler);
// req.user will contain decoded JWT payload
Validation schemas
All schemas in schemas/ directory using Zod:
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
});
## Orchestrator Pattern
### Main Agent Distributes Work
```typescript
// Pseudo-code for orchestrator logic
class OrchestratorAgent {
async handleTask(task: string) {
// Analyze task to determine required agents
const analysis = this.analyzeTask(task);
// Example: "Add user authentication"
if (analysis.requiresBackend) {
const backendResult = await this.delegateToBackendEngineer({
task: 'Implement POST /api/auth/login endpoint',
context: this.getBackendContext(),
});
}
if (analysis.requiresFrontend) {
const frontendResult = await this.delegateToFrontendEngineer({
task: 'Create LoginForm component',
context: this.getFrontendContext(),
dependencies: backendResult, // API endpoint info
});
}
if (analysis.requiresTests) {
const qaResult = await this.delegateToQAEngineer({
task: 'Write integration tests for authentication flow',
context: this.getQAContext(),
artifacts: [backendResult, frontendResult],
});
}
if (analysis.requiresReview) {
const reviewResult = await this.delegateToCodeReviewer({
task: 'Review authentication implementation',
context: this.getReviewContext(),
files: this.getModifiedFiles(),
});
if (reviewResult.issuesFound) {
// Route issues back to appropriate engineer
await this.handleReviewIssues(reviewResult.issues);
}
}
return this.aggregateResults([
backendResult,
frontendResult,
qaResult,
reviewResult,
]);
}
analyzeTask(task: string): TaskAnalysis {
// Use LLM to understand task requirements
// Determine which agents are needed
return {
requiresBackend: true,
requiresFrontend: true,
requiresTests: true,
requiresReview: true,
};
}
}
Sequential vs Parallel Execution
// Sequential: Backend → Frontend → QA → Review
// Use when tasks have dependencies
const backendResult = await backendAgent.implement();
const frontendResult = await frontendAgent.implement(backendResult);
const qaResult = await qaAgent.test([backendResult, frontendResult]);
const reviewResult = await reviewAgent.review([backendResult, frontendResult, qaResult]);
// Parallel: Backend + Frontend simultaneously
// Use when tasks are independent
const [backendResult, frontendResult] = await Promise.all([
backendAgent.implement(),
frontendAgent.implement(),
]);
// Then QA tests both
const qaResult = await qaAgent.test([backendResult, frontendResult]);
Real-World Example: Add Payment Feature
Task: “Add Stripe payment processing to checkout”
Orchestrator analysis:
- Requires: Backend (API), Frontend (UI), QA (tests), Review (security)
- Sequence: Backend → Frontend → QA → Review
Step 1: Backend Engineer
Delegated task: “Implement Stripe payment API endpoint”
Backend Engineer context:
- Root CLAUDE.md: Error handling, TypeScript standards
.claude/agents/backend-engineer.md: API implementation workflowpackages/api/CLAUDE.md: Route patterns, authentication
Backend Engineer generates:
// packages/api/src/routes/payments.ts
import { Router } from 'express';
import { validateSchema } from '../middleware/validation';
import { authenticate } from '../middleware/auth';
import { createPaymentHandler } from '../handlers/payments';
import { createPaymentSchema } from '../schemas/payments';
const router = Router();
router.post(
'/payments',
authenticate,
validateSchema(createPaymentSchema),
createPaymentHandler
);
export default router;
// packages/api/src/schemas/payments.ts
import { z } from 'zod';
export const createPaymentSchema = z.object({
amount: z.number().positive(),
currency: z.enum(['usd', 'eur', 'gbp']),
paymentMethodId: z.string(),
});
// packages/application/src/services/payment-service.ts
import Stripe from 'stripe';
import { Result } from '../types';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function createPayment(
data: CreatePaymentDTO
): Promise<Result<Payment>> {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: data.amount,
currency: data.currency,
payment_method: data.paymentMethodId,
confirm: true,
});
return {
success: true,
data: {
id: paymentIntent.id,
status: paymentIntent.status,
amount: paymentIntent.amount,
},
};
} catch (error) {
return {
success: false,
error: `Payment failed: ${error.message}`,
};
}
}
Output to orchestrator: “Backend complete. Endpoint: POST /api/payments. Schema defined. Ready for frontend.”
Step 2: Frontend Engineer
Delegated task: “Create payment form component that calls POST /api/payments”
Frontend Engineer context:
- Root CLAUDE.md: TypeScript standards
.claude/agents/frontend-engineer.md: Component creation workflowpackages/ui/CLAUDE.md: Component patterns, design system- Backend result: API endpoint details
Frontend Engineer generates:
// packages/ui/src/components/PaymentForm.tsx
import { useState } from 'react';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { Button } from './Button';
import { Input } from './Input';
import { usePayment } from '../hooks/usePayment';
interface PaymentFormProps {
amount: number;
currency: string;
onSuccess: (paymentId: string) => void;
onError: (error: string) => void;
}
export function PaymentForm({
amount,
currency,
onSuccess,
onError,
}: PaymentFormProps) {
const stripe = useStripe();
const elements = useElements();
const { createPayment, loading } = usePayment();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
const cardElement = elements.getElement(CardElement);
if (!cardElement) return;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
onError(error.message || 'Payment method creation failed');
return;
}
const result = await createPayment({
amount,
currency,
paymentMethodId: paymentMethod.id,
});
if (result.success) {
onSuccess(result.data.id);
} else {
onError(result.error || 'Payment failed');
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
<Button type="submit" disabled={loading || !stripe}>
{loading ? 'Processing...' : `Pay ${amount / 100} ${currency.toUpperCase()}`}
</Button>
</form>
);
}
Output to orchestrator: “Frontend complete. Component: PaymentForm. Integrated with Stripe. Ready for testing.”
Step 3: QA Engineer
Delegated task: “Write comprehensive tests for payment flow”
QA Engineer context:
- Root CLAUDE.md: Testing standards (integration > unit, AAA pattern)
.claude/agents/qa-engineer.md: Test writing workflow- Backend + Frontend results: Implementation details
QA Engineer generates:
// packages/api/src/handlers/__tests__/payments.integration.test.ts
import request from 'supertest';
import { app } from '../../app';
import { generateAuthToken } from '../../test-utils/auth';
import Stripe from 'stripe';
jest.mock('stripe');
describe('POST /api/payments', () => {
const mockStripe = Stripe as jest.MockedClass<typeof Stripe>;
beforeEach(() => {
jest.clearAllMocks();
});
it('should create payment successfully with valid data', async () => {
// Arrange
const token = generateAuthToken({ userId: 'user_123' });
mockStripe.prototype.paymentIntents.create.mockResolvedValue({
id: 'pi_123',
status: 'succeeded',
amount: 5000,
});
// Act
const response = await request(app)
.post('/api/payments')
.set('Authorization', `Bearer ${token}`)
.send({
amount: 5000,
currency: 'usd',
paymentMethodId: 'pm_123',
});
// Assert
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.id).toBe('pi_123');
expect(response.body.data.status).toBe('succeeded');
});
it('should return 400 for invalid amount', async () => {
const token = generateAuthToken({ userId: 'user_123' });
const response = await request(app)
.post('/api/payments')
.set('Authorization', `Bearer ${token}`)
.send({
amount: -100, // Invalid: negative
currency: 'usd',
paymentMethodId: 'pm_123',
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('amount');
});
it('should return 401 for unauthenticated request', async () => {
const response = await request(app)
.post('/api/payments')
.send({
amount: 5000,
currency: 'usd',
paymentMethodId: 'pm_123',
});
expect(response.status).toBe(401);
});
it('should handle Stripe API errors gracefully', async () => {
const token = generateAuthToken({ userId: 'user_123' });
mockStripe.prototype.paymentIntents.create.mockRejectedValue(
new Error('Card declined')
);
const response = await request(app)
.post('/api/payments')
.set('Authorization', `Bearer ${token}`)
.send({
amount: 5000,
currency: 'usd',
paymentMethodId: 'pm_123',
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Card declined');
});
});
Output to orchestrator: “Tests complete. Coverage: 95%. All edge cases tested. 4/4 tests passing. Ready for review.”
Step 4: Code Reviewer
Delegated task: “Review payment implementation for security and quality”
Code Reviewer context:
- Root CLAUDE.md: All coding standards
.claude/agents/code-reviewer.md: Review checklist- All implementation files (read-only access)
Code Reviewer generates:
# Code Review: Payment Feature
## Security ✅ PASS
- ✅ Stripe secret key loaded from environment variable
- ✅ Authentication required on payment endpoint
- ✅ Input validation using Zod schema
- ✅ No sensitive data logged
- ✅ HTTPS required (enforced by middleware)
## Code Quality ✅ PASS
- ✅ Follows Result pattern for error handling
- ✅ TypeScript strict mode, no `any` types
- ✅ Consistent naming conventions
- ✅ Proper separation of concerns (routes → handlers → services)
- ✅ Error messages are descriptive
## Testing ✅ PASS
- ✅ Integration tests cover happy path
- ✅ Edge cases tested (invalid input, auth errors, Stripe errors)
- ✅ Tests follow AAA pattern
- ✅ Mocks used appropriately for Stripe API
## Issues Found ⚠️ MINOR
1. **Missing rate limiting** (Severity: MEDIUM)
- Location: `packages/api/src/routes/payments.ts:12`
- Issue: Payment endpoint should have rate limiting to prevent abuse
- Recommendation: Add rate limiting middleware
```typescript
import { rateLimit } from '../middleware/rate-limit';
router.post(
'/payments',
rateLimit({ maxRequests: 10, windowMs: 60000 }), // 10 requests/minute
authenticate,
validateSchema(createPaymentSchema),
createPaymentHandler
);
- Frontend error handling could be improved (Severity: LOW)
- Location:
packages/ui/src/components/PaymentForm.tsx:45 - Issue: Generic error message shown to user
- Recommendation: Map Stripe error codes to user-friendly messages
- Location:
Overall Assessment
Status: APPROVED WITH MINOR CHANGES RECOMMENDED
Quality Score: 9/10
Recommendation: Address rate limiting before merging. Frontend error handling can be improved in follow-up.
**Output to orchestrator**: "Review complete. Status: APPROVED WITH MINOR CHANGES. 2 issues found (1 medium, 1 low). Recommendations provided."
### Step 5: Orchestrator Aggregates Results
```markdown
# Payment Feature Implementation Summary
## Completed Work
✅ **Backend** (Backend Engineer)
- POST /api/payments endpoint
- Stripe integration
- Zod validation schema
- Result-based error handling
✅ **Frontend** (Frontend Engineer)
- PaymentForm component
- Stripe Elements integration
- Loading states and error handling
✅ **Tests** (QA Engineer)
- 4 integration tests
- 95% code coverage
- All tests passing
✅ **Review** (Code Reviewer)
- Security: PASS
- Quality: PASS
- Testing: PASS
- Overall: APPROVED WITH MINOR CHANGES
## Action Items
1. Add rate limiting middleware (Backend Engineer) - MEDIUM priority
2. Improve frontend error messages (Frontend Engineer) - LOW priority
## Status
Feature is **READY FOR MERGE** after addressing rate limiting.
Best Practices
1. Keep Agent Contexts Focused
Do:
# Backend Engineer Context
- API patterns
- Database schemas
- Business logic
Don’t:
# Backend Engineer Context
- API patterns
- Database schemas
- React component patterns ❌ (wrong domain)
- E2E testing strategies ❌ (QA's domain)
2. Define Clear Handoff Points
Agents should know when to delegate:
# Backend Engineer Workflow
5. **Hand off to QA Engineer**
- Code complete, ready for testing
- Provide: endpoint URL, expected inputs/outputs, edge cases to test
You STOP HERE. Do not write tests yourself.
3. Use Read-Only Reviewers
Prevent reviewers from introducing bugs:
codeReviewer: {
canRead: true,
canWrite: false, // READ-ONLY
canDelete: false,
tools: ['Read', 'Grep', 'Glob'], // No Edit or Write
}
4. Monitor Quality Metrics
Track whether specialization improves outcomes:
interface SubAgentMetrics {
backendQuality: number; // 1-10 based on review scores
frontendQuality: number;
testCoverage: number; // percentage
reviewEffectiveness: number; // issues found per review
overallVelocity: number; // features/week
revisionCycles: number; // avg iterations to merge
}
const metrics: SubAgentMetrics = {
backendQuality: 9,
frontendQuality: 8,
testCoverage: 87,
reviewEffectiveness: 8,
overallVelocity: 12,
revisionCycles: 1.5, // Down from 3.5 with single agent
};
5. Accept the Latency Tradeoff
Specialization adds orchestration overhead:
Single agent (generalist):
- Time: 5 minutes
- Quality: 6/10
- Revisions: 3-4 cycles
- Total time to merge: 30-40 minutes
Sub-agents (specialists):
- Time: 15 minutes (3x longer initially)
- Quality: 9/10
- Revisions: 1-2 cycles
- Total time to merge: 20-25 minutes
Result: Sub-agents are FASTER overall despite higher initial latency
Key insight: Higher quality = fewer revisions = faster merge
Tradeoffs
Pros ✅
- Higher quality: Specialized agents produce domain-expert-level code
- Better testing: Dedicated QA agent writes comprehensive tests
- Effective reviews: Read-only reviewer catches issues without introducing bugs
- Clear roles: Each agent has focused responsibility
- Fewer revisions: Quality improvements reduce back-and-forth
- Scalable: Easy to add new specialist agents as needed
Cons ❌
- Increased latency: Multiple agent calls add overhead
- Complex orchestration: Main agent must coordinate handoffs
- More context: Need to maintain agent-specific CLAUDE.md files
- Tool restrictions: Requires capability to control agent permissions
- Overkill for small tasks: Single file changes don’t need full team
When to Use Sub-Agents
Use sub-agents for:
- Large codebases (50K+ LOC)
- Full-stack features touching multiple domains
- Production-critical code requiring high quality
- Teams with established patterns worth enforcing
- Projects with clear domain boundaries
Skip sub-agents for:
- Small utilities or scripts
- Prototypes and experiments
- Single-domain changes (e.g., just frontend)
- Codebases without established patterns
- Time-sensitive hot fixes
Related Concepts
- Boundary Enforcement with Layered Architecture – ESLint rules enforce layer boundaries that sub-agents must respect
- DDD Bounded Contexts for LLMs – Domain boundaries map naturally to specialized agent responsibilities
- Type-Driven Development – Types provide contracts between sub-agents working on shared interfaces
- Making Invalid States Impossible – Tool access restrictions prevent sub-agents from reaching invalid states
- Invariants in Programming and LLM Generation – Role-specific invariants constrain what each sub-agent can do
- Hierarchical Context Patterns – Three-layer context hierarchy enables agent specialization
- Actor-Critic Adversarial Coding – Code Reviewer acts as critic, implementation agents as actors
- Plan Mode Strategic – Human-in-loop planning for complex tasks
- Layered Prompts Architecture – Structuring prompts for specialized agents
- MCP Server for Dynamic Project Context – Provide sub-agents with queryable, on-demand context
- 24/7 Development Strategy – Sub-agents enable autonomous night shifts with dedicated QA and review agents
- Building the Factory – Sub-agent architecture as meta-infrastructure; specialized agents are tools that generate code
- Git Worktrees for Parallel Development – Run multiple sub-agents in parallel worktrees for simultaneous feature development
- Model Switching Strategy – Assign different model tiers to specialized agents: Haiku for simple read/search tasks, Sonnet for implementation, Opus for architecture decisions
References
- Claude Code Agent Documentation – Official documentation for building AI agents with Claude
- Multi-Agent Systems in Software Engineering – Research paper on multi-agent collaboration for code generation

