Read the German version on creativeworkline.at →
A mileage log app contains a large amount of structured data: date, route, purpose, distance, and sometimes odometer readings. Traditional validation rules can detect missing values and obvious contradictions. Semantic anomalies are harder: destinations written in different ways, trips that look suspiciously similar, or patterns that only become visible across several entries.
I explored this problem in the backend of KM Geld, a mileage log app, through a deliberately narrow AI vertical slice. A user-triggered review sends selected trip data to a language model, which proposes possible anomalies. Before any finding can be displayed, the server checks references, original values, and domain rules deterministically. The model is never allowed to modify a trip.
The current result is a working and evaluated backend vertical slice, but still a prototype in terms of product maturity. It is not integrated into a production client and has not been publicly rolled out. That boundary matters: the implementation should support product and architecture decisions without claiming production readiness too early.
Definition: what does meaningful AI evaluation require?
An evaluated AI feature is a limited, working vertical slice whose behavior is assessed with more than a few convincing demo responses. Inputs, outputs, domain rules, failure cases, and quality criteria are explicit and reproducibly testable.
It does not need to be production-ready yet. It should, however, be reliable enough to inform the next product, architecture, and investment decisions with evidence instead of intuition.
The product question: where does an LLM add real value?
Not every data check needs a language model. Odometer continuity, thresholds, and required fields are handled more reliably with ordinary code. LLMs become interesting where semantic similarity, inconsistent labels, or context-dependent anomalies matter.
Five questions guided my decision before I chose a model:
- Which parts can be solved deterministically?
- Where can semantic model support add value?
- Which decision must remain with a person?
- Which data is genuinely required for the task?
- How can a useful result be measured?
The result is not an autonomous mileage log assistant. It is an optional review without automatic changes: the model searches for candidates, the software checks them, and a person decides whether a finding is relevant.
The engineering process
Product idea, goal, and decision boundaries
-> minimized, versioned contract
-> provider-independent model port
-> structured model candidates
-> deterministic domain verification
-> read-only result for a person
-> synthetic evaluation and iteration The sequence separates probabilistic work from deterministic responsibility. The model may search flexibly for relationships. The server remains the authority for verifiable data and rules.
1. Define decision boundaries before the prompt and model
The most important limits existed before the first model integration:
- The review is started explicitly and does not run silently in the background.
- The model cannot change data.
- Results remain suggestions, not automatic domain decisions.
- Legal and tax assessments are outside the feature boundary.
- The core application must keep working without the optional AI service.
These limits reduce risk and keep the vertical slice manageable. A narrow use case can be implemented faster, tested more precisely, and assessed more honestly than a generic “AI assistant.”
2. Send only necessary data across the model boundary
The model does not receive the complete domain object. The trip review uses a separate request contract containing only the fields required for the task:
- short-lived request ID
- date
- origin and destination
- purpose
- distance
- trip type
- optional odometer readings
Stable database IDs, notes, reimbursement rates, audit timestamps, device data, and account data are not part of either the public API contract or the model payload.
Contract and prompt versions are managed separately. Model instructions can evolve after new evaluations without changing the public API contract at the same time. Mobile and web clients remain decoupled from internal prompt iterations.
3. Put Spring AI behind a dedicated Kotlin port
Spring AI handles the technical integration of local and cloud models. The domain and application layers still know neither a concrete provider nor the framework. Their contract is a deliberately small Kotlin port:
fun interface TripAnalysisModel {
fun analyze(command: TripReviewCommand): TripAnalysisResult
} For this vertical slice, I used three adapters for different purposes:
| Adapter | Purpose |
|---|---|
| Deterministic fake | Fast, reproducible standard tests without network access or model cost |
| Local model through Spring AI and Ollama | Develop and evaluate real model responses locally |
| Cloud model through Spring AI and OpenAI | Explicit, paid comparison under separate controls |
Spring AI provides real integration value: provider configuration, model options, timeouts, native structured output, and Spring Boot integration. The dedicated port prevents provider, prompt, and framework details from leaking into domain logic.
The responsibilities remain explicit:
- Spring AI integrates models.
- The application orchestrates the trip review.
- Kotlin domain code decides which candidates are valid.
This separation is particularly relevant in existing JVM and enterprise systems. AI becomes part of a testable software architecture instead of an isolated foreign component.
4. Treat structured output as a format, not as truth
Structured output is useful. The model returns known fields, defined categories, and a machine-readable structure. A formally valid response can still be wrong.
A model could, for example:
- reference a request ID that does not exist
- quote an original value incorrectly
- misclassify a numerical anomaly
- return the same finding several times
- make a prohibited legal or tax statement
- produce unexpectedly long text
The trust boundary therefore does not end at the JSON schema. Model output remains external, probabilistic input.
5. Verify model suggestions before displaying them
Every candidate passes through a server-side verifier. It becomes a visible finding only when the relevant conditions are satisfied.
The verifier checks, among other things:
- Do all referenced IDs belong to the current request?
- Does the evidence cover every affected trip?
- Do observed values exactly match the canonical source data?
- Does a numerical category satisfy the thresholds defined in Kotlin?
- Do the explanation and proposed correction stay within the product boundary?
- Is the finding a duplicate, or does it exceed the display limit?
Numerical rules are not merely described in the prompt; they are recalculated in domain code. A contract test also protects against prompt instructions and server policy drifting apart unnoticed.
This creates a clear division of labor. The language model searches for semantically flexible candidates. The software verifies everything that can be checked against existing data and explicit rules. A separate technical deep dive with concrete Kotlin excerpts will follow.
6. Build evaluation from the beginning
A few impressive responses in a browser are not enough for model selection. The implementation therefore includes a synthetic golden dataset with positive, negative, security-relevant, and larger cases.
The evaluation records, among other things:
- schema validity
- precision and recall overall and per category
- grounding of findings that would actually be displayed
- rejected model candidates
- prompt injection and policy cases
- latency and token usage
- estimated API cost for cloud models
Current versioned evaluation snapshot
These results are a snapshot of the evaluation conducted on August 12, 2026. It used the current AI workflow, a golden dataset containing 59 synthetic test cases, and the model configurations shown below.
| Provider, model, and reasoning | Schema | Precision | Recall | Grounding | Security |
|---|---|---|---|---|---|
| Ollama, gpt-oss:20b, low reasoning effort | 100% | 96.2% | 86.2% | 100% | passed |
| OpenAI, gpt-5.6-luna, low reasoning effort | 100% | 100% | 100% | 100% | passed |
“Security passed” means that all six defined security cases passed: no prompt injection succeeded, and no tax or legal verdict was displayed. It does not represent a complete security assessment of the future production system.
This is not a general model ranking. The results apply only to this combination of model, configuration, prompt, and dataset. Every relevant change requires a new evaluation. A local run avoids API charges, but hardware, energy, and engineering time remain real costs.
The differences are more useful than the headline numbers. The local model passed the defined gates but missed some expected findings. The cloud candidate achieved complete precision and recall inside this limited dataset. These results inform the next product decision; they do not prove quality outside the tested cases.
7. Compare local and cloud models deliberately
A local model enables real model tests without recurring API charges and without an automatic cloud fallback. A cloud model serves as an explicit comparison with separate credentials, budgets, timeouts, and data controls.
Provider selection is intentional. If Ollama is unavailable locally, the system does not silently switch to a paid provider or one with a different privacy profile.
“Local” and “cloud” are not merely technical preferences. Quality, latency, hardware, operational effort, region, retention, and cost need to be evaluated for the specific product.
8. What is still required before production use?
Passing a golden-dataset run does not make the vertical slice production-ready. The current implementation covers the core domain and technical logic. Production use still requires the AI feature, its clients, and its operation to be evaluated as a complete system.
That includes:
- integration and user experience in the intended clients
- authentication, authorization, and protection against abuse
- review of hosting, model providers, and data flows
- monitoring of quality, cost, and failure behavior
- load, security, and end-to-end tests
- clear ownership of model, prompt, and policy updates
- a controlled rollout with real quality and cost metrics
The vertical slice does not answer every production question. It does show whether the product idea is technically viable and which product, operational, and privacy decisions still need to be made before real-world use.
What transfers to other AI products?
The process is not limited to mileage data. It applies wherever a model derives suggestions from existing data and the application needs verifiable boundaries:
- document and data quality checks
- internal support and service assistants
- structured extraction from free text
- preparation of manual approvals
- semantic duplicate and consistency checks
- explainable suggestions in domain applications
The domain model changes. The separation of a minimized contract, model port, verification, human decision, and evaluation remains.
Bottom line
The most important engineering decision was not the choice of model. It was defining the product goal tightly enough to separate probabilistic work from deterministic responsibility.
Kotlin and Spring Boot provide a stable backend foundation: a clear contract, replaceable providers, testable domain rules, controlled failure boundaries, and an evaluation process that makes model decisions traceable.
This turns a quick AI experiment into an evaluated technical foundation that does more than look plausible in a demo. It provides concrete answers for the next stage of product development.
FAQ
Does every AI feature need a golden dataset?
As soon as prompts, models, or configurations need to be compared, reproducible test cases and expected results become important. For an early vertical slice, the dataset can be small, but it should cover the most important positive, negative, and security-relevant cases.
Is structured output enough to make an LLM response safe?
No. Structured output stabilizes the format. IDs, evidence, domain rules, and permitted statements still need to be checked against trusted data and an explicit policy.
Why use Kotlin and Spring Boot for AI engineering?
AI features and LLM integrations require more than model calls. They also need APIs, validation, security, metrics, tests, and integration into existing systems. Kotlin and Spring Boot are especially useful when an organization already operates JVM or enterprise systems and wants to add AI as a maintainable product capability.
Can the same architecture be built with Node.js, TypeScript, or Python?
Yes. The principles are not tied to Kotlin or Spring Boot. A clear API contract, encapsulated model access, deterministic verification, and reproducible evaluation work with Node.js and TypeScript or with Python as well. Frameworks such as LangChain4j, the Vercel AI SDK, and Pydantic AI can support different parts of the integration.
Is a local model automatically better for privacy?
No. Local execution can reduce data transfers, but it does not automatically solve access control, logging, device security, model provenance, or domain risk. The right architecture depends on the product, its data, and its protection requirements.
When is an AI feature ready for production?
Not after a successful model test alone. Client access, abuse controls, cost, privacy, monitoring, failure behavior, user experience, and ownership of model and prompt updates must also be defined and tested in the complete system.