Skip to content

Explore-Plan-Code-Commit

A structured 4-phase workflow for tackling any development task with Claude Code -- from bug fixes to new features.

The Explore-Plan-Code-Commit (EPCC) workflow is the single most important pattern for working with Claude Code. It prevents the most common mistake engineers make: jumping straight to implementation before understanding the context.

1. Explore — understand the existing code
2. Plan — have Claude propose an approach before writing code
3. Code — implement in focused steps
4. Commit — review the diff and commit with a clear message

This works because it prevents Claude from generating code that conflicts with existing patterns, solves the wrong problem, or introduces unnecessary complexity.


Why This Workflow Matters

Without EPCC, a prompt like “add rate limiting” produces generic middleware. Claude does not know your traffic patterns, existing infrastructure, or team preferences. The result is code you have to rewrite.

With EPCC, Claude first understands your Express middleware chain, discovers you have no Redis, and learns your config file conventions. Then it proposes an approach you can evaluate before a single line of code is written.

The investment is small: 2-3 minutes of exploration and planning saves 20-30 minutes of rework.


Phase 1: Explore

The goal is to give Claude (and yourself) a shared understanding of the current state before proposing changes.

When to Explore

  • Starting any non-trivial task (more than a one-line change)
  • Working in unfamiliar code
  • Before proposing an architecture change
  • Before fixing a bug (understand the code path first)

Example Prompts

For a new feature:

Read the files in src/api/ and explain how endpoints are structured
in this project. What patterns do existing endpoints follow for
validation, error handling, and response format?

For a bug fix:

Read the order processing code in src/services/. I have a reported bug:
when a coupon is applied and then the cart quantity changes, the discount
calculates incorrectly. Trace the code path and explain where the
discount is calculated relative to quantity changes.

For understanding infrastructure:

Before we add rate limiting, I need to understand our current API setup.
Look at three things in parallel:
1. How requests are routed -- the middleware chain in our Express app
2. Whether we have any existing rate limiting or throttling anywhere
3. Our infrastructure -- nginx config, Docker setup, load balancer config

Use subagents to investigate these simultaneously.

Tips for the Explore Phase

  • Use numbered lists of specific areas. Vague prompts like “explore the codebase” produce vague results. “Look at these 3 specific things” produces focused answers.
  • Constrain the depth. “Don’t go deep yet — I want the big picture in under 2 minutes of reading” prevents Claude from dumping 500 lines of analysis.
  • Reference specific files and directories. @src/services/ is better than “the services.”
  • Use subagents for parallel investigation. When you have 3+ independent areas to explore, subagents investigate simultaneously instead of serially.

Common Pitfall

Skipping exploration. “I know this code already” is how you end up solving the right problem in a way that conflicts with existing patterns. Five minutes of exploration saves thirty minutes of rework.


Phase 2: Plan

Have Claude propose an approach before writing any code. This is your chance to catch misunderstandings, push back on over-engineering, and set constraints.

When to Plan

  • Any task that touches more than one file
  • Features with multiple viable approaches
  • Refactoring where you need to choose which pattern to apply
  • Architecture decisions

Example Prompts

For an API feature:

I need a new endpoint: POST /api/orders/:id/refund
- Accepts: { reason: string, amount?: number }
- If amount is omitted, refund the full order total
- Only orders with status "delivered" can be refunded
- Returns the updated order with status "refunded"

Based on how the existing endpoints work, what's your plan for
implementing this? Don't write code yet -- just outline the approach.

For a feature with multiple approaches:

I need to add CSV export to the admin tasks table. Based on the
existing code, what's your recommended approach? Should I:
a) Generate the CSV on the frontend from the table data
b) Add a backend endpoint that streams the CSV
c) Something else?

Consider: the table can have up to 50,000 rows.

For architecture decisions with tradeoffs:

Given our setup (Express behind Nginx, no existing rate limiting), propose
3 approaches to rate limiting with tradeoffs. Consider:
- Implementation effort for a team of 5
- Operational complexity (what breaks at 3am?)
- Flexibility for different rate limits per endpoint
- How we'd monitor and alert on rate limiting
- Can we do it without adding Redis?

Pushing Back on the Plan

If Claude’s plan misses something, correct it before implementation:

Good plan, but we actually handle refunds through a separate RefundService,
not directly in the order handler. Adjust the plan to use that service.

