New May 26, 2026

Stop trying to one-shot: How to prompt Claude better

Company/Startup Blogs All from LogRocket Blog View Stop trying to one-shot: How to prompt Claude better on blog.logrocket.com

Claude can write code, explain architecture, generate tests, review pull requests, and draft documentation. But the quality of those outputs depends heavily on how you structure the work.

Stop trying to one-shot How to prompt Claude better

The common mistake is one-shot prompting: giving Claude one broad instruction, such as β€œbuild a scalable authentication system,” and expecting production-ready output. That approach usually produces generic code because Claude has to infer the stack, constraints, architecture, error handling strategy, and success criteria from a vague request.

A better approach is workflow-based prompting. Instead of asking Claude to solve the whole task in one pass, break the work into stages: define the problem, design the architecture, generate implementation, review the output, test failure cases, and refine the result. This mirrors how senior engineers already work, and it turns Claude from a code generator into a guided engineering collaborator.

This article explains how to replace one-shot prompts with structured Claude workflows for architecture planning, implementation, debugging, pull request review, documentation, testing, and automation. For a broader system-level view, see LogRocket’s guide to AI-assisted development governance.

What is one-shot prompting?

One-shot prompting is the practice of giving an AI tool a single broad instruction and expecting a complete, accurate, production-ready result. In software development, it looks like this:

Build a scalable authentication system in Node.js.

That prompt may produce something that looks useful, but it is doing too much at once. It does not define the framework, persistence layer, authentication model, session strategy, security constraints, testing expectations, deployment environment, or integration boundaries.

The result is usually the most statistically common answer, not the right answer for your codebase.

What one-shot prompting gets wrong

One-shot prompting fails because it hides the actual engineering work. The model has to guess at the missing pieces:

The output may look complete, but it often breaks down when you try to integrate it into a real application.

Common failure modes include:

Failure mode What happens Why it matters
Overgeneralized code Claude produces a generic implementation that ignores your stack The code may compile, but it will not fit your architecture
Hidden assumptions Claude silently chooses libraries, patterns, and data models Those choices can conflict with existing conventions
Shallow error handling Happy paths are covered, but failure modes are weak Production systems fail at the edges
Missing tests The output includes code but not enough validation Reviewers inherit the burden of proving correctness
Poor maintainability Everything lands in one or two files The result is harder to extend and review

One-shot prompting feels productive because it is fast. But fast, shallow output can create technical debt faster than writing the code yourself.

One-shot prompting vs. workflow-based prompting

The fix is not a longer one-shot prompt. The fix is a different mental model.

Treat Claude like a collaborator moving through a workflow, not like a function that should return an entire feature from one argument. Anthropic’s prompt engineering guidance emphasizes giving Claude context, defining the task clearly, and iterating on outputs. The same principle applies to engineering work: before asking Claude to build, define what β€œgood” means.

Dimension One-shot prompting Workflow-based prompting
Prompt shape One broad instruction A sequence of focused prompts
Context Minimal or implied Explicit stack, constraints, and goals
Output Large, generic answer Smaller, reviewable artifacts
Reviewability Hard to inspect because everything arrives at once Easier to review by stage
Architecture quality Often accidental Planned before implementation
Testing Often added after the fact Included as part of the workflow
Best use Small, disposable tasks Production code, debugging, documentation, and reviews

Workflow-based prompting is especially useful with Claude because it gives the model room to reason through structure, tradeoffs, and constraints before it writes code.

The structured Claude workflow

A reliable Claude workflow usually follows this pattern:

  1. Define the role and engineering context
  2. State the task clearly
  3. List requirements and constraints
  4. Ask for a plan before code
  5. Generate implementation in small sections
  6. Review the output against explicit criteria
  7. Ask for fixes, tests, and documentation

Here is a reusable prompt structure:

You are a senior [role].

Context: [Project stack, existing architecture, constraints, and relevant files]

Task: [Specific engineering task]

Requirements:

  • [Functional requirement]
  • [Security requirement]
  • [Performance requirement]
  • [Testing requirement]

Constraints:

  • [Framework/library/version constraints]
  • [Architecture constraints]
  • [What not to change]

Return:

  1. Proposed architecture
  2. Implementation plan
  3. Files to create or modify
  4. Code
  5. Tests
  6. Risks and tradeoffs

This prompt does three important things. It defines the model’s role, gives the model the information it needs to make better decisions, and forces the output into a reviewable structure.

Workflow 1: Use Claude for architecture planning before code

