Automation · Applied AI · Engineering

How I Integrated Claude's API into a Spring Boot Microservice Pipeline

At some point every engineering team faces the same release ceremony: someone commits code, someone opens a pull request, someone pings the right reviewer on Slack, someone checks that the right person approved before hitting deploy. It works. It's also entirely manual, inconsistent, and the kind of thing that breaks at 4pm on a Friday.

I built an agent to handle all of it. Here's what I learned integrating Anthropic's Claude API into a Spring Boot microservice — and the engineering decisions that made it actually safe to run in production.

The Problem

Our team was processing releases across multiple services. Every release involved the same steps: commit, open PR, find the right reviewer based on what files changed, notify them, wait for approval, verify authorization, trigger deployment. Nothing about those steps required a human — they just required coordination.

The goal wasn't to remove engineers from the loop. It was to remove the ceremony from the loop, so engineers could focus on the hard decisions rather than the mechanics.

The Architecture

The agent runs as a Spring Boot microservice and orchestrates five steps:

Code → Commit → Open PR → Approve → Deploy

Each step is handled by a dedicated service class. Claude AI sits at two points in the pipeline: generating the PR description from the diff, and detecting which component a change belongs to for reviewer assignment.

AgentOrchestrator.java
// Claude generates a meaningful PR description from the actual diff
String description = claudeAIService.generatePrDescription(diff, repoName);

// Reviewer is assigned based on code ownership mapping
String reviewer = reviewerService.findReviewer(changedFiles);
gitHubService.assignReviewer(pr, reviewer);

Simple on the surface. The interesting engineering was everything around it.

Integrating the Claude Java SDK

Anthropic's Java SDK is straightforward but has a few things worth knowing.

GotchaFix
Model as a raw stringUse the Model.CLAUDE_SONNET_4_6 enum, not a string literal
maxTokens(500) fails to compileSDK expects a long — write 500L
Casting ContentBlock to TextBlockUse .isText() / .asText() accessor methods instead

The full call looks like this:

ClaudeAIService.java
MessageCreateParams params = MessageCreateParams.builder()
        .model(Model.CLAUDE_SONNET_4_6)
        .maxTokens(500L)
        .addUserMessage(prompt)
        .build();

Message response = client.messages().create(params);

String result = response.content().stream()
        .filter(ContentBlock::isText)
        .map(ContentBlock::asText)
        .map(TextBlock::text)
        .findFirst()
        .orElse("Fallback description");

The client is initialized once via @PostConstruct and reused — don't create a new AnthropicOkHttpClient per request.

An agent that can deploy is an agent that can break production.

Engineering Decision #1: The OAuth2 Gate

The most important design decision wasn't about Claude — it was about the deploy trigger.

So I gated the deployment behind OAuth2 token introspection. When a PR approval webhook fires, the agent doesn't immediately trigger the pipeline. It first verifies that the approver's token includes the deploy scope:

OAuth2AuthService.java
public boolean verifyDeploymentAccess(String userToken, String username) {
    ResponseEntity<Map> response = restTemplate.exchange(
            introspectionUrl, HttpMethod.POST,
            new HttpEntity<>(body, headers), Map.class);

    String scopes = (String) tokenInfo.getOrDefault("scope", "");
    if (!Arrays.asList(scopes.split("\\s+")).contains(requiredScope)) {
        throw new SecurityException(
            "User '" + username + "' lacks required scope 'deploy'.");
    }
    return true;
}

If no OAuth2 server is configured (e.g. in local dev), it falls back to a configurable authorized-deployers list. The pipeline never deploys on an anonymous approval.

Engineering Decision #2: Bounded Parallelism

The agent processes multiple repositories concurrently. But "concurrent" without limits is how you saturate the GitHub API, exhaust your connection pool, and get rate-limited at exactly the wrong moment.

I used Spring's ThreadPoolTaskExecutor with a hard cap:

ThreadPoolConfig.java
@Bean(name = "agentExecutor")
public Executor agentExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(properties.getMaxParallelRepos());   // default 7
    executor.setMaxPoolSize(properties.getMaxParallelRepos());
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("agent-pipeline-");
    executor.initialize();
    return executor;
}

Seven concurrent pipelines. Not unlimited. Seven is a number we chose based on GitHub's API rate limits and our average repo processing time. The queue absorbs bursts. The thread name prefix makes it trivially easy to trace in logs.

Engineering Decision #3: Retry with Exponential Backoff

Every GitHub API call goes through Spring Retry:

GitHubService.java
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))
public GHPullRequest openPullRequest(...) throws IOException {
    return repo.createPullRequest(title, head, base, body);
}

On transient failure: wait 2 seconds, then 4, then 8. After three attempts it gives up and marks the repo as FAILED — it doesn't crash the whole pipeline. Other repos keep processing in parallel.

The @EnableRetry annotation on the main application class activates this. Without it, @Retryable silently does nothing.

What Claude Actually Does

The PR description prompt is worth sharing because the quality of output depends heavily on how you frame the context:

prompt template
"You are a senior software engineer reviewing a pull request "
+ "for the repository '%s'.\n\n"
+ "Given the following git diff, write a concise PR description with:\n"
+ "1. A one-line summary of what changed\n"
+ "2. A bullet list of key changes\n"
+ "3. Any potential risks or things to watch during review\n\n"
+ "Keep it under 200 words. Be specific, not generic.\n\n"
+ "Diff:\n```\n%s\n```"

The instruction to be "specific, not generic" matters more than it sounds. Without it, Claude defaults to phrases like "Updated dependencies and fixed issues." With it, it writes descriptions that actually tell reviewers what to look at.

Large diffs are truncated to 12,000 characters before being sent. If the Claude call fails, the PR still opens — it just gets a fallback description. The pipeline doesn't stop because the AI was unavailable.

Results

The agent handles the full release ceremony: commit, PR creation with a meaningful description, reviewer assignment, email notification, OAuth2-verified approval, and deployment trigger. What used to take manual coordination across Slack, GitHub, and email now runs end-to-end without human intervention.

The same pattern — Claude for the cognitive step, Spring for the orchestration, explicit guards around the dangerous operations — applies to any pipeline where you want AI assistance without AI control.

Source code
The full project is open source. github.com/DivakarChowdary/commit-to-deploy-agent →
Spring Boot 3.3Java 17Anthropic Java SDK GitHub API (kohsuke)Spring RetryOAuth2
DK
Divakar Chowdary Kamma
Software Developer, Kaiser Permanente