Advanced Prompting Techniques

45 min advanced Lesson 8

Learning Outcomes

  • Use the think/plan/execute pattern for complex tasks
  • Write effective constraints that shape AI output
  • Structure multi-step instructions for reliable execution
  • Know when to be specific vs when to give AI freedom
  • Combine prompting techniques for maximum effectiveness

Lesson Plan

Segment Duration Topic
Intro 3 min Beyond basic prompting
Demo 10 min Think/plan/execute pattern
Explain 7 min Effective constraints
Demo 10 min Multi-step structured instructions
Explain 7 min Specificity spectrum
Demo 5 min Combining techniques
Wrap-up 3 min Key takeaways

Before You Begin

Pre-work:

  • Complete Lesson 7
  • Review Subject 4 lesson outlines for deeper prompt engineering coverage

Shopping List:

  • Claude Code installed
  • A complex project (something with multiple features and layers)
  • Familiarity with the basic prompting from lessons 1-7

1 The Think/Plan/Execute Pattern

For complex tasks, explicitly separate thinking from doing:

Without the pattern:

> Add pagination to the user list API and frontend

Claude jumps straight to implementation — might miss considerations.

With the pattern:

> I want to add pagination to the user list. 
> 
> THINK: What are the key decisions we need to make? 
> (cursor vs offset pagination, page size, URL params, etc.)
> 
> PLAN: Based on those decisions, outline your implementation plan.
> Which files change? What's the approach?
> 
> Don't implement yet — just think and plan.

What Claude's THINK response looks like:

## Key Decisions

1. **Pagination style:** Cursor-based vs offset?
   - Offset: simpler, but breaks with concurrent inserts/deletes
   - Cursor: more complex, but stable across mutations
   - Recommendation: cursor-based (encoded timestamp + id)

2. **Page size:** Fixed or configurable?
   - Suggest: default 10, allow override via query param (max 100)

3. **URL structure:**
   - GET /api/users?cursor=abc123&limit=20
   - Response includes: data[], nextCursor, hasMore

4. **Frontend state:**
   - Need to track cursor, loading state, hasMore
   - "Load more" button vs infinite scroll?

## Plan
Files to create/modify:
- src/types/pagination.ts (shared cursor types)
- src/api/users.ts (add cursor params, return pagination meta)
- src/hooks/usePaginatedList.ts (new generic hook)
- src/components/UserList.tsx (add load more button)

You review this, then steer:

> Good plan. Two changes:
> - Use cursor-based pagination (not offset)
> - Page size should be 20, not 10
> 
> Now EXECUTE the plan.

Why this works:

  • Catches wrong assumptions before code is written
  • Gives you a review checkpoint
  • Produces more thoughtful implementations
  • Handles complex tasks more reliably

The think step is especially valuable when there's no single "right" answer. Without it, Claude picks an approach silently — and you might not discover the mismatch until you've reviewed 200 lines of code.

TIP
Tip
Use think/plan/execute for any task that spans 3+ files or has multiple valid approaches. For simple single-file tasks, just ask directly.
NOTE
Key Insight
The THINK step often reveals decisions you hadn't considered. 'Should the cursor be an opaque token or a visible timestamp?' — questions like this are better answered before code is written than during review.

2 Effective Constraints

Constraints prevent Claude from going down wrong paths:

Positive constraints (what TO do):

> Requirements:
> - Use our existing Button component from src/components/ui/Button
> - Follow the same data fetching pattern as src/hooks/useUsers.ts
> - All API calls must go through the httpClient in src/lib/http.ts
> - Include loading and error states

Negative constraints (what NOT to do):

> Constraints:
> - Do NOT add any new dependencies
> - Do NOT use inline styles — use Tailwind classes only
> - Do NOT use useEffect for data fetching — use React Query
> - Do NOT modify any existing test files

Boundary constraints:

> Scope: Only modify files in src/features/notifications/
> Do not touch shared components or the API layer.

Quality constraints:

> - Every function must have TypeScript types (no `any`)
> - Maximum function length: 30 lines
> - Every new function needs a unit test

The power of constraints: They're faster to write than detailed instructions and prevent the most common AI mistakes. A good set of constraints can be reused across many prompts.

Reusable constraints in CLAUDE.md:

If you find yourself repeating constraints, put them in your project's CLAUDE.md file:

## Code Constraints
- No new npm dependencies without explicit approval
- All API endpoints require auth middleware
- No inline styles — use Tailwind utility classes
- Functions over 20 lines should be extracted
- All public functions need JSDoc comments

Now every Claude session automatically respects these constraints without you typing them each time. Your prompt becomes just the task:

> Add a password reset endpoint