Claude is strongest when you ask it to reason about structure before implementation. For larger tasks, start with architecture, not code.

For example, instead of asking:

Build a payment webhook service.

Use a structured prompt:

You are a senior backend engineer.

Task: Design a Java Spring Boot service that processes payment webhooks.

Requirements:

  • Idempotency support
  • Retry handling
  • Event logging
  • Redis caching
  • REST endpoint
  • Unit and integration tests

Constraints:

  • Use PostgreSQL for durable event storage
  • Use Redis only for idempotency locks and caching
  • Do not expose JPA entities directly from controllers
  • Include security considerations for webhook signatures

Return:

  1. Folder structure
  2. Architecture overview
  3. Data model
  4. API contract
  5. Error handling strategy
  6. Testing plan
  7. Implementation risks

This produces a better first output because Claude has to separate architecture from implementation. You can review the structure before accepting any code.

A reasonable Spring Boot webhook service might start with this structure:

payment-webhook-service/
β”œβ”€β”€ pom.xml
└── src/
    β”œβ”€β”€ main/
    β”‚   β”œβ”€β”€ java/com/payments/webhook/
    β”‚   β”‚   β”œβ”€β”€ WebhookServiceApplication.java
    β”‚   β”‚   β”œβ”€β”€ config/
    β”‚   β”‚   β”‚   β”œβ”€β”€ RedisConfig.java
    β”‚   β”‚   β”‚   └── WebhookProperties.java
    β”‚   β”‚   β”œβ”€β”€ controller/
    β”‚   β”‚   β”‚   └── WebhookController.java
    β”‚   β”‚   β”œβ”€β”€ dto/
    β”‚   β”‚   β”‚   β”œβ”€β”€ ErrorResponse.java
    β”‚   β”‚   β”‚   β”œβ”€β”€ WebhookRequest.java
    β”‚   β”‚   β”‚   └── WebhookResponse.java
    β”‚   β”‚   β”œβ”€β”€ exception/
    β”‚   β”‚   β”œβ”€β”€ model/
    β”‚   β”‚   β”‚   └── WebhookEvent.java
    β”‚   β”‚   β”œβ”€β”€ repository/
    β”‚   β”‚   β”‚   └── WebhookEventRepository.java
    β”‚   β”‚   β”œβ”€β”€ service/
    β”‚   β”‚   β”‚   β”œβ”€β”€ EventLogService.java
    β”‚   β”‚   β”‚   β”œβ”€β”€ IdempotencyService.java
    β”‚   β”‚   β”‚   β”œβ”€β”€ PaymentEventHandler.java
    β”‚   β”‚   β”‚   └── WebhookProcessorService.java
    β”‚   β”‚   └── util/
    β”‚   β”‚       └── SignatureUtil.java
    β”‚   └── resources/
    β”‚       β”œβ”€β”€ application.yml
    β”‚       └── db/migration/
    β”‚           └── V1__create_webhook_events.sql
    └── test/

At this stage, the goal is not to accept the generated application. The goal is to create a draft architecture that you can interrogate.

Ask Claude to explain its design decisions

After Claude proposes the structure, ask it to defend the design:

Review this architecture before writing code.

Explain:

  1. Why each layer exists
  2. Which components should be interfaces
  3. Which responsibilities belong in each service
  4. Where idempotency should be enforced
  5. Which parts are risky in production
  6. What should be tested first

This moves the conversation from β€œgenerate code” to β€œjustify the system.” That is a much better place to start.

Workflow 2: Generate implementation in small, reviewable pieces

Once the architecture is clear, generate implementation one piece at a time. Do not ask Claude to build the controller, service, repository, DTOs, exceptions, migrations, and tests in one response.

Use staged prompts:

Implement only the webhook processing service.

Include:

  • Idempotency lookup
  • Event persistence
  • Retryable processing
  • Failure logging
  • Recovery behavior

Do not implement:

  • Controller code
  • Redis configuration
  • Database migration
  • Unit tests

Return:

  1. Code
  2. Assumptions
  3. Risks
  4. Tests to write next

The code below is intentionally substantial. It is the kind of output Claude may generate when it has clear requirements. The point is not that you should paste this directly into production. The point is that a structured prompt produces code with enough shape to review: you can inspect the service boundaries, retry strategy, idempotency flow, and failure states.

package com.payments.webhook.service;

