The Main Thread: Java, AI & Software Architecture

Markus Eisele

Practical lessons on enterprise Java, software architecture, and AI coding agents for experienced developers, architects, and technical leads. Drawing on more than two decades in the industry, Markus Eisele explores what works, where tools fall short, and how to make better decisions about the software you build and maintain. www.the-main-thread.com

  1. 2d ago

    Automatic Model Routing: What Your Coding Assistant Chooses for You

    When my coding assistant shows automatic in the model selector, my reaction is mixed. I want the convenience. I also want to see and influence the decision, especially when an investigation gets stuck or a new model gives me something else to try. In this episode, I explain what would make me comfortable handing over routine model selection. A cheaper model can leave more of my budget available for a difficult task. Repeated explanations and corrections can consume that saving, along with my time. I want to compare what it costs to reach a change I can accept. I use a hypothetical Java service that creates duplicate orders after a timeout to make the tradeoffs concrete. Finding the retry configuration may be straightforward. Establishing what happened after the receiving service saved an order needs more investigation. That gives a router more information to act on, while showing why a stronger model also needs the right code, context, and tools. I also question my own expectations about the latest models. Research on novelty and confidence gives me a reason to check whether a response handled the difficult constraint, even when I like the answer. Those studies have limits, which I discuss alongside what a developer can reasonably judge in a familiar service. Then I share experiments I’m driving at IBM to connect context information with recorded activity. I want to follow what the assistant read, which claims it checked, and where an assumption entered the conversation. Being able to inspect that history would help me decide whether to provide more information, request a more capable model, or accept the change. The episode is narrated locally with Fish S2 Pro and my trained LoRA voice adapter. Chapters * 00:00 — Intro * 00:12 — Why my reaction to automatic routing is mixed * 02:45 — More help from the same budget * 05:22 — Routing a Java duplicate-order investigation * 08:15 — Hidden choices and the control I want * 10:43 — What depends on the harness * 12:49 — New models, expectations, and judging quality * 15:39 — Making conversations easier to inspect * 18:28 — The controls that would make me comfortable * 20:47 — Outro Links and references * RouteLLM: Learning to Route LLMs with Preference Data — Research on choosing between stronger and cheaper models using preference data. Its reported cost savings describe the evaluated benchmarks, rather than a guaranteed saving on a development task or subscription. * SWE-Router: Routing in Multi-turn Agentic Software Engineering Tasks — A cheaper model takes exploratory turns before the router decides whether to continue or escalate. In this implementation, the stronger model restarts from the original prompt; the exploration counts toward the cost. * Quantifying the Novelty Bias when Evaluating Interactive Prototypes — A study of 48 participants using functionally identical technology pairs, including chatbots, presented as old and new. The brief interactions with general users do not establish an effect size for professional developers doing sustained coding tasks. Author-hosted paper. * Do Users Write More Insecure Code with AI Assistants? — An earlier security-task study in which assistant users produced less secure code overall while expressing greater confidence in its security. It used an older model and specific tasks; it is not a ranking of current assistants. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

    Automatic Model Routing: What Your Coding Assistant Chooses for You
  2. Oct 28

    AI Context Windows: Comparing the Cost of an Accepted Coding Result

    I see two things coming up regularly in our internal IBM Bob Slack channel. People want a larger context window, and people are surprised how expensive their conversations become. Well, both make sense when you look at what the agent sends to the model with each request. In this episode, I explain what a context window contains and how IBM Bob, Codex, Claude Code, and Gemini CLI decide what goes into it. We follow twenty model requests, starting at 10,000 input tokens and adding another 10,000 each time. The last request contains 200,000 tokens, but the total across those requests is 2.1 million. At the standard GPT-6.1 Sol rate used in the episode, that is $4.20 for input alone, with caching excluded and output or tool charges added separately. Then we look at how caching and compaction change the calculation. I also share guidance from the Book of Bob, my collection of agentic development best practices on an internal IBM website. Unfortunately, I haven’t been able to open source it so far. We go through standing context, tool descriptions, focused task briefs, and short handover notes, using a Java email-validation example to make the advice concrete. And, of course, there is the business model. The labs sell tokens, but serving those tokens costs them money too. I consider those incentives alongside the cost of reaching a result I can accept, including retries and review time. A larger window can help with a change across several modules; the capacity number alone doesn’t tell us what the task will cost or how reliably the model will connect the details. Model limits and API prices are a snapshot from October 14, 2026. The Codex defaults mentioned came from my local installation and can differ across accounts and sessions. The episode is narrated locally with Fish S2 Pro and my trained LoRA voice adapter. Chapters * 00:00 — Intro * 00:12 — Wanting more context and wondering about the bill * 01:52 — What goes into a context window * 04:25 — Model limits and the applications around them * 06:57 — How 200,000 tokens become 2.1 million across a conversation * 09:45 — Caching, pricing tiers, and response time * 13:01 — Long-context reasoning and what compaction can lose * 15:11 — Context practices from the Book of Bob * 20:24 — Selling tokens and the cost of an accepted result * 22:52 — What to measure in your own coding sessions * 24:45 — Outro Links and references Context windows and coding tools * IBM Bob: Context window management — Public documentation for the context breakdown, task limit, and compaction threshold. * OpenAI: GPT-6 Astra and Codex configuration reference — Model limits and application settings. The configuration reference is separate from the local catalog observation discussed in the episode. * Claude model overview and Claude Code extended context — Model capacities and the conditions that apply inside Claude Code. * Gemini 3.1 Pro, Gemini 4 Argon, and Gemini CLI model selection — Check the selected model and whether its limit describes input or a shared input/output window. Conversation costs and caching * OpenAI API pricing — Source for the GPT-6.1 Sol example: $2 per million uncached input tokens at the standard short-context rate on the research date. * OpenAI conversation state — Continuing a conversation with a response ID, including how earlier input is billed. * OpenAI prompt caching — Prefix reuse and the conditions for cache hits. * Claude API pricing — Cache rates and model-specific long-context pricing. Context practices and research * Anthropic: Effective context engineering for AI agents — Retrieving information when needed, compaction, notes, and separate agent contexts. * Lost in the Middle: How Language Models Use Long Contexts — Research on how information placement affected the models tested. These results describe earlier models. * RULER: What’s the Real Context Size of Your Long-Context Language Models? — Evaluating long-context behavior beyond finding a single fact. Thanks for reading The Main Thread! Subscribe for free to receive new posts and support my work. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

    AI Context Windows: Comparing the Cost of an Accepted Coding Result
  3. Oct 21

    Spec-Driven Development: Why Passing Tests Can Miss a Changed Requirement

    I want coding agents to take on more of the implementation. I also want to understand what I’m accepting when they come back with a result. Writing a spec gives those decisions somewhere to happen, but it gives me another set of files to keep current too. In this episode, I revisit the trade-offs around spec-driven development and the fear that the requirements, tests, and code start describing different behavior. In the cart-policy example with IBM Bob, changing the design from 30 to 90 days left the old tests green and every requirement ID connected. Well, the links were intact. The new decision hadn’t reached the assertions. Then I go through the Java inventory experiment with TLA+ and Lean. A model checker found the sequence that let two callers reserve the last item. A separate proof checked the arithmetic rule. Neither check proved that the Java implementation matched its formal definition, so the tests and the review of that connection stayed part of the process. I also consider how this changes as models become more capable. Needing fewer instructions about the implementation doesn’t settle the business rule or tell me which checks are enough to accept a change. The episode is narrated locally with Qwen3-TTS and my trained LoRA voice adapter. Chapters * 00:00 — Intro * 00:10 — What I want from specifications * 01:37 — The cost of vague specs and the maintenance of detailed ones * 04:05 — IBM Bob and the cart policy: 90 days in the design, 30 days in the code * 07:07 — How specifications and code drift apart * 09:31 — One item, two callers: trying TLA+ and Lean with Java * 13:23 — What the formal checks prove, and what they leave open * 15:46 — What better models could change * 19:02 — What I’d keep for the next Java change * 20:36 — Outro Related articles * Why AI Coding Agents Still Need Clear Specs — The correction effort that follows a vague request, and how acceptance criteria can make the intended behavior easier to check. * Spec-Driven Development Needs an Exit Strategy — Small change briefs, the cost of keeping design prose current, and where lasting constraints belong after implementation. * Formal Verification for AI-Generated Java — The inventory example with Java tests, a TLA+ model, and a Lean proof, including the limits of each check. Thanks for listening to The Main Thread podcast! Subscribe for free to receive new posts and support my work. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

    Spec-Driven Development: Why Passing Tests Can Miss a Changed Requirement
  4. Oct 15

    Qwen3-TTS Voice Cloning: What I Learned Building a Local Podcast Voice

    I wanted to bring The Main Thread Podcast back, and recording the episodes myself has a few practical problems. I do my job from home, and I don’t have access to a quiet studio. The kids come in and out, there’s noise in the house, and getting a long, uninterrupted recording is a challenge. Reading a transcript also changes how I sound. It’s different from freely explaining something to another developer. So my main motivation was to put the time into writing a script and developing a narrative that’s easy to follow, then have a text-to-speech model read it in my voice. I didn’t want to go with a paid provider either. I had tried training my voice on a provider’s platform, and the samples sounded flat to me. So I started trying local models on my MacBook. Those experiments eventually led to training a local voice adapter of my own. The target was an article of around three thousand five hundred words, which should become roughly twenty to twenty-five minutes of audio. That’s a long time to listen to a voice that keeps stopping in strange places. It also means the model has to get through technical names, longer explanations, and changes between paragraphs without sounding like a different person halfway through. Thanks for listening to The Main Thread Podcast! Subscribe for free to receive new posts and support my work. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

    Qwen3-TTS Voice Cloning: What I Learned Building a Local Podcast Voice
  5. Jun 28

    How to Find Regression Risks in a Quarkus App Before You Write the Feature

    User unregistration sounds simple until you look at the rest of the system. In a booking application, “delete this user” is never just one DELETE endpoint. It touches active bookings, foreign keys, historical reads, cache state, and the uncomfortable question every senior team eventually has to answer: is this really a hard delete, or is it deactivation, anonymization, or something in between? That is why I like the workflow Alex Soto shows in the latest video. He does not start by asking IBM Bob to write the feature. He starts by asking it to map the regression impact before implementation. That is a much better use of an assistant. Use the Agent Before the Feature Code I still think the basic rule is simple: use AI to go faster on work you understand already. If you hand core business logic to a model and hope for the best, you are outsourcing the part you still need to own. Impact analysis is different. Here the agent helps you read the shape of the system, collect likely break points, and build a regression plan before the feature branch gets noisy. In this case, “shift-left” just means you do the regression thinking before you touch the code that will trigger it. That matters because deletion flows are where business rules stop looking like CRUD. One missing constraint check and you get orphaned rows, broken history screens, or a second-order production issue that only shows up after cleanup jobs or cache refreshes run. Efficient. Also bad. Why IBM Bob Fits This Job It’s not IBM Bob’s magic that makes this work: It is context. Bob runs against the workspace, so it can inspect the actual resources, entities, and services in the application. That gives it a real basis for the regression plan. It is reading the model you are about to change, not guessing from a paragraph of pasted code. That is the difference. A browser chat can still help, but a workspace-aware tool can follow relationships across the codebase. For something like user unregistration, that is where the interesting bugs live. The Prompt Is Small on Purpose Alex starts with a constrained prompt: “Act as a tester. Identify the areas of the system that should be included in regression testing after changes are made to add the unregistration of a user. Don’t implement anything.” Don't implement anything keeps the session focused on analysis. Once an assistant starts writing code too early, it tends to collapse design, impact analysis, and implementation into one optimistic blob. That is fine for demos. It is much less fine when a deletion rule can invalidate half your booking flow. What a Good Regression Plan Should Surface Because Bob can inspect the workspace, it can point at actual system boundaries that deserve tests before the first delete path is implemented. In this example, the report highlights the kind of things I would expect a senior developer to check immediately: * Active booking rules. What happens if the user still has a live reservation, an open payment flow, or another stateful process attached to the account? * Database integrity. Do deletes cascade, fail fast, orphan records, or silently leave history in a shape the rest of the system no longer understands? * Historical read paths. Can past bookings, reports, and audit views still load after the user record changes or disappears? * Cache and derived state. If user data is cached, indexed, or copied into another read model, what gets invalidated and when? * API behavior. What does the endpoint return when the user has already been unregistered, cannot be removed, or does not exist anymore? This is where the workflow earns its keep. The assistant is not replacing engineering judgment. It is helping you build the list you will review anyway, only faster and with fewer blind spots. The Senior Java Questions Start After the First Report A regression plan is a good start. It is still only a start. If I were taking this feature forward, I would want the team to answer a few more questions before implementation: * Is “unregister” really delete? In many systems the right answer is soft delete, anonymization, or status change because legal retention, reconciliation, or audit rules still apply. * Where is the transaction boundary? If one part of the removal fails, what rolls back, and what can be left half-done? * What is the concurrency story? If one request unregisters the user while another creates or confirms a booking, which invariant wins? * What happens on the second call? Idempotence matters here. A repeated delete should not create fresh damage. * Which tests belong at which level? Some checks belong close to the domain model. Others need @QuarkusTest, HTTP assertions, persistence wiring, or a packaged-app check with @QuarkusIntegrationTest. That last point is worth calling out. A lot of teams mix regression planning with test implementation planning and then get a giant bag of tests with no clear boundary. I would rather make that mapping explicit up front. Turn the Report Into Proof The report is useful because it changes what we test first. For a change like this, I would usually split the work into a few layers: * business-rule tests around the unregister decision itself * persistence tests for relationship handling and transaction rollback * @QuarkusTest coverage for the HTTP and CDI wiring * packaged-app checks with @QuarkusIntegrationTest when the deployment shape matters After that, continuous feedback matters more than one big test run at the end. Quarkus continuous testing is a good fit here because the regression plan will usually touch multiple paths, and you want quick confirmation while the model is still moving. Coverage still matters, but later. First make sure the dangerous branches are tested. Then measure what you actually covered. Further Reading * Quarkus: Testing Your Application for @QuarkusTest, transactions in tests, mocks, and @QuarkusIntegrationTest. * Quarkus: Continuous Testing if you want fast feedback while the unregister flow is still changing. * JUnit User Guide for the current programming model, extension model, and test organization options once the regression plan becomes a real suite. * JaCoCo Coverage Counters if you want branch, instruction, and complexity numbers to reflect the paths that actually matter. * IBM Bob if you want to try the workspace-aware analysis workflow Alex uses in the video. If your unregister flow goes through JPA or Hibernate relationships, read the deletion and orphan-handling rules in the ORM documentation for the exact version you run. That is one of those areas where small mapping details decide whether cleanup is correct or catastrophic. Why I Like This Pattern I like this workflow because it uses the assistant where assistants are actually useful: reading a codebase, building a candidate regression map, and giving you a sharper review checklist before implementation starts. You still need to decide the delete semantics. You still need to own the transaction model. You still need to write the tests that prove the feature is safe. But you do not need to build the first regression checklist from memory every single time. Let the tool help there. Save your energy for the decisions that still need an engineer. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

    How to Find Regression Risks in a Quarkus App Before You Write the Feature
  6. Jun 22

    Using JaCoCo and AI to Prioritize Test Coverage in Quarkus Teams

    In the latest video, my co-author Alex Soto shows a test coverage workflow I actually like. I do not care about 100% coverage as a badge. I care about whether the important parts of the system are tested. Past that point, the number gets expensive fast. In some domains you still need to hit a minimum threshold because regulation says so. Ok. That might be a real constraint. Building out tests should follow a more simpler question: where are the gaps that matter, and what should we test first? Alex uses IBM Bob with a JaCoCo report from a Quarkus backend to answer exactly that. It is a boring workflow in the best way. The report grounds the analysis, so you get a list of real testing gaps instead of a random AI guess list. Quarkus and JaCoCo Without Extra Setup Before Bob can analyze anything, you need a coverage report. Quarkus keeps this part simple. Add the quarkus-jacoco dependency to your pom.xml or build.gradle file, run mvn verify, and Quarkus writes the report to target/jacoco-reports. The automatic setup from quarkus-jacoco only covers tests annotated with @QuarkusTest. If your suite also includes standard unit tests without that annotation, you need the regular JaCoCo Maven plugin for those. If you use both at the same time, configure them carefully or you will hit duplicate instrumentation errors. For many Quarkus projects, the extension alone is the simple option and usually the right one. Give the Agent the Real Report This workflow works because the agent has real context. If you ask a coding agent to improve test coverage with no grounding, it will guess. If you give it the JaCoCo XML report, it can see what is already covered, what is still untouched, and which areas deserve attention first. Here is the exact prompt Alex used for the analysis. Prompt: Act as a tester explorer to find test cases to improve the test coverage based on the current JaCoCo test coverage report placed at src/test/resources/jacoco.xml file. Read the JaCoCo file, explore what code has already been tested, and what tests could be done to improve the coverage. It is not necessary to have 100% coverage, but increasing coverage for relevant cases is necessary. It keeps the agent focused on relevant business logic instead of chasing 100% because the number looks nice in a report. Phases Beat a Giant Test Todo List Bob reads the XML and returns a structured view of the current instruction and branch coverage. More importantly, it points at the places that matter, such as core booking logic or user services sitting at zero percent coverage. After that, Bob breaks the work into phases you can actually use. You get a plan, not a wall of possible tests. * Phase 1: Critical Business Logic (High Priority) * Phase 2: REST API Integration (High Priority) * Phase 3: Entity Layer (Medium Priority) * Phase 4: DTOs and Utilities (Low Priority) It also estimates the coverage you can reach after each phase. That gives you a clear next step and a rough idea what the work buys you. Then you can move straight to a follow-up prompt: Follow-up Prompt: Implement the Phase 1! At that point the coding assistant has a bounded job. It reads the code for that phase, understands which classes are involved, and generates the tests for that slice. That is way better than asking it to “improve coverage” and hoping it reads your mind. What Comes Next I like this example because it shows where AI fits well in normal engineering work. The model works from the actual report, with a clear goal, on code that already exists. That is a much better use of these tools than vibing test coverage. Watch the full video to see the workflow in action. In the next video, Alex will look at test execution performance, because faster feedback matters almost as much as better coverage. You can download Bob and try this yourself at bob.ibm.com. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

  7. Apr 3

    API Versioning in Quarkus: The Part AI Doesn’t Solve

    Versioning a REST API is one of those architectural chores that everyone knows they need to do, but nobody actually wants to implement. As your data models evolve, you have to maintain backward compatibility for existing clients while serving new data structures to updated ones. Historically, this meant writing a lot of boilerplate: duplicating entities, adding routing logic, and carefully wiring fallback behavior. The problem is not that this is hard. The problem is that it is easy to get wrong, and once it is wrong, you carry that decision for years. If you want a deeper breakdown of the long-term consequences, the earlier article on Quarkus API Versioning Strategies goes into detail on how these decisions age over time. What changes now is not the problem. What changes is the effort required to implement it. As my co-author Alex Soto (Developer Advocate at IBM) demonstrated in our latest video, generative AI changes the balance. With IBM Bob, you no longer spend time writing versioning plumbing. You spend time choosing the right strategy. And that distinction matters. A Simple Mental Model There are only three real ways to version an API: * Change the URL * Change the request metadata * Change the representation Everything else is just a variation of these three. Bob can implement all of them in seconds. But Bob does not decide which one you should use. What is IBM Bob? Before diving into the strategies, it helps to understand the tool. IBM Bob is an AI coding assistant that understands your project context. It reads your existing Quarkus classes, generates new entities, updates JAX-RS endpoints, and even runs the application in dev mode to validate the result. You are not asking for snippets. You are asking for changes to your system. And this is where things get interesting. Bob will happily generate a second version of your API in seconds. If your strategy is wrong, you just created technical debt faster than ever. 1. Path Versioning (The Pragmatic Choice) This is the most visible approach. The version is part of the URI: /v1/customer /v2/customer In the demo, Alex asked Bob to update the Customer endpoint by renaming the name field to firstname in version two. Bob created a new CustomerV2 entity, added a new @Path("/v2/customer") endpoint, and kept the original version intact. This is simple. It is explicit. It is easy to cache and debug. But it also duplicates your API surface. Every version is a new endpoint, and over time you end up maintaining multiple parallel APIs. This approach works well when you want clarity and when external consumers depend on stable URLs. It becomes harder when you have many versions active at the same time and need to keep them consistent. If you want a deeper comparison of how this scales, the article on Quarkus API Versioning Strategies breaks this down in more detail. 2. Custom Headers (The Clean URI Approach) Here the URI stays the same: /booking The version is passed through a header: Accept-Version: 2 In the demo, Alex asked Bob to rename the customerName field to name in a Booking entity. Bob updated the resource to use @HeaderParam, routing requests to BookingV1 or BookingV2 depending on the header, and defaulting to version one if no header is present. This keeps your URIs clean. Clients do not need to change endpoints. Versioning becomes part of the request metadata. The problem shows up later. After a few versions, you no longer know which clients send which headers. Removing old logic becomes guesswork. You carry old branches in your code because you are not sure who still depends on them. This is where lifecycle management becomes critical. The article on When to Deprecate APIs explains how to handle that transition without breaking consumers. 3. Media Type / Content Negotiation (The REST Purist Way) This approach uses the Accept header with vendor-specific media types: application/vnd.acme.car.v1+json In the demo, Alex asked Bob to rename the year field in a Car entity to date. Bob updated the @Consumes and @Produces annotations to route requests based on the media type, without changing the URI or adding custom headers. This is the most flexible and the most aligned with REST principles. You keep a single resource and serve different representations. But flexibility comes with complexity. Media types are harder to debug. Clients need to construct correct headers. Tooling support is not always consistent. And once you introduce multiple representations, testing becomes more involved. This approach makes sense in large systems where representation evolves independently from the resource. If you want to see how this works in a real integration scenario, the Quarkus Stripe API Versioning Adapter Tutorial shows how to adapt external APIs with similar patterns. The Takeaway The hardest part of API versioning used to be the implementation. With tools like IBM Bob, implementation takes seconds. The hard part is choosing the right strategy and managing its lifecycle over time. Bob will implement whatever you ask for. It will do it correctly and consistently. But it will not fix a bad architectural decision. Pick a strategy that fits your consumers, enforce it consistently, and understand what happens when you need to evolve it. Want to try it on your own codebase? You can check out Bob at ibm.com/bob. This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

    API Versioning in Quarkus: The Part AI Doesn’t Solve
  8. Mar 24

    Expose Legacy Java Apps to AI Agents with the Kubernetes Sidecar Pattern

    Legacy systems are everywhere in enterprise environments. They run critical business processes, hold decades of data, and often cannot be rewritten easily. When teams start experimenting with AI agents, the first instinct is usually to expose new APIs or refactor the existing system so an LLM can interact with it. That approach breaks down quickly. Large enterprise applications have complex dependency trees, fragile integration points, and release processes that move slowly. A small change to expose a new endpoint can ripple through dozens of modules. What starts as “just one API for an agent” turns into a multi-month modernization project. The reality is simple. Most organizations do not need to rewrite their legacy systems to integrate them with AI. They just need a safe way to translate existing APIs into something AI agents understand. That is exactly what the Model Context Protocol (MCP) enables. In a recent demo, my friend and co-author Alex Soto showed how to expose a legacy application to AI agents without modifying the original code. The trick is simple but powerful: run a lightweight Quarkus MCP server as a Kubernetes sidecar next to the legacy application. This pattern creates an AI-friendly interface while leaving the existing system completely untouched. I have written about other approaches to MCP integration in this article: The Real Problem with AI Integration in Enterprise Systems Most teams assume the problem is exposing data to an LLM. The real problem is how to do it safely without destabilizing production systems. Enterprise applications typically have several characteristics that make direct integration risky: Legacy systems evolve slowly because they support critical business processes. Changing them introduces risk. AI integrations, on the other hand, evolve quickly because teams experiment with prompts, tools, and workflows. These two speeds do not match. There is also a security issue. Many internal APIs were never designed to be consumed by external systems. They assume trusted callers, internal networks, and stable traffic patterns. Exposing them directly to AI systems increases the attack surface. Finally, there is an operational concern. AI workloads often behave differently from traditional applications. Agents may call APIs repeatedly while reasoning through a problem. They may retry requests aggressively. They may explore endpoints in unexpected ways. Connecting these workloads directly to a legacy system is not a good idea. So the goal becomes clear: create an adapter layer that speaks MCP while isolating the legacy system. Why the Sidecar Pattern Works So Well The sidecar pattern is widely used in Kubernetes to extend applications without modifying them. Service meshes, logging agents, and security proxies often run as sidecars. The same pattern works extremely well for AI integration. In Alex’s demo, the legacy application and the MCP server run inside the same Kubernetes Pod. Because containers in the same pod share a network namespace, they can communicate through localhost. The MCP server simply calls the legacy REST API internally. This architecture gives several important advantages. First, the legacy application does not change. The sidecar translates requests between the MCP protocol and the existing REST API. Second, the legacy API does not need to be exposed externally. Only the MCP server is visible to AI clients. Third, the MCP layer can evolve independently. You can add new tools, change prompts, or improve output formatting without touching the legacy codebase. This is exactly the kind of separation enterprise teams need. The Demo Architecture Alex’s example uses the classic Spring Boot PetClinic REST application. This project is widely known in the Java community and exposes an OpenAPI specification. The architecture looks like this: The MCP server acts as a translation layer. When an AI agent asks for pets owned by a specific user, the MCP tool invokes the legacy REST endpoint. The response is converted into MCP tool output and returned to the agent. From the agent’s perspective, it is simply calling a tool. From the legacy system’s perspective, nothing changed. Step 1: Create the Quarkus MCP Server The first step is creating a lightweight Quarkus application that will run as the sidecar. Quarkus is a good choice here because it starts quickly, uses little memory, and works well in container environments. Create the project: quarkus create app org.example:petclinic-mcp-sidecar \ --extension=quarkus-rest-jackson,quarkus-rest-client-jackson,quarkus-mcp-server Extensions explained: * quarkus-rest-jackson – provides REST support and JSON serialization * quarkus-rest-client-jackson – allows the sidecar to call the legacy REST API * quarkus-mcp-server – exposes Java methods as MCP tools The application itself is intentionally simple. Its job is only to translate between MCP and the legacy API. Step 2: Generate the REST Client from the OpenAPI Specification The PetClinic application already exposes an OpenAPI specification. Instead of writing REST client code manually, Alex used IBM Bob to generate the client interfaces. The prompt simply referenced the OpenAPI URL and requested client interfaces for the required endpoints. The generated client looked like this: package org.example.petclinic.client; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import java.util.List; @Path("/pets") @RegisterRestClient(configKey = "petclinic-api") public interface PetService { @GET List listPets(); @GET @Path("/{petId}") Pet getPetById(@PathParam("petId") Long petId); @GET @Path("/owner") List getPetsByOwner(@QueryParam("ownerId") Long ownerId); } Because the sidecar runs inside the same pod, the base URL simply points to the legacy container: http://localhost:9966/petclinic/api There is no service discovery or ingress configuration required. Step 3: Expose MCP Tools Next we wrap the REST client inside MCP tools. The Quarkus MCP extension makes this very straightforward. You annotate Java methods with @Tool, and they become available to MCP clients automatically. Example: package org.example.petclinic.tools; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.rest.client.inject.RestClient; import io.quarkiverse.mcp.server.Tool; import java.util.List; @ApplicationScoped public class PetClinicTools { @Inject @RestClient PetService petService; @Tool(description = "List all pets in the clinic") public List listPets() { return petService.listPets(); } @Tool(description = "Get pet by ID") public Pet getPetById(Long id) { return petService.getPetById(id); } } This code does not contain any AI logic. It simply exposes the legacy API as structured tools that an agent can discover and call. The MCP framework handles the protocol layer automatically. Step 4: Deploy the Sidecar in Kubernetes The final step is deploying both containers in the same pod. The existing PetClinic deployment does not change significantly. We just add another container. apiVersion: apps/v1 kind: Deployment metadata: name: petclinic spec: template: spec: containers: - name: petclinic image: springcommunity/spring-petclinic-rest ports: - containerPort: 9966 - name: petclinic-mcp image: example/petclinic-mcp-sidecar ports: - containerPort: 8888 env: - name: QUARKUS_REST_CLIENT_PETCLINIC_API_URL value: http://localhost:9966/petclinic/api Both containers share the same network namespace. The sidecar talks to the legacy app through localhost. External clients talk only to the MCP endpoint. Step 5: Verify the Integration Once deployed, you can verify the MCP server using the MCP Inspector tool. The inspector will … This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit www.the-main-thread.com

About

Practical lessons on enterprise Java, software architecture, and AI coding agents for experienced developers, architects, and technical leads. Drawing on more than two decades in the industry, Markus Eisele explores what works, where tools fall short, and how to make better decisions about the software you build and maintain. www.the-main-thread.com