The Main Thread

Markus Eisele

Welcome to The Main Thread — your strategic companion for navigating the evolving world of enterprise Java, software architecture, and AI-infused systems. Curated by Markus Eisele, a veteran technologist with over two decades of industry experience, this publication connects the core ideas shaping our field today with the innovations that will define it tomorrow. Here, we go beyond frameworks. We explore the “why” behind your architectural choices, the “what if” of platform decisions, and the career implications of living at the intersection of Java, cloud, and intelligence. Because in a world of increasing complexity, the main thread isn’t just a technical construct — it’s a mindset. www.the-main-thread.com

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

    5d ago

    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

    5 min
  2. 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

    5 min
  3. API Versioning in Quarkus: The Part AI Doesn’t Solve

    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

    12 min
  4. 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 list the available tools: listPets getPetById getPetsByOwner Calling a tool should return the same data as the original REST endpoint. The difference is that the output now follows the MCP protocol. This makes it usable by any MCP-compatible AI client. Why This Pattern Matters The sidecar approach solves a problem many enterprises face right now. Companies want to experiment with AI agents, but they cannot risk destabilizing the systems that run their business. By isolating the MCP integration in a separate container, teams can move quickly without touching the legacy codebase. They can: * add new AI tools * improve prompts * integrate with new agent frameworks * iterate on output formats All while the underlying system stays unchanged. This is a powerful modernization strategy. Instead of rewriting legacy systems, we wrap them with intelligent interfaces. Conclusion Legacy applications are not going away. They contain critical business logic and valuable data. The challenge is connecting them to modern AI systems safely. The combination of Quarkus, MCP, and the Kubernetes sidecar pattern provides a practical solution. You expose legacy APIs as AI-friendly tools without modifying the original application. Alex Soto’s demo shows how quickly this can be implemented. With the help of tools like IBM Bob, the boilerplate between OpenAPI specifications and MCP tools can be generated in minutes. This approach allows teams to modernize incrementally while keeping their production systems stable. 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

    11 min
  5. From Standard to Smart

    Mar 9

    From Standard to Smart

    Today’s episode is a special one for me. I’m welcoming my friend and colleague Alex Soto to the show. If you follow the Quarkus ecosystem, you already know Alex. He has been shaping developer experience around Java and cloud-native for years. And today, he’s doing something very practical. He takes an existing Quarkus application. No greenfield. No clean slate. A real project. And he shows how to use IBM Bob to integrate LangChain4j step by step. This is important. Because most teams don’t start from zero. They have code. They have structure. They have production systems. The question is not “How do I build an AI app?” The question is: “How do I add AI to what I already have?” In this episode, Alex answers exactly that. There is a big difference between a CRUD application and an AI-ready application. At least, that’s what most developers think. You have your repository. You have your service layer. You expose everything through REST. It works. It scales. It follows clean architecture. End of story. But here’s the thing. Your users don’t think in endpoints. They don’t think in HTTP verbs. They think in sentences. In this episode, I show how we take a standard Quarkus booking application and turn it into a natural language assistant. We don’t rewrite it. We don’t redesign the architecture. We connect it to an LLM in a controlled way. And we use IBM Bob to guide the transformation. This article breaks down what actually happens under the hood. The Starting Point: A Classic Quarkus Service Our starting point is familiar. * A BookingRepository that talks to the database * A BookingService that implements business logic * A BookingResource exposing HTTP endpoints This is how most enterprise Java applications look. Clean separation. Clear boundaries. But there is friction. If a user wants to check a booking, they need to know: * The correct endpoint * The booking ID * The correct parameter format The system is precise. The user must be precise too. That works for APIs. It does not work for conversational interfaces. The goal is not to replace the existing architecture. The goal is to augment it. Step 1: Adding the AI Backbone The first real step is simple: we add LangChain4j to the project. Instead of manually browsing extensions and copying Maven coordinates, we told Bob: “Register dependency to use OpenAI.” Bob inspected the project and added: quarkus-langchain4j-openai This sounds trivial. It is not. Doing this step-by-step matters. When you instruct an AI assistant incrementally, it works with the real state of your project. It reduces hallucinations. It keeps changes scoped. It prevents massive diff explosions. This is how you should work with coding agents in production codebases. Small steps. Controlled context. Explicit instructions. Now the application is AI-capable. But not AI-integrated. Step 2: Defining the AI Service In Quarkus, an AI Service is your contract to the LLM. We asked Bob to create a BookingAssistant interface. What we got was more than a stub. Bob created: * System messages defining the assistant persona * Methods for chat interactions * Methods for cancellation explanations * Methods for confirmation generation The system message is critical. If you define: “You are a helpful booking assistant.” You shape the entire behavior of the model. Tone. Intent. Scope. Without this, the LLM behaves like a generic chatbot. With it, it behaves like a domain assistant. This is architecture, not decoration. You are defining boundaries for probabilistic behavior. Step 3: Giving the AI Real Power with Tools An LLM can generate text. It cannot read your database. That’s where Tools come in. Tools are simply Java methods exposed to the LLM with metadata. The model decides when to call them. We told Bob: “Expose the getBookingDetails method as a tool.” Bob did three important things. First, it annotated the existing method in BookingService with @Tool.Second, it generated a natural language description of what the method does.Third, it updated the assistant configuration so the LLM knows this capability exists. This is the real transformation. We did not duplicate business logic.We did not create parallel AI-only services.We reused existing domain code. Now when a user says: “Show me details for booking 123456” The model reasons about intent, decides that getBookingDetails is relevant, calls the tool, and incorporates the result into its answer. This is controlled augmentation. Your service remains the source of truth.The LLM becomes an orchestrator. Step 4: Closing the Loop with a Chat Endpoint Once the assistant exists and tools are registered, we need an entry point. Bob generated: * A new REST endpoint for chat interaction * A scaffolded application.properties with OpenAI configuration * Example configuration values (API key placeholder, model, temperature) One interesting detail. Bob scanned the existing test data and generated a cURL command using a real booking number found in the project. That means it understood the code context well enough to extract meaningful data. This is where AI-assisted development becomes practical. It does not just generate code. It connects dots inside your repository. What Actually Changed? Let’s be clear. We did not replace REST endpoints.We did not remove the service layer.We did not introduce magic. We added: * A LangChain4j extension * An AI Service interface * Tool annotations on existing business logic * A chat endpoint That’s it. The existing architecture stayed intact. This is important for enterprise systems. You cannot afford a rewrite just to add conversational access. You need incremental transformation. Architectural Insight: Standard and Smart Can Coexist There is a misconception that AI-first systems require a different architecture. In reality, the clean layering of a Quarkus application is ideal for AI augmentation. Your service layer already encapsulates domain logic.Your repository already isolates persistence.Your REST layer already defines boundaries. You just expose selected methods as tools and let the LLM orchestrate them. The intelligence does not replace structure. It sits on top of it. Why Use Bob for This? You could wire all this manually. But Bob helps with: * Discovering correct extensions * Generating consistent AI Service interfaces * Adding tool annotations correctly * Updating prompts and configuration * Generating example test calls The key benefit is not speed alone. It is correctness within context. Bob reads your project. It sees your packages. It understands your test data. It proposes changes that align with your codebase. That reduces the cognitive load when introducing AI features into legacy or existing systems. From CRUD to Conversational The gap between a standard application and an intelligent one is no longer architectural. It is connective. You already have the data.You already have the business logic.You already have clear domain boundaries. AI integration becomes a matter of: * Exposing logic as tools * Defining the assistant persona * Wiring a chat endpoint That’s the shift. You don’t build a new system. You make your existing system accessible through language. Final Thought Transforming a Quarkus application into an AI-ready assistant is not about hype. It is about controlled extension of your existing architecture. In the podcast episode, we walk through the full implementation live with IBM Bob. The takeaway is simple. You don’t need to throw away your Java services to make them smart.You just need to connect them the right way. 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

    9 min
  6. Feb 4

    Most Java Persistence Bugs Are Boring. Quarkus 3.31 Fixes Them at Compile Time.

    When Alex talks about persistence, he usually does not start with syntax. He starts with consequences. In his recent deep dive on Jakarta Data in Quarkus 3.31.0, the real story is not a new annotation or another repository interface. The story is about moving database correctness from runtime to compile time. This matters because most persistence bugs are boring. They are not deadlocks or exotic transaction anomalies. They are simple things: renamed fields, broken query strings, invalid sort orders, and mismatches between entities and queries that only show up after deployment. Jakarta Data, as implemented in Quarkus 3.31.0, attacks exactly this class of problems. From Panache Convenience to Jakarta Data Discipline Quarkus developers already know Panache. It made Hibernate ORM approachable by reducing boilerplate and offering both Active Record and repository styles. But Panache was always a Quarkus-specific abstraction. Powerful, yes. Standardized, no. With Jakarta EE introducing Jakarta Data, Quarkus had a choice: bolt the spec on top of the existing engine, or rethink the internals. The result is Panache 2.0, a new engine that keeps the developer ergonomics but implements Jakarta Data as a first-class, specification-driven model. The shift is subtle but important. You are no longer just using a Quarkus feature. You are writing against a standard that other runtimes can implement, while still benefiting from Quarkus’ aggressive compile-time optimizations. The Real Enabler: Hibernate’s Annotation Processor The most important technical detail in Alex’s session is not a repository annotation. It is the Hibernate annotation processor. Traditional Panache queries often rely on strings: find("name", "Alex") This looks harmless until someone renames name to fullName. The code still compiles. Tests might still pass. Production fails later. With the annotation processor enabled, Hibernate generates a static meta-model at build time. For a User entity, you get a User_ class with strongly typed references to every mapped attribute. If the field disappears or changes type, your code stops compiling. This is not a convenience feature. This is a correctness guarantee. Once you enable the processor, every query that uses the meta-model becomes refactoring-safe by construction. Jakarta Data Repositories: Interfaces, Not Implementations Jakarta Data repositories look deceptively simple. You define an interface, annotate it with @Repository, and extend CrudRepository. That is it. There is no implementation class. There is nothing to generate manually. At build time, Quarkus creates the implementation and wires it as a CDI bean. The important part is what does not exist anymore: * No handwritten repository implementations * No duplicated CRUD logic * No string-based query glue code Instead, you describe intent. Find by this field. Insert this entity. Update that aggregate. Quarkus handles the rest. Because repositories are normal CDI beans, they fit cleanly into service layers, transactional boundaries, and testing setups you already use. Type Safety Where It Actually Hurts Type safety is an overused term. In this case, it is very concrete. Sorting is a good example. In older code, you often see something like: "ORDER BY name DESC" This is legal Java. It is also fragile. With Jakarta Data and the generated meta-model, sorting becomes: Order.desc(User_.name) If name no longer exists, the compiler fails. The same applies to pagination, nested property navigation, and derived queries that walk object graphs. You get IDE completion, refactoring support, and early failure. This is especially relevant for large teams, where entity models evolve continuously and not everyone remembers which query strings exist in which module. Transparency Instead of Magic One concern many architects have with repository abstractions is loss of visibility. Alex explicitly addresses this. By enabling SQL logging with quarkus.hibernate-orm.log.sql=true, you can see exactly what SQL Jakarta Data generates. The output is clean, predictable, and optimized. There is no hidden runtime interpretation layer. Everything is resolved at build time. This is a recurring Quarkus theme: push work to build time, fail early, and make runtime behavior boring. Why This Matters for Enterprise Codebases Jakarta Data in Quarkus 3.31.0 is not about writing less code. It is about writing code that is harder to break accidentally. For enterprise systems with long lifetimes, frequent refactoring, and many contributors, compile-time guarantees matter more than clever APIs. Moving query validation, sorting correctness, and repository wiring to build time reduces an entire class of production failures. Panache 2.0 is the quiet engine behind this shift. Jakarta Data is the standard that makes it portable. Quarkus is the runtime that makes it practical. Jakarta Data in Quarkus does not try to reinvent persistence. It tightens it. By combining a specification-driven repository model with aggressive compile-time validation, Quarkus turns persistence into something that behaves more like modern Java code and less like a stringly typed DSL hiding in annotations. If you care about refactorability, correctness, and long-term maintainability, this is one of the most important changes in the Quarkus 3.x line. 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

    9 min
  7. What the 2025 U.S. AI Action Plan Means for Enterprise Java and ERP Systems

    07/30/2025

    What the 2025 U.S. AI Action Plan Means for Enterprise Java and ERP Systems

    The U.S. AI Action Plan, while aiming to accelerate AI innovation by reducing "red tape" and fostering private-sector growth, explicitly states it does not override critical existing regulations like HIPAA (for healthcare) and SOX (for financial reporting). Instead, it seeks to harmonize AI adoption with these legal obligations. The plan acknowledges that complex regulatory landscapes have slowed AI adoption in sensitive sectors like healthcare and finance. I have taken a deeper look at what this means for the industry and gave both the Action Plan and my interpretation to Googles NotebookLLM to create a nice overview. And here you go. Pretty spot on. Compare yourself and find my original interpretation on LinkedIn: What the 2025 U.S. AI Action Plan Means for Enterprise Java and ERP Systems Key takeaways from the plan's and my analysis: • No Override, But Harmonization: The plan emphasizes regulatory sandboxes and "AI Centers of Excellence" where new AI tools can be tested under supervisory frameworks, ensuring compliance with existing laws. NIST is tasked with developing national AI standards and evaluation guidelines that will incorporate HIPAA and SOX requirements for risk assessments, audit trails, and data protection. • Data Protection is Paramount: For HIPAA, the plan's focus on secure compute environments, privacy, and confidentiality for large datasets aligns directly with the need to protect Protected Health Information (PHI). AI applications must maintain the confidentiality, integrity, and availability of PHI. • Auditability for Financial Integrity: For SOX, internal controls and accurate financial reporting remain non-negotiable. AI tools used in financial reporting must provide audit trails, explainability, and error detection. • Embracing Open and Self-Hosted Models: Both HIPAA and SOX compliance are significantly supported by the plan's push for open-source and open-weight AI models. This is crucial because organizations handling sensitive PHI or financial data often cannot send this data to external, closed model vendors. Self-hosting models within a company's secure environment reinforces control over sensitive data. • Secure-by-Design and Incident Response: The plan promotes "secure-by-design" AI technologies to guard against threats like data poisoning and adversarial attacks. It also calls for AI-specific incident response plans, which are vital for SOX compliance to detect and report anomalies that could affect financial statements. • Integration with Existing Systems: For traditional ERP/Java back-ends, the plan signals a need to embed AI transparently into existing process workflows, ensuring robust audit logs to meet compliance requirements. Enterprise architects will need to adopt strong AI evaluation and governance frameworks and build secure infrastructure with robust data governance. 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

    5 min

About

Welcome to The Main Thread — your strategic companion for navigating the evolving world of enterprise Java, software architecture, and AI-infused systems. Curated by Markus Eisele, a veteran technologist with over two decades of industry experience, this publication connects the core ideas shaping our field today with the innovations that will define it tomorrow. Here, we go beyond frameworks. We explore the “why” behind your architectural choices, the “what if” of platform decisions, and the career implications of living at the intersection of Java, cloud, and intelligence. Because in a world of increasing complexity, the main thread isn’t just a technical construct — it’s a mindset. www.the-main-thread.com