import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.payments.webhook.dto.WebhookRequest; import com.payments.webhook.dto.WebhookResponse; import com.payments.webhook.exception.WebhookProcessingException; import com.payments.webhook.model.WebhookEvent; import com.payments.webhook.model.WebhookEvent.ProcessingStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service @RequiredArgsConstructor @Slf4j public class WebhookProcessorService {

private final IdempotencyService idempotencyService; private final EventLogService eventLogService; private final PaymentEventHandler paymentEventHandler; private final ObjectMapper objectMapper;

@Transactional public WebhookResponse ingest(String idempotencyKey, WebhookRequest request) { Optional<WebhookEvent> existing = eventLogService.findByIdempotencyKey(idempotencyKey); if (existing.isPresent()) { log.info("Duplicate webhook received key={}", idempotencyKey); return WebhookResponse.duplicate(existing.get()); }

if (!idempotencyService.tryAcquire(idempotencyKey)) { WebhookEvent raceWinner = eventLogService.findByIdempotencyKey(idempotencyKey) .orElseThrow(() -> new WebhookProcessingException( "Idempotency conflict on key: " + idempotencyKey)); return WebhookResponse.duplicate(raceWinner); }

WebhookEvent event = buildEvent(idempotencyKey, request); event = eventLogService.save(event);

processWithRetry(event.getId(), request);

return WebhookResponse.accepted(event); }

@Retryable( retryFor = WebhookProcessingException.class, maxAttemptsExpression = "#{${webhook.retry.max-attempts:3}}", backoff = @Backoff( delayExpression = "#{${webhook.retry.initial-interval-ms:1000}}", multiplierExpression = "#{${webhook.retry.multiplier:2.0}}", maxDelayExpression = "#{${webhook.retry.max-interval-ms:30000}}" ) ) public void processWithRetry(String eventId, WebhookRequest request) { log.info("Processing webhook eventId={} type={}", eventId, request.getEventType()); eventLogService.markProcessing(eventId);

try { paymentEventHandler.handle(request); eventLogService.markCompleted(eventId); log.info("Webhook processed successfully eventId={}", eventId); } catch (Exception ex) { String message = "Processing failed for eventId=%s: %s" .formatted(eventId, ex.getMessage()); eventLogService.markFailed(eventId, ex.getMessage()); throw new WebhookProcessingException(message, ex); } }

@Recover public void recover(WebhookProcessingException ex, String eventId, WebhookRequest request) { log.error("Retry attempts exhausted for eventId={}", eventId, ex);

eventLogService.findById(eventId) .ifPresent(event -> idempotencyService.release(event.getIdempotencyKey())); }

private WebhookEvent buildEvent(String idempotencyKey, WebhookRequest request) { String rawPayload; try { rawPayload = objectMapper.writeValueAsString(request); } catch (JsonProcessingException ex) { log.warn("Failed to serialize webhook payload; using fallback string", ex); rawPayload = request.toString(); }

return WebhookEvent.builder() .idempotencyKey(idempotencyKey) .eventType(request.getEventType()) .paymentId(request.getPaymentId()) .amount(request.getAmount()) .currency(request.getCurrency()) .status(ProcessingStatus.PENDING) .rawPayload(rawPayload) .attemptCount(0) .build(); } }

This is a useful draft, but it still needs review. In fact, the code contains a subtle production problem: processWithRetry() is called from inside the same class. With Spring AOP, annotations like @Retryable are applied through proxies, so self-invocation can bypass the retry behavior. That is exactly why a review stage matters.

Next, generate the persistence layer separately:

Now implement the event persistence layer.

Include:

  • WebhookEvent entity
  • WebhookEventRepository
  • EventLogService
  • Flyway migration

Constraints:

  • Do not expose the entity directly in API responses
  • Enforce a unique constraint on idempotency_key
  • Store only sanitized payload fields
  • Keep status transitions explicit

A generated migration might look like this:

-- V1__create_webhook_events.sql
CREATE TABLE webhook_events (
    id               VARCHAR(36)    NOT NULL,
    idempotency_key  VARCHAR(128)   NOT NULL,
    event_type       VARCHAR(64)    NOT NULL,
    payment_id       VARCHAR(64)    NOT NULL,
    amount           NUMERIC(18,2)  NOT NULL,
    currency         CHAR(3)        NOT NULL,
    status           VARCHAR(32)    NOT NULL DEFAULT 'PENDING',
    raw_payload      TEXT,
    attempt_count    INTEGER        NOT NULL DEFAULT 0,
    last_error       VARCHAR(1024),
    created_at       TIMESTAMPTZ    NOT NULL DEFAULT NOW(),
    updated_at       TIMESTAMPTZ    NOT NULL DEFAULT NOW(),
    processed_at     TIMESTAMPTZ,

CONSTRAINT pk_webhook_events PRIMARY KEY (id), CONSTRAINT uq_idempotency_key UNIQUE (idempotency_key), CONSTRAINT chk_status CHECK (status IN ( 'PENDING', 'PROCESSING', 'COMPLETED', 'FAILED' )), CONSTRAINT chk_amount_positive CHECK (amount > 0) );

