When building cloud services around Generative AI, developers often face two major challenges:
To address this, I built EchoEngineβa lightweight, high-concurrency Spring Boot 3.4 application designed to ingest YouTube URLs, scrape publicly available metadata cleanly without YouTube API keys, and generate 3-bullet-point technical summaries using Googleβs Gemini 2.5 Flash model.
In this article, I will break down the system architecture, how Java 21 Virtual Threads (Project Loom) kept the application non-blocking, and the real-world GCP API quota traps I ran into while building it.
spring-ai-starter-model-google-genai
)
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
ποΈ Architectural Overview & Core Workflow
EchoEngine isolates data fetching, prompt construction, and LLM communication into clear service boundaries.
[ HTTP Client ]
β
βΌ (POST /api/summarize)
[ EchoController ] ββ(Extracts Video ID)βββΊ [ YouTubeSummaryService ]
β
βββββββββββββββββββ΄ββββββββββββββββββ
βΌ βΌ
[ YouTubeClient ] [ Spring AI ChatClient ]
(oEmbed + HTML Regex) (Google Gemini 2.5)
1. Key-Less YouTube Metadata Extraction
Instead of registering for developer API keys, YouTubeClient uses public endpoints:
Title: Fetched via YouTubeβs public oEmbed endpoint (https://www.youtube.com/oembed?url=...&format=json).
Description: Extracted via Springβs RestClient by parsing raw HTML meta tags using regular expressions.
2. High-Concurrency via Java 21 Virtual Threads
Because calls to YouTube and Gemini involve substantial network latency, standard OS thread pools risk exhaustion under load. By configuring Virtual Threads in application.properties, every incoming request runs on a lightweight virtual thread:
spring.threads.virtual.enabled=true
When RestClient blocks waiting for Gemini to finish generating text, the underlying carrier thread is unmounted to perform other tasks, giving the application non-blocking scalability without reactive stream complexity.
3. Spring AI Model Orchestration
Spring AI auto-configures a ChatClient bean using settings declared in application.properties:
spring.ai.google.genai.api-key=${GEMINI_API_KEY}
spring.ai.google.genai.chat.options.model=gemini-2.5-flash
The service constructs a targeted prompt and hands it off cleanly to the LLM:
@Service
public class YouTubeSummaryService {
private final ChatClient chatClient;
public YouTubeSummaryService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("You are a technical assistant. Given a video's title and description, " +
"provide a 3-bullet-point technical summary.")
.build();
}
public String summarizeVideo(String videoUrl) {
// Fetch metadata via YouTubeClient & pass to Gemini
return chatClient.prompt().user(promptText).call().content();
}
}
π Real-World Debugging & Battle Scars
The "Corporate Sandbox" 429 API Quota Trap
During early testing, the initial API key continuously threw a 429 Quota Exceeded (limit: 0) error despite zero previous requests.
The Cause: The key was generated inside a corporate-managed Google Cloud Platform project domain. GCP automatically enforces an explicit zero-quota default (limit: 0) on enterprise free tiers until billing accounts are linked.
The Fix: Migrating to an unmanaged developer sandbox project in Google AI Studio unlocked standard developer free-tier allowances.
Model Override Issues
Spring AI's default configuration targeted gemini-2.0-flash out-of-the-box. Since the AI Studio account allocation was explicitly set for the gemini-2.5-flash model endpoint, runtime parameters were passed to force the override:
./mvnw spring-boot:run "-Dspring-boot.run.arguments=--spring.ai.google.genai.api-key=YOUR_KEY --spring.ai.google.genai.chat.options.model=gemini-2.5-flash"
β‘ How to Run Locally
Set the API Key:
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
Start the Service:
./mvnw spring-boot:run
Trigger the POST Endpoint:
$body = @{ url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" } | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8080/api/summarize" -Method Post -Body $body -ContentType "application/json"
π― Key Takeaways
Decoupled Architecture: Separating scraping, prompt logic, and controllers makes the codebase maintainable and testable.
Virtual Threads simplify high-I/O applications: Project Loom delivers high throughput without rewriting code to be reactive.