Tips for the Plan Phase

  • Include real constraints. Team size, infrastructure, no-go technologies. The more constraints you give, the more targeted the response.
  • Ask “what makes this hard?” before “how should we do it?” This prevents Claude from glossing over difficult parts.
  • Request the plan in a specific format. For architecture decisions, ask for an ADR. For features, ask for a numbered step list.

Common Pitfall

Accepting the first plan without scrutiny. Claude defaults to positive framing. Ask “what could go wrong with this approach?” or “be critical” to get a more honest assessment.


Phase 3: Code

Implement the agreed-upon plan in focused, reviewable steps. Never ask Claude to build everything at once.

When to Stage

Always. Even small features benefit from staged implementation:

  1. Core logic first, verify it works
  2. Edge cases and error handling
  3. Tests
  4. Documentation (if needed)

Example Prompts

Staged implementation:

Option B is right for now. Let's implement in stages. Don't do everything
at once -- I want to review each stage before moving to the next.

Stage 1: Install express-rate-limit and add a global rate limiter as the
first middleware. 100 requests per 15-minute window per IP. Return
standard rate limit headers.

Start with stage 1 only.

Building on stage feedback:

Looks good. Two changes: move the rate limit config values to our config
file instead of hardcoding them. And add a log line when someone gets
rate limited -- we need to know if this is happening.

Next stage:

Stage 2: Add per-route rate limiting. The login endpoint should be
stricter -- 5 attempts per 15 minutes. The search endpoint should be
more permissive -- 200 per 15 minutes. Everything else keeps the
global default.

Tips for the Code Phase

  • Review 10-20 lines at a time, not 200. If the diff is too large to review, you asked for too much in one step.
  • Tell Claude which files to put code in. “Put the handler in src/api/orders.ts and the refund logic in src/services/RefundService.ts” prevents Claude from inventing its own file structure.
  • Reference existing patterns. “Follow the same pattern as the existing EmailNotifier and SmsNotifier classes” gets you consistent code.
  • Ask Claude to run tests after each stage. “Run the tests after” is your safety net.

Common Pitfall

Accepting code without reading the diff. Claude generates plausible code that may have subtle bugs, missing edge cases, or unasked-for changes. Always check what actually changed.


Phase 4: Commit

Review the complete set of changes and commit with a descriptive message.

Example Prompts

Self-review before committing:

Review all the changes we've made in this session. Check for:
- Security issues (can the rate limit be bypassed?)
- Edge cases we missed
- Performance concerns with the in-memory store
- Anything that doesn't follow our existing code patterns

Be critical. I'd rather fix things now than in code review.

Viewing the diff:

Show me the complete diff for all changes.

Committing:

Commit this with message "fix: restore DISTINCT in search queries to
prevent duplicate results" and reference PR #247 in the body.

Tips for the Commit Phase

  • Ask Claude to review its own work. Claude in “review mode” applies different heuristics than Claude in “build mode.” It finds issues it missed during implementation.
  • Phrase the review as “be critical.” Claude defaults to positive framing. Explicit permission to find problems produces better reviews.
  • Reference the original context in commit messages. If fixing a regression, reference the PR that introduced it. This creates a paper trail.

Common Pitfall

Skipping the self-review. Asking Claude to critique what it just produced is consistently one of the highest-value prompts you can write.


Complete Walkthrough: Building Rate Limiting from Scratch

This walkthrough shows the full EPCC cycle applied to a real feature.

Scenario: Product has requested rate limiting on the API. You need to research the options, decide on an approach, implement it, and document it.

Explore

You type:

Before we add rate limiting, I need to understand our current API setup.
Look at three things in parallel:
1. How requests are routed -- the middleware chain in our Express app
2. Whether we have any existing rate limiting or throttling anywhere
3. Our infrastructure -- nginx config, Docker setup, load balancer config

Use subagents to investigate these simultaneously.

Claude does:

  • Launches 3 parallel subagents
  • Subagent 1 maps the middleware chain: cors -> bodyParser -> requestLogger -> authCheck -> router. No rate limiting anywhere.
  • Subagent 2 searches the entire codebase for “rate”, “limit”, “throttle”, “429” — finds nothing except a TODO comment
  • Subagent 3 reads nginx.conf, Dockerfile, docker-compose.yml — finds Nginx as a reverse proxy with no rate limiting directives
  • Synthesizes: “No rate limiting exists at any layer.”