CREATE INDEX idx_event_type_status ON webhook_events (event_type, status); CREATE INDEX idx_payment_id ON webhook_events (payment_id); CREATE INDEX idx_created_at ON webhook_events (created_at DESC); CREATE INDEX idx_status_updated ON webhook_events (status, updated_at) WHERE status IN ('PENDING', 'FAILED');

CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_webhook_events_updated_at BEFORE UPDATE ON webhook_events FOR EACH ROW EXECUTE FUNCTION set_updated_at();

Notice how this snippet gives reviewers something concrete to inspect. They can ask whether raw_payload should exist at all, whether DUPLICATE belongs in the persisted enum, whether the indexes match expected query patterns, and whether the idempotency key should be normalized before it reaches the database.

The same staged approach works for controllers. Ask Claude for the controller only after the service and persistence responsibilities are clear:

Implement only the Spring Boot controller for webhook ingestion.

Include:

  • POST /api/v1/webhooks
  • Idempotency-Key header validation
  • Webhook signature validation boundary
  • 202 response for accepted events
  • 200 response for duplicate events

Return only controller and DTO code.

Claude may generate code like this:

@PostMapping
public ResponseEntity<WebhookResponse> receive(
        @RequestHeader("Idempotency-Key") String idempotencyKey,
        @RequestHeader("X-Webhook-Signature") String signature,
        @RequestBody byte[] rawBody,
        @Valid @RequestBody WebhookRequest request) {

boolean valid = SignatureUtil.verify(rawBody, signature, properties.getSignature().getSecret()); if (!valid) { throw new InvalidSignatureException("Webhook signature verification failed"); }

WebhookResponse response = processor.ingest(idempotencyKey, request);

HttpStatus status = response.getStatus() == WebhookEvent.ProcessingStatus.DUPLICATE ? HttpStatus.OK : HttpStatus.ACCEPTED;

return ResponseEntity.status(status).body(response); }

This is useful to keep in the article because it shows both the benefit and risk of AI-generated code. The code looks plausible, but Spring cannot reliably bind two @RequestBody parameters from the same request body stream. A stronger design would move raw-body signature verification into a filter or request wrapper, then pass a validated DTO into the controller.

Workflow 3: Build an AI self-review loop

A good Claude workflow includes a review stage before human review. After implementation, ask Claude to audit its own output against specific engineering standards.

Review the generated code against the following criteria:
  1. SOLID principles
  2. Security risks
  3. Performance bottlenecks
  4. Clean architecture boundaries
  5. Testability
  6. Production readiness

For each issue:

  • Assign severity: Critical, High, Medium, or Low
  • Explain why it matters
  • Provide a concrete fix

This prompt often surfaces the exact issues you would otherwise have to find manually. That matters because AI-assisted development often shifts work from writing code to reviewing generated code. LogRocket has covered this pattern in more detail in why AI coding tools shift the real bottleneck to review.

For the webhook example, a useful review may look like this:

Severity Issue Why it matters Recommended fix
Critical @Retryable is called from the same class via this Spring AOP proxies do not intercept self-invocation, so retry may not run Move retry logic into a separate service
Critical Two @RequestBody parameters in one controller method The request body stream can be consumed only once Buffer raw body in a filter or request wrapper
High JPA entity returned directly from REST endpoints Persistence model leaks into API contract Map to response DTOs
Medium Weak default webhook secret App can start with a known fallback secret Require the secret through environment configuration
Medium Raw webhook payload stored unredacted Payment payloads may include sensitive data Persist a sanitized projection
Medium Idempotency key used directly in Redis key Unvalidated input can pollute key namespaces Validate and sanitize key format
Low Event type dispatch uses a switch statement Adding event types requires modifying the dispatcher Use a handler registry or strategy pattern

After the review, ask Claude to apply the highest-priority architectural fix first:

Apply the critical retry fix only.

Refactor the code so retry behavior lives in a separate Spring bean. Do not change the controller, DTOs, or database schema. Return:

  1. New class
  2. Modified service
  3. Explanation of why this fixes Spring AOP self-invocation