Claude will automatically use auth middleware, Tailwind for any UI, and keep functions short — because those constraints are in the project context.

WARNING
Watch Out
Don't over-constrain. If you specify every detail, Claude has no room to apply best practices you might not have thought of. Constrain the boundaries, not every step.
TIP
Tip
Negative constraints (DO NOT) are more powerful than positive constraints for preventing AI mistakes. Claude tends to follow 'do not use inline styles' more reliably than 'always use Tailwind classes.'

3 Multi-Step Instructions

For complex tasks, numbered steps execute more reliably than paragraphs:

Paragraph style (unreliable for complex tasks):

> Add a notification system. Users should be able to receive 
> notifications that are stored in the database and shown in 
> a dropdown in the header. They should be able to mark them 
> as read and we need real-time updates with WebSockets.

Numbered steps (reliable):

> Add a notification system. Execute these steps in order:
> 
> 1. Create the Notification type in src/types/notification.ts
>    Fields: id, userId, type (info|warning|error), title, body, 
>    read (boolean), createdAt
> 
> 2. Create the database migration for a notifications table
>    matching the type above
> 
> 3. Create API endpoints in src/api/notifications.ts:
>    - GET /api/notifications (list for current user, unread first)
>    - PATCH /api/notifications/:id/read (mark as read)
>    - DELETE /api/notifications/:id
> 
> 4. Create a useNotifications hook in src/hooks/ that:
>    - Fetches notifications on mount
>    - Provides markAsRead and deleteNotification functions
>    - Exposes unreadCount
> 
> 5. Create NotificationDropdown component in src/components/
>    - Bell icon with unread count badge
>    - Dropdown showing recent notifications
>    - "Mark all as read" button
> 
> 6. Add the dropdown to the Header component

Why numbered steps work better:

  • Each step has a clear completion criteria
  • Claude can reference step numbers ("step 3 depends on step 1's types")
  • You can approve step by step if needed
  • Missed steps are obvious

When paragraphs fail — a real example:

Compare these two prompts for the same task:

Paragraph style:

> I need a search feature that indexes all products, supports 
> fuzzy matching, shows results in a dropdown as the user types 
> with debouncing, highlights the matching text, and navigates 
> to the product page on click.

Claude might implement 4 out of 5 requirements because natural language makes it easy to lose track of items buried mid-sentence.

Numbered style:

> Add product search with autocomplete. Steps:
> 
> 1. Create a search index utility (src/lib/search.ts) that:
>    - Indexes product name, description, and category
>    - Supports fuzzy matching (Levenshtein distance ≤ 2)
> 
> 2. Create a useSearch hook (src/hooks/useSearch.ts) that:
>    - Debounces input by 300ms
>    - Returns matched products with highlighted match regions
> 
> 3. Create SearchDropdown component (src/components/SearchDropdown.tsx):
>    - Shows results below the input
>    - Highlights matching text in bold
>    - Navigates to /products/:id on click
>    - Dismisses on Escape or click outside

Every requirement is accounted for. Nothing gets buried. Claude can tick through them systematically.

TIP
Tip
Order steps by dependency: types → database → API → hooks → components → integration. Each step can reference what was created in previous steps.

4 The Specificity Spectrum

Not every prompt needs maximum detail. Match specificity to the task:

Low specificity (give Claude freedom):

> Write tests for the UserService class. Cover the main paths.

Good when: Claude knows your testing patterns and you trust its judgement.

Medium specificity (guide the approach):

> Write tests for UserService.createUser(). Test:
> - Successful creation
> - Duplicate email rejection
> - Invalid input validation
> Use vitest and our factory helper in tests/helpers/.

Good for: most day-to-day work.

High specificity (control the output):

> Write a test for UserService.createUser() that:
> - Uses the userFactory.build() helper to create test data
> - Mocks the database with vi.mock('../db')
> - Asserts that the returned user has an id and createdAt
> - Asserts that the password is NOT in the returned object
> - Uses the existing testDb helper for cleanup
> - Follows the Arrange/Act/Assert pattern

Good when: specific patterns are required and Claude doesn't know them.

When to be specific:

  • Security-sensitive code (auth, crypto, permissions)
  • When Claude has gotten it wrong before
  • When there's a non-obvious project convention
  • When the task has specific acceptance criteria

When to give freedom:

  • Standard CRUD operations
  • Tests for existing code
  • Boilerplate and scaffolding
  • Tasks where Claude consistently produces good output

5 Combining Techniques

The most effective prompts combine multiple techniques:

Example — complete structured prompt:

