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.
// 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.
| Gotcha | Fix |
|---|---|
| Model as a raw string | Use the Model.CLAUDE_SONNET_4_6 enum, not a string literal |
maxTokens(500) fails to compile | SDK expects a long — write 500L |
Casting ContentBlock to TextBlock | Use .isText() / .asText() accessor methods instead |
The full call looks like this:
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.
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:
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:
@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:
@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:
"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.