← All articles
Giona Granchelli

How to Integrate LLMs into Java & Spring Boot Without Losing Control

Learn how to integrate LLMs into Java and Spring Boot using typed contracts, structured output, model routing, tool governance, human approval, resilience, and audit evidence.

Diagram illustrating Spring Boot application integrating AI models through a governed runtime boundary

Adding an LLM call to a Spring Boot application is easy.

var response = client.generate(prompt);

The difficult part starts immediately afterwards.

Can the response be trusted by application code? What happens if the model returns malformed data? Can confidential information be sent to this provider? Can the model call a tool that changes application state? What happens when an action requires human approval? Can you switch between a cloud model and a local model without rewriting business logic?

And six months later, can you reconstruct why the system made a particular decision?

These are not prompt-engineering problems. They are software architecture and runtime-governance problems.

For existing Java and Spring Boot systems, a robust approach is to treat AI as a controlled execution boundary rather than another SDK scattered throughout the application.

This article develops that architecture step by step and shows how we apply these principles in TramAI, our JVM runtime for governed AI workflows.


The Core Problem: LLMs Are Nondeterministic Components

Traditional enterprise applications are built around contracts. A method accepts known input types and returns something predictable.

An LLM behaves differently. It accepts natural language and may produce valid output, malformed JSON, incomplete data, a semantically wrong answer, or an unexpected tool request. The surrounding model call can also fail through timeouts, throttling, provider errors, or infrastructure failures.

That does not make LLMs unsuitable for enterprise software. It means they should not be allowed to erase the contracts around the rest of the system.

A useful architectural rule is:

Keep nondeterminism inside the AI boundary. Keep the surrounding application deterministic.

┌─────────────────────────────────────────────────────────────┐
│                 Spring Boot Application                     │
│               Controller → Domain Service                   │
└──────────────────────────────┬──────────────────────────────┘
                               │ typed application contract

┌─────────────────────────────────────────────────────────────┐
│                 Governed Execution Runtime                  │
│                                                             │
│  Contract validation & repair   │  Classification & DLP     │
│  Model policy & routing         │  Tool authorization       │
│  Human approval continuation    │  Resilience & recovery    │
│  Audit sequencing & runtime evidence                        │
└──────────────────────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│              Providers & Self-Hosted Inference              │
│       OpenAI  │  Anthropic  │  Ollama  │  vLLM Local        │
└─────────────────────────────────────────────────────────────┘

1. Do Not Put Model SDKs Inside Business Logic

A common first implementation places provider-specific SDK clients, prompt construction, parsing, and retry loops directly inside application @Service classes. Repeat this in five services and you no longer have an AI integration; you have an AI architecture distributed across your business code.

Instead, expose the business capability through an explicit application contract:

@AiService
public interface InvoiceAnalyzer {

    @Operation(
        prompt = "Analyze the invoice and return its structured assessment.",
        model = "invoice-model"
    )
    InvoiceAnalysis analyze(String invoiceText);
}

Spring injects the interface as an application dependency while the execution layer owns provider execution and the configured runtime boundaries around it:

@Service
public class BillingService {

    private final InvoiceAnalyzer invoiceAnalyzer;

    public BillingService(InvoiceAnalyzer invoiceAnalyzer) {
        this.invoiceAnalyzer = invoiceAnalyzer;
    }

    public InvoiceAnalysis process(String invoiceText) {
        return invoiceAnalyzer.analyze(invoiceText);
    }
}

2. Use Typed Outputs When Software Depends on the Answer

A conventional JavaBean DTO defines what the application expects:

public class InvoiceAnalysis {

    private String supplier;
    private double amount;
    private String currency;
    private RiskLevel risk;

    public String getSupplier() { return supplier; }
    public void setSupplier(String supplier) { this.supplier = supplier; }

    public double getAmount() { return amount; }
    public void setAmount(double amount) { this.amount = amount; }

    public String getCurrency() { return currency; }
    public void setCurrency(String currency) { this.currency = currency; }

    public RiskLevel getRisk() { return risk; }
    public void setRisk(RiskLevel risk) { this.risk = risk; }
}

(Note: For ordinary Spring application DTOs, Java records are excellent. TramAI’s current JavaBean structured-output path uses POJO getters and setters, so the example above uses a conventional JavaBean DTO.)

The rule is simple: When software depends on the answer, give the answer a software contract.


3. Structured Output Constrains Representation, Not Truth

A typed return type does not automatically make an LLM truthful:

Structured output constrains representation, not truth.

An LLM can return { "amount": 4200.00, "currency": "EUR" } which satisfies every JSON schema constraint and still be factually wrong.

However, a robust structured-output pipeline eliminates structural uncertainty through feedback repair:

Return Type

Schema Generation

Model Response

Parsing & Validation
    ├── success ──────────────► Typed Result

    └── failure

      Repair Feedback

         Retry

      Typed Result or Explicit Failure