That prompt produces a smaller and safer change:

@Service
@RequiredArgsConstructor
@Slf4j
public class WebhookRetryExecutor {

private final EventLogService eventLogService; private final PaymentEventHandler paymentEventHandler; private final IdempotencyService idempotencyService;

@Retryable( retryFor = WebhookProcessingException.class, maxAttemptsExpression = "#{${webhook.retry.max-attempts:3}}", backoff = @Backoff( delayExpression = "#{${webhook.retry.initial-interval-ms:1000}}", multiplierExpression = "#{${webhook.retry.multiplier:2.0}}", maxDelayExpression = "#{${webhook.retry.max-interval-ms:30000}}" ) ) public void processWithRetry(String eventId, WebhookRequest request) { eventLogService.markProcessing(eventId);

try { paymentEventHandler.handle(request); eventLogService.markCompleted(eventId); } catch (Exception ex) { eventLogService.markFailed(eventId, ex.getMessage()); throw new WebhookProcessingException( "Processing failed for eventId=%s".formatted(eventId), ex); } }

@Recover public void recover(WebhookProcessingException ex, String eventId, WebhookRequest request) { log.error("All retries exhausted eventId={}", eventId, ex); eventLogService.findById(eventId) .ifPresent(event -> idempotencyService.release(event.getIdempotencyKey())); } }

Then the processor depends on the retry executor instead of invoking its own annotated method:

@Service
@RequiredArgsConstructor
@Slf4j
public class WebhookProcessorService {

private final IdempotencyService idempotencyService; private final EventLogService eventLogService; private final WebhookRetryExecutor retryExecutor; private final ObjectMapper objectMapper;

@Transactional public WebhookResponse ingest(String idempotencyKey, WebhookRequest request) { Optional<WebhookEvent> existing = eventLogService.findByIdempotencyKey(idempotencyKey); if (existing.isPresent()) { return WebhookResponse.duplicate(existing.get()); }

if (!idempotencyService.tryAcquire(idempotencyKey)) { return eventLogService.findByIdempotencyKey(idempotencyKey) .map(WebhookResponse::duplicate) .orElseThrow(() -> new WebhookProcessingException( "Idempotency conflict: " + idempotencyKey)); }

WebhookEvent event = eventLogService.save(buildEvent(idempotencyKey, request)); retryExecutor.processWithRetry(event.getId(), request); return WebhookResponse.accepted(event); }

private WebhookEvent buildEvent(String idempotencyKey, WebhookRequest request) { String rawPayload; try { rawPayload = objectMapper.writeValueAsString(request); } catch (JsonProcessingException ex) { rawPayload = request.toString(); }

return WebhookEvent.builder() .idempotencyKey(idempotencyKey) .eventType(request.getEventType()) .paymentId(request.getPaymentId()) .amount(request.getAmount()) .currency(request.getCurrency()) .status(ProcessingStatus.PENDING) .rawPayload(rawPayload) .attemptCount(0) .build(); } }

This is the workflow loop in action: generate code, review it, isolate one issue, apply one fix, and review again.

Use a severity table to make the review actionable

Ask Claude to format review findings as a table. Tables are easier to scan and easier to convert into PR tasks.

Return the review as a table with these columns:
  • Severity
  • File or component
  • Issue
  • Why it matters
  • Recommended fix
  • Should block merge? yes/no

This turns a vague AI review into an engineering checklist.

Workflow 4: Use Claude for structured debugging

Debugging is one of the highest-value use cases for Claude, but only if you provide the right context.

Do not prompt it like this:

Fix this error.

Use a structured debugging prompt:

You are a senior backend engineer debugging a production issue.

Error: [paste error message]

Stack trace: [paste stack trace]

Relevant code: [paste code snippet]

Runtime context:

  • Framework:
  • Version:
  • Environment:
  • Recent changes:
  • Expected behavior:
  • Actual behavior:

Explain:

  1. Most likely root cause
  2. Evidence for that diagnosis
  3. Minimal fix
  4. Safer production fix
  5. Long-term refactor
  6. Test cases that would prevent recurrence

For the webhook example, the debugging target might be the self-invocation problem from the generated service:

@Transactional
public WebhookResponse ingest(String idempotencyKey, WebhookRequest request) {
    WebhookEvent event = eventLogService.save(buildEvent(idempotencyKey, request));
    processWithRetry(event.getId(), request);
    return WebhookResponse.accepted(event);
}

