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.
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.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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.
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.
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.
A reliable Claude workflow usually follows this pattern:
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
Here are reusable prompt templates for common engineering workflows.
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
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 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.
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
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
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
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.
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.
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.

Learn how to build advanced Next.js forms with rule engines, client-side previews, Server Actions, and server-validated form logic.

AI is reshaping engineering teams emotionally as well as technically. A CTO shares insights on fear, trust, burnout, identity, and leading through AI change.

Learn what context rot is, why AI agent sessions degrade over time, and how to fix it with compaction, prompt anchoring, context files, plan files, and RAG.

Learn about TypeScript v6’s breaking changes, new ES2025 features, and deprecated options. A complete migration guide from v5 to prepare for v7.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now