Plan

You type:

Given our setup (Express behind Nginx, no existing rate limiting), propose
3 approaches to rate limiting with tradeoffs. Consider:
- Implementation effort for a team of 5
- Operational complexity (what breaks at 3am?)
- Flexibility for different rate limits per endpoint
- How we'd monitor and alert on rate limiting
- Can we do it without adding Redis?

Claude produces a full ADR with 3 options and recommends the Express middleware approach with in-memory store for now, with a migration path to Redis when the team scales to multiple instances.

Code (Staged)

Stage 1 — Global limiter:

Option B is right for now. Let's implement in stages. Don't do everything
at once -- I want to review each stage before moving to the next.

Stage 1: Install express-rate-limit and add a global rate limiter as the
first middleware. 100 requests per 15-minute window per IP. Return
standard rate limit headers.

Claude installs the package, creates the middleware, wires it into the server, shows the diff. You review.

Refinement:

Looks good. Two changes: move the rate limit config values to our config
file instead of hardcoding them. And add a log line when someone gets
rate limited.

Stage 2 — Per-route limits:

Stage 2: Add per-route rate limiting. The login endpoint should be
stricter -- 5 attempts per 15 minutes. The search endpoint should be
more permissive -- 200 per 15 minutes.

Stage 3 — Tests:

Stage 3: Write tests for the rate limiting. I need tests that verify:
1. Normal requests below the limit work fine
2. Requests above the limit get 429
3. The rate limit headers are present in every response
4. The login endpoint has a stricter limit than the default
5. Rate limits reset after the window expires

Commit (with Self-Review)

You type:

Review all the changes we've made in this session. Check for:
- Security issues (can the rate limit be bypassed?)
- Edge cases we missed
- Performance concerns with the in-memory store
- Anything that doesn't follow our existing code patterns

Be critical. I'd rather fix things now than in code review.

Claude identifies 3 issues:

  1. The in-memory store does not handle X-Forwarded-For correctly when behind Nginx — it is rate limiting Nginx’s IP, not the client’s
  2. The store is not shared between worker processes if the app uses cluster mode
  3. The rate limit warning log should include the request path, not just the IP

You fix all 3 before committing.


Quick Reference: EPCC Prompts by Task Type

New endpoint

Explore: "Read the files in src/api/ and explain the endpoint patterns."
Plan:    "I need a POST /api/orders/:id/refund. What's your plan?"
Code:    "Implement the refund endpoint following our plan."
Commit:  "Show me the diff, then commit."

New component

Explore: "Read src/components/ and explain the component conventions."
Plan:    "I need a SearchableDropdown. Here are the requirements: [list]"
Code:    "Build it following existing patterns. Write tests."
Commit:  "Review for edge cases, then commit."

Adding to existing code

Explore: "Read @src/services/NotificationService.java. What channels exist?"
Plan:    "I need to add Slack. Follow the same pattern as Email and SMS."
Code:    "Implement Slack notifications. Include unit tests."
Commit:  "Run all tests, show the diff, commit."

Bug fix

Explore: "Check git log for what changed Monday. Trace the code path."
Plan:    "Before fixing, verify: are there tests? Are other endpoints affected?"
Code:    "Fix all affected files. Write a regression test."
Commit:  "Commit referencing the original PR in the body."

Common Patterns Across All EPCC Workflows

Always start by reading existing code

Don’t jump to implementation. Ask Claude to explore first.

Give Claude the “why,” not just the “what”

“Add logging” is less useful than “Add logging because we can’t diagnose payment failures in production.”

Use incremental steps

Big changes should be broken into small steps with verification at each step.

Let Claude run the tests

After every change: “Run the tests.” This is your safety net.

Review before committing

“Show me the diff” is your last checkpoint before committing.

The Prompt Checklist

Before asking Claude to build a feature, make sure your prompt includes:

  • What the feature does (in specific, testable terms)
  • Where the code goes (which files, directories, modules)
  • Constraints (follow existing patterns, use specific libraries, no external deps)
  • Edge cases (what happens with empty input, missing data, errors?)
  • Verification (run tests, build, or describe how to manually verify)