Sub-Agent Architecture: Specialized Agents for Higher Quality Code

James Phoenix
James Phoenix

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:

Udemy Bestseller

Learn Prompt Engineering

My O'Reilly book adapted for hands-on learning. Build production-ready prompts with practical exercises.

4.5/5 rating
306,000+ learners
View Course

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 };
}
  1. Add validation

    • Define Zod schema in packages/api/src/schemas/users.ts
    • Validate at API boundary (route layer)
  2. Handle errors

    • Use Result pattern, no exceptions
    • Return descriptive error messages
    • Log errors for debugging
  3. 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 workflow
  • packages/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 workflow
  • packages/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
   );
  1. 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

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

References

Topics
Agent ArchitectureAgent RolesCode QualityDivision Of LaborHierarchical AgentsOrchestrationSpecialized AgentsSub AgentsTeam SimulationTool Access Control

More Insights

Cover Image for Thought Leaders

Thought Leaders

People to follow for compound engineering, context engineering, and AI agent development.

James Phoenix
James Phoenix
Cover Image for Systems Thinking & Observability

Systems Thinking & Observability

Software should be treated as a measurable dynamical system, not as a collection of features.

James Phoenix
James Phoenix