@Retryable(retryFor = WebhookProcessingException.class) public void processWithRetry(String eventId, WebhookRequest request) { eventLogService.markProcessing(eventId); paymentEventHandler.handle(request); eventLogService.markCompleted(eventId); }

A good Claude response should not only say β€œmove this to another service.” It should explain the cause, evidence, and prevention:

Question Strong answer
Root cause processWithRetry() is invoked from inside the same class, bypassing the Spring proxy
Minimal fix Move retry behavior to a separate injected service
Safer fix Add an integration test that proves retry actually occurs
Prevention Add architecture tests to prevent retry and transaction boundaries from being mixed casually

You can then ask Claude to generate the prevention test. For example, an ArchUnit-style guardrail might look like this:

@AnalyzeClasses(packages = "com.payments.webhook")
class ArchitectureTest {

@ArchTest static final ArchRule no_circular_dependencies = slices().matching("com.payments.webhook.(*)..") .should().beFreeOfCycles();

@ArchTest static final ArchRule retry_logic_lives_in_retry_executor = methods() .that().areAnnotatedWith(Retryable.class) .should().beDeclaredInClassesThat() .haveSimpleNameEndingWith("RetryExecutor") .because("retry boundaries should be explicit and invoked through Spring proxies"); }

This is where structured prompting becomes more valuable than quick patching. Claude is not just fixing one error; it is helping you convert a bug into a reusable guardrail.

For more on validating AI-generated code instead of accepting it at face value, see LogRocket’s guide to fixing AI-generated code.

Workflow 5: Use Claude for pull request review

Claude can help review pull requests, but a single β€œreview this PR” prompt is too broad. It usually returns a mix of useful comments, generic advice, and low-priority style feedback.

A better PR review workflow separates the review into passes:

  1. Correctness
  2. Performance
  3. Security
  4. Test coverage
  5. Readability and maintainability

Start by exporting the diff:

git diff main > pr_changes.txt

Then prompt Claude one pass at a time:

You are a staff engineer reviewing a pull request.

Review this diff for correctness only.

Focus on:

  • Broken behavior
  • Incorrect assumptions
  • Missing edge cases
  • Race conditions
  • API contract changes

Do not comment on style, naming, formatting, or performance unless it causes a correctness issue.

Return:

  1. Blocking issues
  2. Non-blocking issues
  3. Questions for the author
  4. Suggested tests

Then run a security pass:

Review the same diff for security risks.

Focus on:

  • Input validation
  • Authentication and authorization
  • Secret handling
  • Logging of sensitive data
  • Unsafe defaults
  • Dependency or supply-chain risks

Return only actionable findings.

Finally, run a maintainability pass:

Review the same diff for maintainability.

Focus on:

  • Over-engineering
  • Unclear abstractions
  • Duplicated logic
  • Poor names
  • Missing comments where intent is not obvious
  • Places where a future developer may misunderstand the code

This approach produces more useful feedback because each pass has a clear purpose. It also prevents minor style suggestions from distracting from correctness and security issues.

Generate a PR summary

Claude is also useful for PR descriptions. After reviewing the diff, ask:

Generate a pull request summary.

Include:

  • Problem statement
  • Implementation approach
  • Files or modules changed
  • Testing performed
  • Risks and rollback plan
  • Reviewer notes

A good output might look like this:

This PR adds idempotent payment webhook processing.

Changes:

  • Adds WebhookController for inbound provider events
  • Adds Redis-backed idempotency lock handling
  • Persists webhook lifecycle events in PostgreSQL
  • Adds retry handling for transient processing failures
  • Adds unit tests for idempotency, signature verification, and event logging

Testing:

  • Unit tests for duplicate webhook detection
  • Controller tests for invalid signatures and missing headers
  • Repository migration verified locally

Risks:

  • Signature verification depends on raw request body handling
  • Retry behavior should be verified with an integration test
  • rawPayload storage should be reviewed for PII before production

This can save time, but it should still be reviewed. PR summaries are communication artifacts, and they shape how reviewers understand the change.

Workflow 6: Use Claude to generate documentation

One-shot documentation prompts usually produce vague, generic explanations:

Write docs for this code.

That prompt does not define the audience, the docs format, or what the reader needs to do next.

Use a documentation workflow instead:

Generate developer documentation for this module.

Audience: Backend engineers who need to maintain or extend the service.

Include:

  1. What the module does
  2. Architecture overview
  3. Request flow
  4. API contract
  5. Configuration
  6. Error handling
  7. Operational risks
  8. Local development steps
  9. Examples

Then refine by audience:

Rewrite the documentation for an onboarding engineer.

