Claude Code Generates Unexpected Bugs: 4 Real-World Spring Pitfalls and Fixes

During a Spring‑based interview platform project, the author discovered four typical bugs introduced by Claude Code—transactional self‑invocation loss, AI‑generated NullPointerExceptions, async task failures after entity deletion, and Redis Stream message buildup—and explains the root causes and concrete remediation steps.

JavaGuide
JavaGuide
JavaGuide
Claude Code Generates Unexpected Bugs: 4 Real-World Spring Pitfalls and Fixes

1. Transactional self‑invocation disables @Transactional

The author used Claude to generate a "upload and process" flow inside a single KnowledgeBaseUploadService. Two methods were annotated with @Transactional, and the first method called the second via this.processInNewTx(file). Because the call bypasses the Spring AOP proxy, the REQUIRES_NEW propagation on processInNewTx never takes effect, leaving the inner transaction inactive.

@Service
public class KnowledgeBaseUploadService {
    @Transactional
    public void uploadAndProcess(File file) {
        saveFile(file);
        // self‑invocation: proxy is bypassed
        this.processInNewTx(file);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processInNewTx(File file) {
        // expected new transaction, but proxy is skipped
    }
}

Spring implements @Transactional via dynamic AOP proxies. A normal external bean call goes through the proxy, which starts or joins a transaction. A self‑call goes directly to the target object, so the transaction interceptor never runs.

Fix: Move the method that requires a new transaction to a separate bean and inject it, ensuring the call passes through the proxy.

@Service
@RequiredArgsConstructor
public class KnowledgeBaseUploadService {
    private final KnowledgeBaseProcessService processService;

    @Transactional
    public void uploadAndProcess(File file) {
        saveFile(file);
        processService.processInNewTx(file); // proxy call
    }
}

@Service
public class KnowledgeBaseProcessService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processInNewTx(File file) {
        // new transaction is now active
    }
}

2. AI‑generated NullPointerException in response parsing

During stress testing of the interview‑evaluation feature, the system occasionally threw a NullPointerException because Claude‑generated parsing code assumed the AI response was always well‑formed JSON. The LLM sometimes omitted fields, misspelled keys, changed types, or produced truncated JSON due to token limits.

Problematic code:

for (int i = 0; i < dto.questionEvaluations().size(); i++) {
    // NPE!
}

Fix: Treat the AI output as untrusted. First, validate the JSON structure (schema or field checks). Then, in the business layer, guard against null or empty collections and provide fallback handling.

List<QuestionEvaluationDTO> evaluations = dto.questionEvaluations();
if (evaluations == null || evaluations.isEmpty()) {
    log.error("AI response error: evaluation list missing or empty, sessionId={}", sessionId);
    evaluations = Collections.emptyList(); // keep the flow alive
}
int n = Math.min(evaluations.size(), questions.size());
for (int i = 0; i < n; i++) {
    QuestionEvaluationDTO ev = evaluations.get(i);
    // further field‑level null checks as needed
}

3. Async task crashes after entity deletion

When a user deletes a resume, an asynchronous analysis task that was already queued continues to run. The task attempts to load the now‑deleted entity, resulting in errors such as "Resume does not exist: ID=35".

Root cause flow:

User uploads resume → message sent to Redis Stream.

Analysis fails → message stays in pending list for retry.

User deletes resume → DB record removed.

Consumer retries → cannot find resume → error.

Fix: Perform a lifecycle check at the start of the async handler. Distinguish between unrecoverable errors (missing entity, illegal arguments) and recoverable ones (temporary network glitches). Unrecoverable errors should be logged and ACKed to remove them from the pending list; recoverable errors should be left un‑ACKed to trigger retry.

private void processMessage(StreamMessageId messageId, Map<String, String> data) {
    Long resumeId = Long.parseLong(data.get("resumeId"));
    var resumeOpt = resumeRepository.findById(resumeId);
    if (resumeOpt.isEmpty()) {
        log.warn("Entity deleted, skipping async task: resumeId={}", resumeId);
        ackMessage(messageId); // unrecoverable, remove from pending
        return;
    }
    try {
        Resume resume = resumeOpt.get();
        // business logic …
        ackMessage(messageId);
    } catch (TransientDependencyException e) {
        log.warn("Transient error, will retry: resumeId={}, msgId={}", resumeId, messageId, e);
        throw e; // keep message pending for retry
    } catch (Exception e) {
        log.error("Processing failed: resumeId={}, msgId={}", resumeId, messageId, e);
        throw e;
    }
}

4. Redis Stream message accumulation

A Stream in Redis grew beyond 100 messages because the consumer only used XACK to acknowledge consumption. XACK removes entries from the consumer group's pending list (PEL) but does **not** delete the actual stream entries. Without explicit XDEL or XTRIM/MAXLEN, old messages remain and consume memory.

Key commands: XADD – add a message to the stream. XREADGROUP – consumer group reads messages. XACK – remove entry from PEL. XDEL – delete a specific stream entry. XTRIM / MAXLEN – trim the stream length.

Fix: When adding messages, specify a MAXLEN limit (e.g., 1000) so the stream behaves like a circular buffer. This automatically discards the oldest entries.

// before fix
stream.add(StreamAddArgs.entries(message));

// after fix – keep only latest 1000 entries
stream.add(StreamAddArgs.entries(message)
    .trimNonStrict().maxLen(1000));

Using trimNonStrict() performs an approximate trim (the ~ operator) which is more performant than an exact trim.

Conclusion

AI‑assisted code generation dramatically speeds up development, but it also raises the bar for code review. Developers must understand the underlying framework mechanisms—such as Spring AOP proxies, defensive parsing of AI output, async task lifecycle checks, and Redis Stream semantics—to avoid hidden “time‑bomb” bugs.

Source repository: https://github.com/Snailclimb/interview-guide

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAI code generationRedisSpringAsyncTransactionalBug debugging
JavaGuide
Written by

JavaGuide

Backend tech guide and AI engineering practice covering fundamentals, databases, distributed systems, high concurrency, system design, plus AI agents and large-model engineering.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.