If validation fails because risk is invalid, the model receives precise feedback and another opportunity to produce valid structure before the application receives a typed result or explicit failure.


4. Provider Routing Should Be Driven by Data Classification

Once the AI capability has a stable contract, provider selection becomes an infrastructure policy rather than hardcoded client logic.

For example, an organization might define:

PUBLIC        ──► Approved global cloud model
INTERNAL      ──► Trusted enterprise provider
CONFIDENTIAL  ──► Approved regional provider
RESTRICTED    ──► Local model only

Classification should happen before provider execution, and sensitive fields that are not required by the model should be removed or redacted before crossing a trust boundary. This is fundamentally different from asking the model in a prompt not to reveal information: prompt instructions influence model behavior, while DLP and routing determine what information reaches the network in the first place.

The important question is no longer “Which API do we call?” It becomes: Which providers are allowed to receive this workload?

For companies requiring strict data isolation, explore our Private & Sovereign AI Deployments.


5. Runtime Controls for Tools and Side Effects

Prompts are not security boundaries. Placing an instruction in a system prompt saying “Do not reveal personal information” or “Do not transfer money” does not control network boundaries or tool execution.

Sensitive operations should be controlled by runtime policy:

Model requests tool call ──► Runtime Policy ──► ALLOW / DENY / REQUIRE_APPROVAL ──► Tool execution

The model may propose an action; the runtime decides whether the action is authorized.


6. Durable Human Approval and State Continuation

When an operation requires human approval (e.g., approving a high-value payment), approval cannot be a simple boolean flag inside an HTTP request.

The workflow must:

  1. Create an approval request;
  2. Suspend execution;
  3. Persist continuation state;
  4. Wait hours or days;
  5. Resume the correct workflow exactly once upon human decision.
AI workflow ──► Sensitive operation ──► REQUIRE_APPROVAL ──► Persist state ──► SUSPENDED

                                                         Human approves ◄──────────┘

                                                       Reload continuation

                                                         Resume & Execute

TramAI treats approval, suspension, persistence, and replay-safe continuation as runtime concerns rather than leaving each application to reinvent them independently.


7. Resilience Operating Inside Policy Constraints

Model providers fail, time out, or throttle. Resilience features (retries, circuit breakers, fallback providers) must respect data policy boundaries.

If a RESTRICTED workload fails on a local model, failing over to an unrestricted cloud model would restore availability by violating data boundaries. Resilience must operate inside governance constraints, not around them.


8. Runtime Controls Must Be Deterministically Testable

Architects frequently ask: “Can we test any of this without spending money and depending on model randomness?”

Runtime controls should be testable independently of model quality. Routing rules, policy decisions, tool authorization, retries, and data classification rules should have deterministic tests that run fast in CI/CD without calling a live LLM or incurring API costs.


9. Logging Is Not the Same as Evidence

AI workflows introduce decisions that require explicit explanation: model selection, provider allowance, DLP redaction, tool authorization, human approvals, and resume events.

For important workflows, governance decisions should produce structured execution evidence. TramAI can emit structured audit and runtime evidence when configured, including tamper-evident audit sequencing.


Acknowledging Spring AI

If your primary problem is broad Spring-native LLM integration, RAG, vector stores, or Model Context Protocol (MCP), Spring AI provides a mature, native framework for those concerns.

The architecture described here focuses on the additional boundary that arises when model execution must obey enterprise data classification, tool permissions, human approval, and durable workflow replay.


Where TramAI Fits

TramAI grew from this exact architectural problem. It is a Kotlin-first JVM runtime for governed AI workflows, designed to keep AI-specific concerns inside an explicit execution boundary:

Typed JVM contracts (@AiService)

Structured output & validation/repair

Classification & DLP

Runtime policy & model routing

Tool governance & human approval

Audit sequencing & runtime evidence

TramAI is under active development. It focuses specifically on the point where AI execution meets governance, authority, and recoverable business workflows.


Conclusion

The first LLM call in a Java application is rarely the hard part. The difficult questions arrive afterwards: Can I trust this output? Where can this data go? Which tools can the model use? Who authorizes side effects? Can we later explain what happened?

A better approach is to create a dedicated governed boundary:

Deterministic Application ──► Typed AI Contract ──► Governed Execution Runtime ──► Nondeterministic Model

The model may remain probabilistic. Your architecture does not have to become probabilistic with it.


Adding AI to an Existing JVM System?

Constant Labs helps Java and Kotlin teams design the boundary between models and production business logic—from a focused architecture review to implementation with Spring Boot and TramAI.

Giona Granchelli

Giona Granchelli

• Author & Lead Engineer

Founder of Constant Labs & creator of TramAI. Specializes in production software architecture, Spring Boot, and private AI integrations for European businesses.