Assume they understand Spring Boot, but they do not know this codebase.

Add:

  • Glossary
  • Common failure modes
  • Where to add a new webhook event type
  • How to run the tests locally

Generated documentation often describes what code does, but misses why it exists. Add this prompt:

Review the documentation and add design intent.

For each major component, explain:

  • Why it exists
  • What decision it protects
  • What tradeoff it makes
  • What a future developer should avoid changing casually

That produces documentation that is more useful for maintenance.

Workflow 7: Use Claude for test generation

Claude can generate useful tests, especially for deterministic functions, service methods, and controller behavior. But it often over-mocks or writes tests that assert implementation details instead of user-visible behavior.

Use a staged test workflow:

Generate a test plan before writing tests.

Code under test: [paste code]

Return:

  1. Happy-path scenarios
  2. Edge cases
  3. Failure modes
  4. Security-related tests
  5. Integration tests
  6. Tests that should not be written because they would be brittle

Then ask for one test class at a time. The example below is long enough to show the value of the workflow: it covers first acquisition, duplicate handling, null Redis responses, release behavior, and lock checks.

package com.payments.webhook.service;

import com.payments.webhook.config.WebhookProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class) @DisplayName("IdempotencyService") class IdempotencyServiceTest {

@Mock RedisTemplate<String, Object> redisTemplate; @Mock ValueOperations<String, Object> valueOps; @Mock WebhookProperties properties; @Mock WebhookProperties.Idempotency idempotencyProps;

@InjectMocks IdempotencyService idempotencyService;

private static final String KEY = "evt_abc123"; private static final String REDIS_KEY = "webhook:idempotency:" + KEY;

@BeforeEach void setUp() { when(properties.getIdempotency()).thenReturn(idempotencyProps); when(idempotencyProps.getTtlSeconds()).thenReturn(86400L); when(redisTemplate.opsForValue()).thenReturn(valueOps); }

@Test @DisplayName("tryAcquire returns true on first call") void tryAcquire_firstCall_returnsTrue() { when(valueOps.setIfAbsent(eq(REDIS_KEY), eq("1"), eq(Duration.ofSeconds(86400)))) .thenReturn(true);

assertThat(idempotencyService.tryAcquire(KEY)).isTrue(); verify(valueOps).setIfAbsent(REDIS_KEY, "1", Duration.ofSeconds(86400)); }

@Test @DisplayName("tryAcquire returns false when key already exists") void tryAcquire_duplicate_returnsFalse() { when(valueOps.setIfAbsent(eq(REDIS_KEY), eq("1"), any(Duration.class))) .thenReturn(false);

assertThat(idempotencyService.tryAcquire(KEY)).isFalse(); }

@Test @DisplayName("tryAcquire treats null Redis response as false") void tryAcquire_nullResponse_returnsFalse() { when(valueOps.setIfAbsent(any(), any(), any(Duration.class))).thenReturn(null);

assertThat(idempotencyService.tryAcquire(KEY)).isFalse(); }

@Test @DisplayName("release deletes the Redis key") void release_deletesKey() { idempotencyService.release(KEY);

verify(redisTemplate).delete(REDIS_KEY); }

@Test @DisplayName("isLocked returns true when key exists") void isLocked_keyExists_returnsTrue() { when(redisTemplate.hasKey(REDIS_KEY)).thenReturn(true);

assertThat(idempotencyService.isLocked(KEY)).isTrue(); }

@Test @DisplayName("isLocked returns false when key is absent") void isLocked_keyAbsent_returnsFalse() { when(redisTemplate.hasKey(REDIS_KEY)).thenReturn(false);

assertThat(idempotencyService.isLocked(KEY)).isFalse(); } }

After Claude generates tests, ask it to critique them:

Review the generated tests.

Identify:

  • False confidence risks
  • Over-mocking
  • Missing failure cases
  • Assertions that are too weak
  • Tests that duplicate implementation details

This is important because AI-generated tests can look thorough while still missing the behavior that matters. LogRocket’s experiment on replacing a test suite with AI agents shows the same general pattern: AI-generated tests can be useful, but they need careful review around assumptions, selectors, and failure modes.

Good test-generation targets

Claude is usually strongest for:

Use more caution with:

A good rule: let Claude draft tests, but make a human decide what confidence those tests actually provide.

Workflow 8: Use Claude for automation scripts

Claude is useful for small internal automation tasks, but one-shot prompts often produce scripts that work once and then become hard to reuse.

Instead of:

Write a script to automate dependency upgrades.

Use:

Design a reusable automation script.

Task: Scan a GitHub repository for outdated dependencies and create upgrade pull requests.

Requirements:

  • Support dry-run mode
  • Handle npm and Python dependencies
  • Respect GitHub API rate limits
  • Avoid duplicate PRs
  • Add structured logging
  • Fail safely without modifying files when credentials are missing

Return:

  1. Design overview
  2. CLI arguments
  3. Error handling strategy
  4. Implementation
  5. Example commands
  6. Risks and limitations

A strong automation output should explain not only what the script does, but how it behaves when something goes wrong.

For example, Claude might return usage instructions before the full script:

pip install requests packaging toml

Dry run first. No branches or PRs are created.

GITHUB_TOKEN=ghp_xxx GITHUB_REPO=acme/backend
python dep_upgrader.py --dry-run

Real run. Python dependencies only, limited to five PRs.

python dep_upgrader.py
--token ghp_xxx
--repo acme/backend
--ecosystem python
--limit 5
--label "dependencies,automated"

Then it should explain the design decisions:

Requirement Why it matters
Dry-run mode Lets teams test behavior safely
Idempotency Prevents duplicate branches or PRs
Rate-limit handling Avoids GitHub API failures
Structured logging Makes CI failures easier to diagnose
Clear CLI flags Makes the script reusable across teams
Safe credential handling Prevents accidental token exposure

Then ask Claude to review the script:

Review this script as if it will run in CI.

Focus on:

  • Secret handling
  • Idempotency
  • Rate limits
  • Error handling
  • Rollback behavior
  • Observability

Automation is where structured prompting matters most. A fragile one-off script can create more operational risk than the manual task it replaced.

Prompt templates for Claude workflows

Here are reusable prompt templates for common engineering workflows.

Architecture prompt

You are a senior software architect.

Design a solution for: [task]

Context: [stack, existing architecture, constraints]

Return:

  1. Architecture overview
  2. Data flow
  3. Components and responsibilities
  4. Interfaces
  5. Risks and tradeoffs
  6. Testing strategy
  7. What you need to know before implementation

Implementation prompt

Implement only this stage:
[scope]

Use: [stack, libraries, versions]

Constraints:

  • [constraint]
  • [constraint]

Do not:

  • [out-of-scope item]
  • [out-of-scope item]

Return:

  • Files changed
  • Code
  • Notes on assumptions
  • Tests to add next

Review prompt

Review this generated code.

Focus on:

  1. Correctness
  2. Security
  3. Performance
  4. Maintainability
  5. Architecture boundaries
  6. Tests

Return a severity-ranked table with concrete fixes.

Debugging prompt

Debug this issue.

Error: [paste error]

Relevant code: [paste code]

Context: [paste runtime details]

Return:

  1. Root cause
  2. Evidence
  3. Minimal fix
  4. Safer production fix
  5. Long-term prevention
  6. Tests

Documentation prompt

Generate developer documentation for this module.

Audience: [audience]

Include:

  1. Purpose
  2. Architecture
  3. Setup
  4. API contract
  5. Examples
  6. Failure modes
  7. Maintenance notes

Test prompt

Create a test plan for this code before writing tests.

Return:

  1. Behaviors to test
  2. Edge cases
  3. Failure modes
  4. Mocks required
  5. Tests that would be brittle
  6. Recommended test order

Best practices for prompting Claude

The best Claude prompts are not just longer. They are more constrained, easier to evaluate, and easier to iterate on.

Use these rules:

This is also where tools like Claude Code become more useful. LogRocket’s guide to leveling up Claude Code covers practical patterns like hooks, commands, and worktrees that can make Claude workflows more repeatable.

When not to use Claude

Claude is useful, but it should not replace engineering judgment.

Avoid relying on Claude alone for:

In these cases, Claude can still help with planning, review, and test generation. But a human should own the final architecture, risk assessment, and merge decision.

Conclusion

Better Claude output does not come from one perfect prompt. It comes from a better workflow.

One-shot prompting asks Claude to guess the task, architecture, constraints, tests, and tradeoffs all at once. Workflow-based prompting separates those steps so each output can be reviewed, improved, and tested before moving forward.

The practical shift is simple:

AI-assisted development is not about typing less. You have to turn engineering judgment into repeatable prompts, review steps, and guardrails. Claude becomes much more useful when you stop treating it like a one-shot code generator and start using it as part of a structured development workflow.

The post Stop trying to one-shot: How to prompt Claude better appeared first on LogRocket Blog.

Scroll to top