> ## Task
> Add rate limiting to all API endpoints.
> 
> ## Context
> - Express.js API in src/api/
> - Currently no rate limiting exists
> - We use Redis for caching (src/lib/redis.ts)
> 
> ## Think First
> What rate limiting strategy makes sense for our API? Consider:
> - Different limits for auth vs public endpoints
> - Per-user vs per-IP limiting
> - Which endpoints need stricter limits
> 
> ## Plan
> After thinking, outline which files to create/modify.
> 
> ## Constraints
> - Use our existing Redis connection (don't add new dependencies)
> - Auth endpoints: 5 requests/minute per IP
> - Regular endpoints: 60 requests/minute per user
> - Return 429 status with Retry-After header
> - Do NOT modify existing test files
> 
> ## Execute
> After I approve the plan, implement it.

This prompt:

  • States the task clearly (what)
  • Provides context (where we are)
  • Asks for thinking first (quality gate)
  • Includes specific constraints (boundaries)
  • Has a clear execution trigger (when to code)

Full worked example — watching the combined approach in action:

Here's how the rate limiting prompt plays out across the full conversation:

Claude's THINK response:

## Rate Limiting Strategy

For our Express API with Redis, I'd recommend a sliding window 
approach using sorted sets. Here's my reasoning:

- Auth endpoints (login, register, reset-password): 5 req/min per 
  IP to prevent brute force attacks
- Regular endpoints: 60 req/min per authenticated user (using 
  their user ID from the JWT)
- Public endpoints (health check, docs): no limit

Key consideration: we should use the existing Redis connection 
pool from src/lib/redis.ts rather than creating a new connection.

Claude's PLAN response:

## Files to Create
1. src/middleware/rateLimiter.ts — the middleware factory
2. src/middleware/rateLimiter.test.ts — unit tests

## Files to Modify  
3. src/api/auth.ts — apply strict limit to auth routes
4. src/api/index.ts — apply standard limit to all other routes

## Approach
- Create a configurable middleware factory: createRateLimiter(options)
- Two presets: authLimiter (5/min/IP) and apiLimiter (60/min/user)
- Store request counts in Redis sorted sets (timestamp as score)
- Clean expired entries on each request
- Return 429 with Retry-After header when exceeded

You approve with one adjustment:

> Good plan. One change: also add a way to skip rate limiting 
> in test environments (check NODE_ENV). Proceed with execution.

Claude's EXECUTE phase then produces all the code, following the plan exactly and incorporating your adjustment.

The entire interaction took 3 messages: your structured prompt, Claude's think+plan, and your approval. Without the combined approach, this same task might take 6-8 messages of back-and-forth as Claude asks clarifying questions and you redirect its choices.

NOTE
Key Insight
You don't need all these sections every time. The template is a toolkit — use the parts that help for each specific task. Simple tasks need simple prompts.
TIP
Tip
Save your best structured prompts as templates. A 'new feature' template, a 'refactor' template, and a 'debug' template cover 80% of complex tasks. Copy, fill in the blanks, and go.

Questions & Answers

Q: Don't detailed prompts take too long to write?
A 60-second prompt that gets it right on the first try is faster than a 5-second prompt that needs 4 rounds of correction. Invest in the prompt proportional to the task complexity.
Q: Should I save my structured prompts?
Yes — especially reusable ones. If you find yourself writing similar prompts repeatedly, save them as templates in a prompts/ folder or as Claude custom commands.
Q: When is a prompt too structured?
When it's longer than the code it produces. Or when you're specifying implementation details so precisely that you might as well write the code yourself. Prompts should describe WHAT and constraints, not line-by-line implementation.
Q: Does the think/plan/execute pattern waste tokens on planning?
The planning "cost" is 50-100 tokens. A wrong implementation that needs redoing costs 500-2000 tokens. Planning pays for itself on any task with more than one valid approach. Skip it only for trivial, single-file changes where there's obviously one right answer.
Q: How do I know if my constraints are working?
Look at Claude's output. If it's violating your constraints, they may be too vague or contradictory. If it's following them but the code feels over-constrained (too many small functions, unnecessary abstractions), you've been too specific. Good constraints eliminate bad paths without dictating the solution.

Key Takeaways

  1. Think/Plan/Execute — separate reasoning from implementation for complex tasks
  2. Constraints prevent mistakes — faster to write than detailed instructions
  3. Numbered steps are more reliable than paragraphs for multi-step work
  4. Match specificity to risk — more detail for critical code, less for boilerplate
  5. Combine techniques — task + context + constraints + think-first = best results
  6. Save and reuse — good prompt structures are templates for future tasks

Next Steps: In Lesson 9 — Large Project Management, you'll learn techniques for working with large codebases where context management becomes critical.