It Has Never Been This Easy to Build Gen AI Features in Java
Source: Dev.to
Introduction
Building generative AI applications in Java used to be a complex, boiler‑plate‑heavy endeavor. You’d wrestle with raw HTTP clients, hand‑craft JSON payloads, parse streaming responses, manage API keys, and stitch together observability—all before writing a single line of actual AI logic. Those days are over.
Genkit Java is an open‑source framework that makes building AI‑powered applications in Java as straightforward as defining a function. Pair it with Google’s Gemini models and Google Cloud Run, and you can go from zero to a production‑deployed generative AI service in minutes, not days.
This is a complete, working example. Clone it, set your API key, and run.
Who is this for?
If you’re a Java developer, you’ve probably watched the Gen AI revolution unfold mostly in Python and TypeScript. The tooling, the frameworks, the tutorials—all skewed toward those ecosystems. Java developers were left to either build everything from scratch or use verbose, low‑level SDKs.
What the example shows
A Java application with a translation AI flow powered by Gemini via Genkit, showcasing:
- Typed flow inputs –
TranslateRequestclass with@JsonPropertyannotations. - Structured LLM output – Gemini returns a
TranslateResponseJava object directly (no manual JSON parsing). - Typed flow outputs – The flow returns a fully typed
TranslateResponseto the caller.
All of this lives in a single Java file plus two model classes. No Spring Boot, no annotation soup, no XML configuration—just clean, readable, type‑safe code.
Prerequisites
| Tool | Minimum version |
|---|---|
| Java | 21+ (Eclipse Temurin recommended) |
| Maven | 3.6+ |
| Node.js | 18+ (for the Genkit CLI) |
| Google GenAI API key | Free from Google AI Studio |
| Google Cloud SDK | Only for Cloud Run deployment |
Install the Genkit CLI
The Genkit CLI is your command‑line companion for developing and testing AI flows.
npm install -g genkit
Verify the installation:
genkit --version
The CLI powers the Dev UI and provides a seamless development experience (more on that below).
Project structure
genkit-java-getting-started/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/
│ │ ├── App.java # ← Main application
│ │ ├── TranslateRequest.java # ← Typed flow input
│ │ └── TranslateResponse.java # ← Typed flow + LLM output
│ └── resources/
│ └── logback.xml # Logging configuration
├── pom.xml # Maven config with Genkit + Jib
├── run.sh # Quick‑start script
└── README.md # This article
Quick‑start
git clone https://github.com/xavidop/genkit-java-getting-started.git
cd genkit-java-getting-started
export GOOGLE_API_KEY=your-api-key-here
genkit start -- mvn compile exec:java
That’s it—two commands. Your AI‑powered Java server runs at http://localhost:8080, and the Genkit Dev UI is available at http://localhost:4000.
You can also run the app directly:
mvn compile exec:java
Model classes
TranslateRequest.java – flow input
package com.example;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/**
* Input for the translate flow.
*/
public class TranslateRequest {
@JsonProperty(required = true)
@JsonPropertyDescription("The text to translate")
private String text;
@JsonProperty(required = true)
@JsonPropertyDescription("The target language (e.g., Spanish, French, Japanese)")
private String language;
public TranslateRequest() {}
public TranslateRequest(String text, String language) {
this.text = text;
this.language = language;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
@Override
public String toString() {
return String.format("TranslateRequest{text='%s', language='%s'}", text, language);
}
}
TranslateResponse.java – flow output & LLM structured output
package com.example;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/**
* Structured output for the translate flow.
*/
public class TranslateResponse {
@JsonProperty(required = true)
@JsonPropertyDescription("The original text that was translated")
private String originalText;
@JsonProperty(required = true)
@JsonPropertyDescription("The translated text")
private String translatedText;
@JsonProperty(required = true)
@JsonPropertyDescription("The target language")
private String language;
public TranslateResponse() {}
public TranslateResponse(String originalText, String translatedText, String language) {
this.originalText = originalText;
this.translatedText = translatedText;
this.language = language;
}
public String getOriginalText() {
return originalText;
}
public void setOriginalText(String originalText) {
this.originalText = originalText;
}
public String getTranslatedText() {
return translatedText;
}
public void setTranslatedText(String translatedText) {
this.translatedText = translatedText;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
@Override
public String toString() {
return String.format(
"TranslateResponse{originalText='%s', translatedText='%s', language='%s'}",
originalText, translatedText, language);
}
}
Note: The
@JsonPropertyDescriptionannotations are key. Genkit passes them to Gemini as part of the JSON schema, so the model knows exactly what each field represents.
Now you have a clean, ready‑to‑run Java project that demonstrates typed inputs, structured LLM outputs, and a minimal‑boilerplate development experience with Genkit. Happy coding!
Genkit Java Setup
Genkit genkit = Genkit.builder()
.options(GenkitOptions.builder()
.devMode(true)
.reflectionPort(3100)
.build())
.plugin(GoogleGenAIPlugin.create())
.plugin(jetty)
.build();
The GoogleGenAIPlugin reads your GOOGLE_API_KEY automatically.
The JettyPlugin handles HTTP.
Genkit wires everything together.
Defining the translate flow
genkit.defineFlow(
"translate",
TranslateRequest.class, // ← typed input
TranslateResponse.class, // ← typed output
(ctx, request) -> {
String prompt = String.format(
"Translate the following text to %s.\n\nText: %s",
request.getLanguage(),
request.getText()
);
return genkit.generate(
GenerateOptions.builder()
.model("googleai/gemini-3-flash-preview")
.prompt(prompt)
.outputClass(TranslateResponse.class) // ← Gemini returns a typed object!
.config(GenerationConfig.builder()
.temperature(0.1)
.build())
.build()
);
}
);
What’s happening?
| Step | Explanation |
|---|---|
TranslateRequest.class as the flow input | Genkit automatically deserializes incoming JSON into a TranslateRequest object – no manual Map.get() casting. |
TranslateResponse.class as the flow output | The flow returns a typed object that Genkit serializes to JSON for the HTTP response. |
outputClass(TranslateResponse.class) on the generate call | Genkit sends the JSON schema derived from TranslateResponse to Gemini. Gemini returns structured JSON, which Genkit deserializes back into a TranslateResponse object – no manual parsing needed. |
Single defineFlow call | • Registers the flow in Genkit’s internal registry |
• Exposes it as a POST /api/flows/translate endpoint | |
| • Makes it visible in the Dev UI | |
| • Adds full OpenTelemetry tracing automatically | |
| • Tracks token usage, latency, and error rates |
Compare this to writing a Spring Boot controller + service + DTO + config + exception handler for the same functionality. Genkit eliminates all that boilerplate.
Development Experience
Running genkit start launches a visual Dev UI at :
- Browse all flows – see
translatewith its typed input/output schemas. - Run flows interactively – fill in a
TranslateRequestJSON, click Run, and see theTranslateResponseinstantly (nocurlneeded). - Inspect traces – view which model was called, the input/output, execution time, and token usage.
- View registered models & tools – see all available Gemini models and any custom tools you’ve defined.
- Test tool calling – watch Gemini decide to call your tools in real‑time.
- Manage datasets & evaluations – create test datasets and evaluate AI outputs.
Building & Deploying with Jib
The project uses Jib to build and push container images directly from Maven—no Dockerfile, no Docker daemon.
# Set your GCP project
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
# Build the container image and push it to Google Container Registry
# (No Docker needed – Jib does it all from Maven!)
mvn compile jib:build -Djib.to.image=gcr.io/$PROJECT_ID/genkit-java-app
# Deploy to Cloud Run
gcloud run deploy genkit-java-app \
--image gcr.io/$PROJECT_ID/genkit-java-app \
--region $REGION \
--platform managed \
--allow-unauthenticated \
--set-env-vars "GOOGLE_API_KEY=$GOOGLE_API_KEY" \
--memory 512Mi \
--cpu 1
Why Jib?
- No Dockerfile – container image is built directly from the Maven project.
- No Docker daemon – Docker isn’t required on your machine.
- Fast rebuilds – dependencies, classes, and resources are layered; only changed layers are rebuilt.
- Reproducible builds – deterministic and independent of the local Docker environment.
- Direct push – image is sent straight to GCR/Artifact Registry without a local
docker push.
Optional: Build a local Docker image (requires Docker)
mvn compile jib:dockerBuild -Djib.to.image=genkit-java-app
Testing the Translate Flow
Once the server is running, send a TranslateRequest JSON and receive a structured TranslateResponse.
curl -X POST http://localhost:8080/api/flows/translate \
-H 'Content-Type: application/json' \
-d '{"text": "Building AI applications has never been easier", "language": "Spanish"}'
Example response
{
"originalText": "Building AI applications has never been easier",
"translatedText": "Construir aplicaciones de IA nunca ha sido tan fácil",
"language": "Spanish"
}
Try other languages
# French
curl -X POST http://localhost:8080/api/flows/translate \
-H 'Content-Type: application/json' \
-d '{"text": "Genkit makes Java AI development simple", "language": "French"}'
# Japanese
curl -X POST http://localhost:8080/api/flows/translate \
-H 'Content-Type: application/json' \
-d '{"text": "Hello world", "language": "Japanese"}'
Notice the response is always a structured JSON object, not a raw string. That’s the power of
outputClass(TranslateResponse.class): Gemini returns structured data that Genkit deserializes into your Java class automatically.
Production‑Grade Features
When you use Genkit you get more than a thin wrapper around API calls:
-
OpenTelemetry tracing for every flow execution
- Latency tracking per flow and per model call
- Token usage (input / output / “thinking” tokens)
- Error rates and failure tracking
- Full span hierarchy showing the execution path
-
Model swapping – change the underlying model with a single line:
// Switch from Gemini to OpenAI
.plugin(OpenAIPlugin.create())
// Or use Anthropic Claude
.plugin(AnthropicPlugin.create())
Genkit gives you a production‑ready framework for building, testing, and deploying AI‑powered Java applications with minimal boilerplate and maximum observability.
Getting Started with Genkit Java
// Or run locally with Ollama
.plugin(OllamaPlugin.create())
Genkit supports 10+ model providers, vector databases (Pinecone, Weaviate, PostgreSQL), Firebase integration, and more. This is where Genkit really shines for Java developers: flows, generate calls, and even LLM responses are fully typed.
Defining a Typed Flow
// The flow takes a TranslateRequest and returns a TranslateResponse
genkit.defineFlow("translate", TranslateRequest.class, TranslateResponse.class, ...);
Generating Typed Responses
// The LLM returns a TranslateResponse directly, no string parsing
genkit.generate(
GenerateOptions.builder()
.outputClass(TranslateResponse.class)
.build()
);
Genkit derives JSON schemas from your @JsonProperty and @JsonPropertyDescription annotations and sends them to Gemini, so the model returns structured data that maps directly to your Java classes. No object casting, no response.getText() + objectMapper.readValue(), no runtime surprises.
What This Project Covers
- RAG – Retrieval‑Augmented Generation with vector stores (Firestore, Pinecone, pgvector, Weaviate)
- Multi‑agent orchestration – Coordinate multiple AI agents
- Chat sessions – Multi‑turn conversations with session persistence
- Evaluations – RAGAS‑style metrics to measure your AI output quality
- MCP Integration – Connect to Model Context Protocol servers
- Spring Boot – Use the Spring plugin instead of Jetty for existing Spring apps
- Firebase – Deploy as Cloud Functions with Firestore vector search
Explore the full Genkit Java documentation and the samples directory to dive deeper.
Why Use Genkit Java?
- Typed inputs/outputs eliminate parsing errors.
- Structured LLM responses map directly to Java classes.
- Built‑in observability for easy debugging.
- Seamless deployment to cloud platforms.
Result: Building powerful generative AI applications with minimal code.
You can find the full code of this example in the GitHub repository.
Happy coding!