Java is an object-oriented programming language that allows engineers to produce software for multiple platforms. Our resources in this Zone are designed to help engineers with Java program development, Java SDKs, compilers, interpreters, documentation generators, and other tools used to produce a complete application.
I Built a Java Version Manager by Fixing Other Tools' Open Bugs
Rethinking Java Design Patterns: From OOP to FP
The Java Story | The Official Documentary provides more than a retrospective on one of the most influential programming languages. Tracing Java’s journey from the Oak project at Sun Microsystems to its widespread adoption in enterprise systems, embedded devices, and global platforms, the documentary highlights how technical constraints, strategic decisions, and architectural choices shaped its evolution. For those interested in technology history, it serves as a valuable case study of how software transitions from experimentation to infrastructure, and how early engineering decisions continue to impact modern systems. The documentary’s significance reaches beyond the Java community. It examines enduring themes in software engineering, including portability, backward compatibility, platform design, ecosystem governance, standardization, and the interplay between technology and community. It also reframes innovation as an ongoing process of adaptation, compromise, and collaboration, rather than a single breakthrough. As such, The Java Story is recommended viewing for anyone interested in how technologies endure, evolve, and become foundational to entire industries. From Oak to Open Source Java originated in 1991 as Oak, a component of Sun Microsystems’ Green Project. Initially intended for consumer electronics, it was redirected to the emerging Web when that market did not materialize. Its promise of portability, later described as “Write Once, Run Anywhere,” challenged the idea that software must be tied to a specific operating system. Java’s growth coincided with the browser wars. Netscape partnered with Sun to integrate Java into its browser, while its own scripting language, first called Mocha and later LiveScript, was renamed JavaScript to leverage Java’s popularity. Although the two languages were not technically related, their names reflected strategic alliances as Sun and Netscape sought to limit Microsoft’s influence over desktop software and the Web. The unofficial phrase “In a world without fences, who needs Gates?” captured the spirit of this competition. Java was not initially open source by today’s licensing standards. The term open source was coined in 1998, three years after Java’s public release. However, Java introduced concepts aligned with the movement, including portability, publicly available specifications, shared implementations, and an ecosystem beyond a single vendor. Over time, the Java community became a leading force in open source. Projects such as Tomcat, Maven, Eclipse, Hibernate, and Spring demonstrated that open collaboration could include individual developers, universities, foundations, startups, and global companies. The release of OpenJDK under the GNU General Public License in 2006 completed this transition, establishing Java as both an open-source platform and a model for long-term collaboration among competing organizations. What Makes Java, Java Open source is no longer unique among programming languages, as many now publish their source code and accept community contributions. Java distinguishes itself by combining open-source implementation with open standards. This means Java is not only modifiable code, but also a platform defined by public specifications, compatibility requirements, and shared governance. This model supports a broad vendor ecosystem. Java enables multiple Java Virtual Machine implementations, development tools, distributions, cloud platforms, and enterprise runtimes. The same approach applies to Java EE and Jakarta EE, where independent vendors implement shared specifications. As a result, Java is not tied to the priorities or decisions of a single company. The Java Community Process is central to this structure. Through Java Specification Requests, the JCP offers a formal process for proposing, reviewing, and standardizing platform changes. While this approach may be slower than single-organization models, it ensures transparency, compatibility, and long-term stability. The combination of open source and open standards is a key strength of Java. It enables companies to compete through their implementations while collaborating on the platform. More broadly, the JCP serves as a model for technologies and organizations aiming to balance innovation, governance, vendor diversity, and sustainable evolution. Java’s Impact on the Software Industry Java did not invent the virtual machine, garbage collection, or object-oriented programming, but it helped introduce these concepts to mainstream commercial software development. Previously, programming languages were closely tied to specific operating systems and hardware. Java shifted this paradigm by making the Java Virtual Machine the primary execution target and promoting the promise of “Write Once, Run Anywhere.” This model showed that a virtual machine could deliver portability while still allowing ongoing performance improvements. Innovations like just-in-time compilation, adaptive optimization, and advanced garbage collectors made the JVM a highly optimized runtime. Java’s success also encouraged languages such as Kotlin, Scala, Clojure, and Groovy to adopt the JVM as their execution platform. Java also shaped software design, testing, and documentation practices. JUnit established automated unit testing as a standard and inspired the broader xUnit framework family. JavaDoc made generating reference documentation from source code routine. Additionally, Java books, design patterns, and community practices promoted object-oriented principles to a generation of software engineers. While Java was initially associated with object-oriented programming, the platform expanded to support multiple paradigms. Features such as generics, annotations, lambda expressions, functional-style streams, reflection, and concurrency APIs enabled developers to use object-oriented, functional, declarative, and metaprogramming techniques. Java’s broader impact lies not only in its features but also in how it reshaped expectations for portability, managed runtimes, testing, documentation, and language evolution. Becoming Part of the Java Story Being included in the documentary, even briefly, is deeply meaningful to me. The footage is from when I received a JCP Award, but the moment’s significance extends well beyond the award itself. Since Java 8, I have served on the JCP Executive Committee, participated in several Java Specification Requests, and contributed to discussions that shaped the evolution of the Java platform. Open source and Java transformed my understanding of software, community, and the possibilities of a technical career. Through the Java community, I learned from those who created and shaped the platform. I improved my software design and implementation skills, expanded my professional network, accessed opportunities beyond my local market, and built an international career. I also contributed to the transformation of enterprise Java into Jakarta EE, helping to shape the next generation of specifications for cloud-native and enterprise applications. This journey developed more than just my technical skills. Community participation taught me to communicate complex ideas, write clearly, speak publicly, collaborate across cultures, and contribute constructively amid differing opinions and interests. These skills enabled me to participate in both implementation work and in technology boards and strategic discussions that shape the future of platforms and standards. Open source also encouraged me to improve my English, learn new languages, and build friendships that have become like family. I encourage you to watch the documentary, but do not stop there. Join a Java user group, attend a conference, contribute to an open-source project, participate in a specification, or start a conversation with someone in the community. Java’s story was built by those who chose to participate, and its next chapter will be written the same way. Conclusion Studying Java’s history offers insight into how software has transformed society. Java shaped not only programming languages and enterprise systems, but also the infrastructure supporting business, government, communication, finance, education, and daily digital services. Its story shows that software engineers do more than write code. By building systems, standards, and communities, they help shape how the world functions. I hope this documentary inspires you as it did me. My journey with Java began around Java 8, when I joined the JCP Executive Committee and learned firsthand about the decisions, people, and challenges that shaped the platform before my involvement. I am grateful this history is now shared in such an engaging way. Watch the documentary, explore the community, and consider joining us — not only to understand Java’s history, but to help shape its future.
In high-volume, enterprise Java applications, business logic has a natural tendency to degrade into procedural complexity. You start with a straightforward task, such as calculating a discount for a pharmacy claim or evaluating a financial transaction. And before long, the core service method transforms into a multi-hundred-line monolith choked with nested if-else branches and brittle switch statements. This code smell is more than just an eyesore; it creates significant technical debt. It is exceptionally difficult to unit test, violates fundamental object-oriented design principles, and introduces severe regression risks where adding a single business rule threatens to break three existing ones. When evaluating software architecture, a foundational principle stands clear: If you are explicitly checking an object's type or status flag to determine how to execute business logic against it, your code is violating encapsulation. In a modern, cloud-native architecture, software should be open for extension but closed for modification (The Open-Closed Principle). The most elegant weapon for achieving this balance is the Strategy Design Pattern. The Anti-Pattern: Procedural Control Flow Consider a standard enterprise implementation of a pharmacy claim discount calculator. A junior approach typically relies on conditional routing strings hardcoded into the execution path: Java public class LegacyClaimService { public double calculateDiscount(Claim claim) { if (claim.getType() == null) { return 0.0; } // Brittle conditional routing if (claim.getType().equals("SENIOR")) { return claim.getAmount() * 0.20; } else if (claim.getType().equals("VETERAN")) { return claim.getAmount() * 0.15; } else if (claim.getType().equals("CHRONIC_CARE")) { return claim.getAmount() * 0.10; } else { return 0.0; } } } Every time the business team introduces a new discount category, an engineer must manually check out this core service file, append a new conditional branch, alter the monolithic execution path, and run a full regression test suite across every single unrelated discount type. This is an operational bottleneck that scales poorly. Refactoring Pattern 1: Functional Enums for Lightweight Strategies For stateless, mathematical, or rule-based routing, Java Enums can be combined with Functional Interfaces to build highly optimized, self-contained strategy catalogs. By declaring an abstract interface and passing Java 8+ lambdas directly into the enum constants, we cleanly encapsulate the logic exactly where it belongs. First, define the explicit behavioral contract: Java @FunctionalInterface public interface DiscountStrategy { double apply(double amount); } Next, implement the strategy blueprint within a structured Enum, incorporating a defensive lookup mechanism to protect against system crashes: Java import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; public enum ClaimDiscount implements DiscountStrategy { SENIOR(amount -> amount * 0.20), VETERAN(amount -> amount * 0.15), CHRONIC_CARE(amount -> amount * 0.10), DEFAULT(amount -> 0.0); private final DiscountStrategy strategy; ClaimDiscount(DiscountStrategy strategy) { this.strategy = strategy; } // Static optimization cache to prevent continuous array cloning via values() private static final Map<String, ClaimDiscount> LOOKUP_MAP = Arrays.stream(values()) .collect(Collectors.toMap(ClaimDiscount::name, e -> e)); /** * Defensive lookup pattern to prevent runtime IllegalArgumentExceptions */ public static ClaimDiscount fromType(String type) { if (type == null) { return DEFAULT; } return LOOKUP_MAP.getOrDefault(type.toUpperCase(), DEFAULT); } @Override public double apply(double amount) { return this.strategy.apply(amount); } } With this infrastructure in place, your core orchestration service simplifies down to a readable, self-documenting implementation: Java public class ModernClaimService { public double getFinalPrice(Claim claim) { return ClaimDiscount.fromType(claim.getType()) .apply(claim.getAmount()); } } Refactoring Pattern 2: Spring-Managed Component Strategies While functional enums work perfectly for stateless calculations, production enterprise applications frequently require strategies that interact with stateful infrastructure, such as querying external databases, invoking REST clients, or accessing cloud caches. For these heavy, stateful operations, you can combine the Strategy Pattern with Spring's dependency injection framework to build a dynamic plugin registry. Define the Stateful Contract Java public interface ComplexValidationStrategy { boolean validate(Claim claim); String getStrategyName(); } //Step 2: Implement Component Strategies import org.springframework.stereotype.Component; @Component public class AdjudicationValidationStrategy implements ComplexValidationStrategy { // Spring automatically injects required infrastructure beans locally private final DatabaseRepository repo; public AdjudicationValidationStrategy(DatabaseRepository repo) { this.repo = repo; } @Override public boolean validate(Claim claim) { return repo.checkAdjudicationHistory(claim.getClaimId()); } @Override public String getStrategyName() { return "ADJUDICATION"; } } @Component public class CoPayValidationStrategy implements ComplexValidationStrategy { @Override public boolean validate(Claim claim) { // Stateful co-pay validation logic goes here return claim.getAmount() > 0; } @Override public String getStrategyName() { return "COPAY"; } } Architect the Dynamic Strategy Registry Spring natively supports injecting all implementations of an interface directly into a collection. By using a configuration bean or a service constructor, you can map these components programmatically into a map lookup: Java import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @Service public class ClaimValidationOrchestrator { private final Map<String, ComplexValidationStrategy> registry; // Spring auto-injects every class implementing ComplexValidationStrategy into this List public ClaimValidationOrchestrator(List<ComplexValidationStrategy> strategies) { this.registry = strategies.stream() .collect(Collectors.toMap( ComplexValidationStrategy::getStrategyName, Function.identity() )); } public boolean executeValidation(String strategyType, Claim claim) { ComplexValidationStrategy selectedStrategy = registry.get(strategyType.toUpperCase()); if (selectedStrategy == null) { throw new IllegalArgumentException("No valid strategy registered for type: " + strategyType); } return selectedStrategy.validate(claim); } } Production Engineering Considerations Avoiding Performance Pitfalls with Enum.values(): In high-concurrency processing environments, avoid calling Enum.values() or Enum.valueOf() directly within incoming execution loops. Every call to MyEnum.values() forces the JVM to allocate a brand-new array footprint under the hood to preserve array mutability. Always utilize a static, pre-cached map lookup to ensure 0(1) constant-time performance overhead.Granular Unit Testing: By decoupling your validation or execution logic into separate strategy classes or functional constants, you can bypass heavy integration testing bootstrap processes. You no longer need to spin up a complete Spring Boot web context or use complex Mockito frameworks just to verify a simple business calculation rule. Each strategy variant can be verified via isolated, fast-running unit tests.Concurrency and Thread Safety: When utilizing Spring-managed component strategies, keep in mind that Spring beans are singletons by default. Ensure your strategy implementations remain entirely stateless regarding the request context. Pass all volatile transaction data strictly through the method parameters rather than class-level fields. Architectural Strategy Matrix Feature Legacy If-Else Routing Strategy Pattern Architecture Code Readability Low (Choked with Spaghetti loops) High (Clean, self-documenting layers) Extensibility Path Risky (Modifies compiled source files) Safe (Appends isolated classes/constants) Testing Footprint Complex (Requires mocking massive contexts) Minimal (Simple, targeted unit verifications) Execution Performance Linear degradation via String comparison Optimized hash map map lookup () Framework Integration Procedural conditional structures Native inversion-of-control compliance Summary Clean programming isn't defined by how much complex code you can fit into a single method; it is defined by how much code you can safely extend without rewriting existing foundations. By extracting chaotic conditional business rules out of your core services and encapsulating them into interchangeable, modular strategies, you build a system designed for change. This level of true architectural decoupling is the secret ingredient that transforms standard microservice components into resilient, enterprise-grade production platforms.
AI code generation tools are fantastic at writing isolated snippets of code, but they quickly fall short when they need to understand a running application's state. When a compiled class fails, or a local database container drops, standard AI coding assistants are left guessing. They lack runtime context, environment visibility, and any real-time connection to your active local development workspace. The Model Context Protocol (MCP) bridges this gap by standardizing how AI applications interact with local tools. By leveraging the standalone quarkus-agent-mcp server, you can turn your local AI coding companion into a "living" pair programmer that can build, configure, and debug your Quarkus applications in real time. Why AI Needs a Standalone Agentic Connection Standard code assistants operate entirely out-of-band. They read your static source files and generate code based on pre-trained patterns. They cannot interact with your running JVM, read console logs, or probe your local environment. This creates a tedious loop of copying terminal errors, pasting them into a chat window, receiving speculative fixes, and repeating the cycle. While Quarkus offers an in-app Dev MCP server, it suffers from a fundamental limitation: it lives inside the running application process. If your code fails to compile or crashes on startup due to a missing bean, a bad database migration, or a broken dependency, the application dies — and the in-process MCP server dies with it. The agent loses its connection and is left completely blind. The quarkus-agent-mcp server solves this by running as a completely standalone, always-available process. It wraps your active quarkus dev session as a managed child process. If the application crashes, the agent server survives, allowing the AI to inspect the compiler output, diagnose the error, and execute a fix. Core Capabilities of the Quarkus Agent By exposing a standardized control plane to tools like Claude Code, IBM Bob, Cursor, and GitHub Copilot, the standalone agent server unlocks powerful automations: Project scaffolding: The quarkus_create tool can build a brand-new application from scratch. You can tell your agent to "create a Quarkus REST API with PostgreSQL," and it will select the right extensions, bootstrap the build system, and start dev mode automatically.Lifecycle control: The agent can programmatically start, stop, and restart your dev mode applications. It handles Maven and Gradle wrappers seamlessly under the hood.Dev MCP proxying: The standalone server proxies calls directly to the internal Dev UI. This allows the AI to trigger unit tests, inspect exposed REST endpoints, and manage dev services.Semantic documentation search: Instead of making up imaginary APIs, the agent can use semantic search (quarkus_searchDocs) to parse local, pre-indexed documentation. Markdown ┌──────────────────────────────────────────────────────────┐ │ Your IDE with AI assistant │ └────────────────────────────┬─────────────────────────────┘ │ Local JSON-RPC via MCP ▼ ┌──────────────────────────────────────────────────────────┐ │ Quarkus Agent MCP (Standalone Server) │ └────────────────────────────┬─────────────────────────────┘ │ Process Mgmt & Dev UI Proxy ▼ ┌──────────────────────────────────────────────────────────┐ │ Your Running Quarkus App │ └──────────────────────────────────────────────────────────┘ Bootstrapping Your Agentic Setup With JBang Getting started with quarkus-agent-mcp is incredibly straightforward, especially if you use JBang. You do not need to compile custom helper jars or configure complicated environment paths. You can boot the local MCP server directly from your terminal: Shell jbang quarkus-agent-mcp@quarkusio --port 8080 --project-dir ./my-quarkus-app Once the server is running, you can connect your preferred AI agent. For instance, if you are using Claude Code, you can register the local tool server using standard input/output transport: Shell claude mcp add quarkus-agent -- jbang quarkus-agent-mcp@quarkusio The editor automatically discovers the tool mappings, allowing the agent to safely read, modify, and manage your local workspace. Skills Before Code: Guiding Your Assistant One of the most powerful paradigms introduced by the Quarkus Agent is "skills before code". An agent can read specific extension skills via the quarkus_skills tool to learn optimal development patterns, common pitfalls, and testing practices before writing a single line of Java. You can define custom, domain-specific skills using a simple SKILL.md markdown file in your repository. This acts as the source of truth for the AI assistant, ensuring it follows your team's architectural standards instead of guessing. Java package com.example.skills; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.mcp.runtime.annotations.Tool; @ApplicationScoped public class CorporateArchitectureSkills { @Tool(name = "scaffold_rest_endpoint", description = "Generates a standardized, secure Quarkus REST resource.") public String scaffoldRestEndpoint(String entityName, String path) { return """ package com.example.api; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.transaction.Transactional; @Path("%s") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class %sResource { @POST @Transactional public void create(%s entity) { // Enterprise persistence patterns } } """.formatted(path, entityName, entityName); } } When the AI assistant needs to create a new endpoint, it bypasses generic web training data, references your corporate skill, and directly calls your custom tool. The result is clean, company-compliant Java code on the first attempt. Real-World Scenario: Automated Crash Recovery Let’s trace a common development headache: a failing database migration. Imagine you are working on a service that depends on a PostgreSQL database. You write a new JPA entity, but make a typo in your Liquibase migration file, causing the Quarkus application to crash on startup. Normally, this halts your momentum. You have to hunt down the stack trace in your terminal, locate the broken SQL block, search the documentation, fix the typo, and rebuild. With the standalone quarkus-agent-mcp managing your workspace, the recovery loop is entirely automated: Surviving the crash: While the application fails and shuts down, the standalone agent server remains running.Locating the bug: The agent recognizes the crash and calls the Dev MCP proxy tool devui-exceptions_getLastException. This returns a clean JSON payload containing the exception class, the exact error message, and the specific file path.Applying the fix: Using the precise error location, the assistant opens the Liquibase migration file, corrects the syntax, and saves it.Restarting and verifying: The agent calls quarkus_start to reboot dev mode. It monitors the application log stream (quarkus_logs) to verify that the database connection successfully initializes and the app is ready for testing. Managing Workspace Security and Privacy A common concern with local agentic execution is security. Because the server is standalone and interacts over a secure stdio channel or local loopback HTTP interface, your code remains private. The Quarkus Agent MCP runs entirely on your local machine. It does not harvest telemetry or transmit your source code to third-party endpoints. Network outbound calls are strictly gated — limited only to querying Maven Central for public extensions, pulling documentation updates, or fetching dependency updates. You have complete authority over which local tools you expose, creating a secure sandbox for AI pair programming. Summary The local developer experience is evolving rapidly. By integrating the standalone quarkus-agent-mcp with your environment, you move far beyond basic code completion. You create a highly collaborative assistant that understands your running JVM, leverages local documentation, and can actively recover from application crashes. This integration proves that modern Java with Quarkus is uniquely suited to lead the future of agentic AI development. Check out more from my series here.
The upcoming release of the July 28 Model Context Protocol (MCP) specification is a massive milestone for AI integration. By shedding the baggage of stateful connections and embracing a streamlined, stateless HTTP paradigm, MCP has finally become enterprise-ready. Developers can now build highly scalable, decentralized AI tool networks that integrate directly with enterprise data. However, statelessness and flexibility come with a distinct set of trade-offs. The newly introduced capabilities — specifically custom _meta payload objects, dynamic parameter routing, and x-mcp-header mapping — have opened up novel, highly sophisticated attack vectors. If your AI agents can execute code, query databases, or access internal APIs, security cannot be an afterthought. When building these tool gateways in Java, Quarkus provides the ideal framework to defend your infrastructure. Its reactive architecture, strict build-time validation, and enterprise-grade security integrations allow you to intercept, sanitize, and authorize requests before they ever touch your business logic. The New MCP Threat Landscape: Understanding the Attack Vectors In older stateful models, security was heavily reliant on the transport layer and long-lived socket authentication. With the new stateless paradigm, every incoming HTTP request is self-contained. While this makes load-balancing trivial, it shifts the entire security burden to the application layer. Attackers targeting MCP implementations generally exploit three primary vulnerabilities: 1. Protocol Confusion and Header Desynchronization The July 28 specification allows client tool arguments to be mapped directly to HTTP headers (using the x-mcp-header format). This is highly convenient for routing, but it presents a serious risk of "Protocol Confusion." If an attacker manipulates the client-side host to send an HTTP header that contradicts the JSON-RPC payload in the request body, a naive server might authorize the request based on the header but execute a completely different, unauthorized command payload. 2. Metadata Injection via _meta To support stateless sessions, tracing, and custom client context, the new protocol allows client hosts to pass arbitrary JSON data within a _meta parameter. If your Java backend trusts this metadata blindly — for instance, using it to route requests, dynamically construct database queries, or populate system logs — you open the door to classic injection attacks, log forging, and remote code execution (RCE). 3. Privilege Escalation through Prompt Manipulation Large language models (LLMs) are notoriously susceptible to prompt injection. If an attacker tricks an LLM into calling your Quarkus-hosted tool with altered parameters, the LLM will act as a proxy attacker. Without strict validation boundaries at the Java API layer, the LLM can execute commands with the privileges of your application's service account. Markdown [Attacker] ──(Prompt Injection)──> [LLM Host] ──(Manipulated Payload)──> [Quarkus MCP Server] ──(Unauthorized Access)──> [Internal Database] ▲ [Strict Validation Boundary] Defensive Strategy 1: Strict Input Sanitization with Bean Validation The first line of defense is ensuring that no unvalidated data ever reaches your database or internal services. Quarkus integrates seamlessly with Hibernate Validator (Jakarta Bean Validation), allowing you to define declarative, bulletproof rules directly on your data transfer objects (DTOs). Because LLMs can easily generate unexpected, highly erratic JSON payloads, your DTOs must enforce strict limits on string lengths, formats, and unexpected fields. Here is an example of a hardened tool-execution payload model: Java package com.example.mcp.security; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public class HardenedCustomerLookupArguments { @NotNull(message = "Customer ID is required") @Size(min = 8, max = 12, message = "Customer ID must be between 8 and 12 characters") @Pattern(regexp = "^CUST-[0-9]{4,8}$", message = "Invalid Customer ID format") private String customerId; @Size(max = 100, message = "Context query exceeds maximum allowed length") @Pattern(regexp = "^[a-zA-Z0-9\\s,._-]*$", message = "Query contains forbidden special characters") private String traceContext; // Getters and Setters public String getCustomerId() { return customerId; } public void setCustomerId(String customerId) { this.customerId = customerId; } public String getTraceContext() { return traceContext; } public void setTraceContext(String traceContext) { this.traceContext = traceContext; } } By enforcing alphanumeric patterns and explicit length boundaries, you block malicious payloads (such as SQL injection snippets or directory traversal paths) long before your application logic processes them. Defensive Strategy 2: Blocking Desync Attacks With Reactive Filters To prevent protocol confusion, you must guarantee that the incoming HTTP headers perfectly match the JSON-RPC execution arguments. Quarkus's reactive architecture allows us to write non-blocking ContainerRequestFilter implementations that intercept the HTTP request, extract the payload, and perform this crucial validation step at the gateway boundary. The following filter verifies that the Mcp-Name HTTP header exactly aligns with the method called in the JSON-RPC body, rejecting any mismatched requests: Java package com.example.mcp.security; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.MediaType; import java.io.ByteArrayInputStream; import java.io.IOException; import io.vertx.core.json.JsonObject; @Provider @McpSecureBoundary public class McpHeaderValidationFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { String mcpHeaderMethod = requestContext.getHeaderString("Mcp-Name"); if (mcpHeaderMethod == null || mcpHeaderMethod.isBlank()) { abortWithBadRequest(requestContext, "Missing required Mcp-Name header"); return; } // Read and buffer the entity stream for validation byte[] bodyBytes = requestContext.getEntityStream().readAllBytes(); requestContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); try { JsonObject bodyJson = new JsonObject(new String(bodyBytes)); String bodyMethod = bodyJson.getJsonObject("params").getString("name"); // Mitigate Protocol Confusion: Both values must align perfectly if (!mcpHeaderMethod.equals(bodyMethod)) { abortWithBadRequest(requestContext, "Protocol Desync: Header and body method mismatch"); } } catch (Exception e) { abortWithBadRequest(requestContext, "Malformed JSON payload"); } } private void abortWithBadRequest(ContainerRequestContext context, String message) { context.abortWith(Response.status(Response.Status.BAD_REQUEST) .type(MediaType.APPLICATION_JSON) .entity("{\"error\": \"" + message + "\"}") .build()); } } Defensive Strategy 3: Zero-Trust Authentication via OIDC and OAuth 2.1 Because MCP tools execute high-privilege actions on internal systems, you must verify the identity of the invoking agent host. The July 28 specification recommends OAuth 2.1 paired with Proof Key for Code Exchange (PKCE) for the authorization flow. Quarkus provides first-class support for securing endpoints with OpenID Connect (OIDC). By importing the quarkus-oidc extension, you can easily turn your stateless MCP server into a secure resource server that validates JSON Web Tokens (JWTs) issued by enterprise identity providers like Keycloak, Okta, or Microsoft Entra ID. Enforcing security is simple. First, define the configuration in your application.properties: Properties files quarkus.oidc.auth-server-url=https://identity.your-enterprise.com/realms/mcp-realm quarkus.oidc.client-id=mcp-gateway-service quarkus.http.auth.permission.mcp.paths=/mcp/v1/* quarkus.http.auth.permission.mcp.policy=authenticated Then, secure your execution endpoints using standard Java annotations: Java package com.example.mcp; import jakarta.annotation.security.RolesAllowed; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import io.smallrye.mutiny.Uni; @Path("/mcp/v1") public class SecureMcpResource { @POST @Path("/tools") @RolesAllowed("ai-agent-role") public Uni<McpResponse> executeSecureTool(McpRequestPayload payload) { // This logic is completely secured under OAuth 2.1 return Uni.createFrom().item(new McpResponse("Authorized data access achieved.")); } } Summary The transition to a stateless Model Context Protocol represents a massive architectural leap forward, but it demands an equally sophisticated security posture. Unsanitized metadata, header-to-body desynchronization, and injection vulnerabilities can easily turn a powerful AI assistant into a severe liability. By taking advantage of Quarkus’s robust security landscape — including declarative validation, reactive filters, and native OIDC support — Java developers can comfortably build hardened, production-ready MCP gateways that protect critical enterprise assets, enforce access control, and mitigate modern AI-driven threats. Check out more from my series here.
The Hidden Cost of API Versioning Hell Continuous API evolution is non-negotiable in contemporary software development, yet maintaining backward compatibility remains an incredibly expensive and labor-intensive hurdle. Core schema mutations frequently force downstream enterprise clients into disruptive and unplanned refactoring cycles, stalling product velocity. The typical industry fix — maintaining multiple, hard-coded API routes (e.g., /v1, /v2) — inevitably results in severe codebase sprawl, fractured engineering focus, and massive technical debt for the API provider. To break this cycle, this article outlines raqs (Response Agnostic Query System): a novel, dynamic proxy architecture designed to eliminate client-side disruption entirely. By intercepting traffic and executing on-the-fly schema transformations, raqs allows legacy clients to request data against deprecated contracts while the core upstream backend remains free to evolve. The raqs Solution: A Bifurcated Architecture Running complex natural-language processing or machine-learning inference directly within a high-throughput network routing path is typically a recipe for catastrophic latency. To solve this, raqs splits the network and intelligence layers into two distinct operational planes: The Orchestration Plane (Java 21/Spring Boot): Acting as the primary ingress proxy, this layer intercepts requests, manages multi-tier cache retrieval, handles distributed synchronization, and executes structural JSON transformations. The Inference Plane (Python/FastAPI): Operating as a probabilistic fallback mechanism, this agent calculates semantic and structural relationships between schema keys only when a deterministic mapping rule is missing. Core Architectural Decision Matrix ComponentNaive/Standard Approachraqs ImplementationConcurrency ManagementOS Thread Pooling (Tomcat Defaults) Java 21 Virtual Threads (Project Loom) SynchronizationPolling / Thread.sleep() loop Redisson Distributed Locking (Pub/Sub) Caching TierSingle-node In-Memory Cache Multi-tier (Caffeine L1 + Redis L2) Semantic MappingPure Semantic Models (LLM/Dense Vector) Hybrid Ensemble (Vector + Lexical Distance) Scaling Imperatively With Java 21 Virtual Threads The Orchestration Plane must handle thousands of concurrent client requests while checking caches, holding locks, or awaiting responses from the Inference Plane. The traditional platform-thread pooling model introduces massive operating system overhead and memory footprint under heavy I/O saturation. By building on Java 21 virtual threads (Project Loom), raqs assigns a lightweight, user-mode virtual thread to every single request lifecycle. When a thread encounters an L1/L2 cache miss, it is gracefully unmounted from its underlying OS carrier thread. The carrier thread is freed to handle other active network traffic, while the suspended virtual thread waits to resume once the schema mapping becomes available. This allows us to write straightforward, blocking imperative code that scales out with the efficiency of complex reactive systems. Defeating Cache Stampedes: The "Hero Thread" Pattern A major architectural risk for dynamic proxies is the cache stampede (or thundering herd problem). If a rolling backend deployment instantly mutates 50 schema keys, a burst of 1,000 concurrent client requests will simultaneously experience an L1/L2 cache miss. Without intervention, this triggers a massive wave of redundant, CPU-heavy inference calls that can completely crash the system. We mitigate this by implementing the "Hero Thread" pattern utilizing Redisson distributed locks: Java // Conceptual implementation of the Hero Thread pattern in the Orchestration Plane String lockKey = "lock:schema:" + legacyVersion + ":" + upstreamVersion; RLock distributedLock = redissonClient.getLock(lockKey); // Check L1/L2 cache first MappingRule mapping = cacheManager.getMapping(legacyVersion, upstreamVersion); if (mapping == null) { // Attempt to acquire the distributed lock via Redis Pub/Sub mechanisms if (distributedLock.tryLock()) { try { // The "Hero Thread" has the lock and invokes the Inference Plane mapping = inferenceClient.fetchProbabilisticMapping(legacySchema, upstreamSchema); cacheManager.populateCaches(legacyVersion, upstreamVersion, mapping); } finally { distributedLock.unlock(); } } else { // Non-hero threads are suspended by Loom and wait for cache population mapping = waitForCacheOrRetry(legacyVersion, upstreamVersion); } } return transformJsonPayload(rawResponse, mapping); By enforcing this structure, exactly one thread (the "Hero Thread") takes the computational penalty of invoking the ML Inference Plane. The remaining 49 or 999 concurrent threads are cleanly suspended by Loom, waking up via Redis Pub/Sub to read the finalized, cached ruleset. Pragmatic AI: Why "Pure Semantic" Models Fail During initial prototyping, we found that relying solely on dense vector embeddings (like Cosine Similarity) for short JSON dictionary keys yields dangerous false-positive collisions. For instance, a dense vector model will frequently map the legacy key firstName directly to a new key named lastName because they share highly overlapping linguistic contexts within general training data. To prevent silent data corruption, raqs uses a Hybrid Ensemble Scoring Model that evaluates both semantic meaning and lexical structure: Semantic evaluation: Keys are projected into a vector space using the all-MiniLM-L6-v2 transformer model, calculating Cosine Similarity S_semantic. Lexical evaluation: To account for common developer syntax changes (such as camelCase to snake_case), we compute the normalized Levenshtein distance S_lexical. Through empirical calibration, we fixed the hyperparameters at W_semantic = 0.7 and W_lexical = 0.3. If the combined score fails to clear a strict acceptance threshold (e.g., 0.80), the mapping is rejected. Ensemble Scoring Dynamics in Action Legacy KeyNew KeySemantic ScoreLexical ScoreEnsemble ResultfirstNamefirst_name0.950.88 0.929 (Accept)userIdaccount_id0.820.40 0.694 (Reject)firstNamelastName0.880.55 0.781 (Reject)zipCodepostalCode0.890.60 0.803 (Accept) As shown above, a pure semantic evaluation would have mistakenly accepted firstName as lastName due to its high 0.88 similarity vector. The 30% lexical penalty successfully suppresses the final score below the 0.80 threshold, preserving data integrity. Performance Telemetry and Benchmarks To test the efficacy of this architecture, we subjected the raqs proxy to a load test of 1,000 requests with a concurrency cap of 50, simulating a sudden, zero-knowledge v1-to-v2 upstream schema evolution on a standard CPU-bound host machine. The cold start: Upon initialization against an empty cache, the Redisson distributed lock correctly isolated the thundering herd. Exactly one thread executed the Hybrid ML Inference, completing in 504.65 ms. The blocked threads: The remaining 49 concurrent threads were safely unmounted from OS carrier threads by Loom, waiting for lock release via Pub/Sub and completing with an average latency of 554.24 ms. The steady state: Once the rules were promoted to the Caffeine (L1) and Redis (L2) caches, the subsequent 950 requests bypassed the Inference Plane entirely. The Orchestration Plane achieved an outstanding steady-state processing latency of just 10.25 ms ($\sigma = 2.19\text{ ms}$). This performance distribution demonstrates that the computational cost of machine learning inference can be entirely isolated to cold starts, making real-time, dynamic API translation exceptionally practical for enterprise-scale traffic. The Path Forward API evolution shouldn't force a broken trade-off between breaking client applications or drowning in a versioned codebase sprawl. By pairing the non-blocking concurrency of Java 21 with a highly disciplined, multi-tier distributed proxy, we can build data layers that adapt dynamically to contract shifts. Future iterations of this paradigm will expand beyond simple key mutations to incorporate deep structural payload transformations, JSON path awareness, and automatic data type coercion. Key Takeaways Eliminate versioning sprawl: Engineers can reduce the overhead of traditional API versioning by introducing a dynamic proxy that maps evolving schemas to legacy expectations on-the-fly. Scale imperatively via Java 21: Virtual Threads (Project Loom) allow high-throughput routing middleware to scale using a readable thread-per-request model without heavy reactive frameworks. Implement the "Hero Thread" pattern: Utilizing Redisson distributed locking ensures that expensive schema inference tasks are executed exactly once during high-traffic evolution events. Deploy pragmatic hybrid scoring: Combining dense vector embeddings with normalized Levenshtein distance drastically reduces false-positive mapping collisions. Achieve sub-15ms latency: Decoupling high-latency inference from the routing path ensures that 95% of steady-state traffic experiences near-native performance.
The Model Context Protocol (MCP) completely changed how we connect large language models to real-world data and tools. However, early versions of the protocol had a massive bottleneck for enterprise developers: they relied heavily on stateful, long-lived sessions. If you wanted to scale out your AI tools to handle thousands of concurrent agent workflows, you had to deal with sticky sessions, complex load balancing, and heavy memory overhead. The newest updates to the MCP specification solve this problem by introducing a completely stateless HTTP foundation. By removing the traditional initialization handshake and session IDs, MCP servers can now function as lightweight, independent microservices. When you combine this stateless evolution with cloud-native Java, you get the ultimate stack for cloud-native AI infrastructures. Why Stateless MCP Matters for Your Cloud Architecture In older stateful setups, an LLM host maintained an open connection to your server. If that specific server instance crashed or scaled down, the entire context of the conversation loop was lost. The latest specification shifts the paradigm. Every request sent from an AI agent or LLM host to an MCP server is now fully self-contained. The routing relies on two standard HTTP headers: Mcp-Method: Specifies the action (such as executing a tool or fetching a resource)Mcp-Name: Directs the request to the specific tool definition. Because the server no longer needs to remember who is calling it, you can place a standard load balancer in front of a cluster of MCP servers, distribute incoming requests evenly, and scale down to zero when traffic stops. The Cloud-Native Java Advantage: High-Density AI Tools While languages like Python and Node.js are popular in the AI space, they often struggle with heavy production workloads, multi-threading, and deep enterprise integration. Traditional Java solves these enterprise issues but comes with a high memory footprint and slower startup times—making it expensive to run as serverless microservices. This is exactly where cloud-native Java (e.g., Quarkus) shines. By utilizing ahead-of-time (AOT) compilation and GraalVM native images, Quarkus strips away the boilerplate runtime overhead. Plain Text ┌─────────────────────────────────────────────────────┐ │ Traditional Java MCP: ~150MB Ram | 2.5s Startup │ └─────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────┐ │ Cloud-Native Java MCP: ~18MB Ram | 0.015s Startup │ └─────────────────────────────────────────────────────┘ Instead of a single heavy backend trying to host dozens of different LLM tools, you can break your tools into highly specialized microservices. You can deploy a database-lookup tool, an internal API proxy, and a document parser as completely separate cloud-native Java applications. They will start instantly, use less than 20MB of RAM each, and scale up instantly when an AI agent triggers them. Building a Stateless MCP Resource With Cloud-Native Java Implementing a stateless tool in cloud-native Java with Quarkus is remarkably clean. By leveraging the reactive routing capabilities of Quarkus and standard Java objects, you can map the incoming JSON-RPC payloads directly to your business logic. Here is a conceptual example of how a stateless MCP tool controller looks in Quarkus using standard REST annotations: Java package com.example.mcp; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import io.smallrye.mutiny.Uni; @Path("/mcp/v1") public class StatelessMcpResource { @POST @Path("/tools") @Produces(MediaType.APPLICATION_JSON) public Uni<McpResponse> handleToolExecution( @HeaderParam("Mcp-Method") String method, @HeaderParam("Mcp-Name") String toolName, McpRequestPayload payload) { // The request is entirely self-contained; no session lookup required. if ("tools/call".equals(method) && "fetch_customer_data".equals(toolName)) { return executeCustomerLookup(payload.getArguments()); } return Uni.createFrom().item(McpResponse.error("Tool or method not found")); } private Uni<McpResponse> executeCustomerLookup(JsonElement arguments) { // Business logic interacting with reactive databases or internal services return Uni.createFrom().item(new McpResponse("Customer data retrieved successfully.")); } } Summary The combination of a stateless protocol and a cloud-native Java framework removes the operational friction in building enterprise AI features. By deploying stateless MCP servers on cloud native Java - Quarkus, you gain the type of predictable scaling, rapid response times, and bulletproof reliability that modern production environments demand. Check out more from my series here.
The year is 2026, and the way software is built has fundamentally shifted. We are no longer just writing code for other humans to read; we are building systems that AI coding agents, such as Cursor, GitHub Copilot Agent Mode, Claude Code, and autonomous CLI tools, will navigate, debug, and extend. As Java developers, we are blessed with robust tooling. If you are using Quarkus, you already possess a superpower: Supersonic Subatomic Java with an ultra-fast developer loop, continuous testing, and built-in Dev Services. However, AI agents frequently get tripped up by enterprise Java repositories. They overcomplicate simple architectures, write blocking code where reactive code belongs, or waste tokens trying to spin up manual Docker containers when Quarkus Dev Services could do it out of the box. The fix? AGENTS.md. Let’s explore how to use this emerging open standard to make your Quarkus applications instantly digestible for AI agents. What Is AGENTS.md? The AGENTS.md specification is a tool-agnostic open standard (pioneered by the Agentic AI Foundation) designed to sit at the root of a repository. Think of your standard README.md as human onboarding documentation: it contains high-level architecture narratives, badges, and project philosophy. AGENTS.md, on the other hand, is an executable runtime instruction layer for AI. It is concise, deterministic, imperative, and explicitly structured to prevent "context window bloat" while giving autonomous agents the exact boundaries and commands they need to succeed. The Anatomy of an Agent-Ready Quarkus Codebase When an AI agent initializes inside your workspace, it reads your project structure. Because Quarkus spans both imperative and reactive paradigms, an unguided AI agent will often hallucinate or mix patterns. An effective AGENTS.md for a Quarkus ecosystem must explicitly define three pillars: Operational commands: The exact Maven/Gradle sequences for running, testing, and live-reloading.Architectural boundaries: Strict rules regarding blocking vs. non-blocking code and data access patterns.Infrastructure management: Forcing the agent to utilize Quarkus Dev Services rather than provisioning external databases. Hands-On: The Ultimate Quarkus AGENTS.md Template Drop this exact AGENTS.md file into the root of your Quarkus repository to drastically improve the quality of AI-generated code and autonomous refactoring tasks. Markdown ## Tech Stack & Ecosystem Context - **Runtime**: Java 25, Quarkus 3.x (Supersonic Subatomic Java). - **Build Tool**: Maven (`mvnw` wrapper present). - **Extensions**: REST, Hibernate ORM with Panache, Quarkus Dev Services. - **Database**: PostgreSQL (Managed entirely via Dev Services). ## Critical Operational Commands - **Launch Development Mode**: `./mvnw quarkus:dev` - **Execute All Tests**: `./mvnw test` - **Continuous Testing**: Start `./mvnw quarkus:dev` and press `r` to toggle background testing. - **Production Package**: `./mvnw package` ## Architectural Boundaries & Coding Standards ### 1. Reactive vs. Blocking Rules - Default to **REST**. Endpoints returning `Uni<T>` or `Multi<T>` must NEVER invoke blocking operations. - If a method blocks, annotate it explicitly with `@Blocking`. ### 2. Data Access (Hibernate ORM with Panache) - Use the **Panache Active Record pattern** extending `PanacheEntity`. Do NOT write custom repositories or explicit DAO layers unless complex business logic demands it. - **Transaction Management**: Annotate mutate operations with `@Transactional`. Never manage transactions manually. ```java // Correct Agent Output Example: @Entity public class Developer extends PanacheEntity { public String name; public String specialty; public static Uni<Developer> findByName(String name) { return find("name", name).firstResult(); } } ``` ## Scaffolding Lifecycle for New Microservices When scaffolding a new microservice (e.g., "Scaffold a new microservice for user billing"), the agent follows this deterministic lifecycle: ### 1. Reads the Command Layer - **Bypass manual configuration**: Do NOT generate raw `pom.xml` text by hand, which frequently leads to version mismatches or missing dependency management blocks. - **Use Quarkus tooling**: Rely on the official Quarkus Maven plugin command structure. ### 2. Executes the Tooling - **Command**: Run the explicit `mvn io.quarkus.platform:quarkus-maven-plugin:create` command directly inside your terminal workspace. - **Example**: ```bash mvn io.quarkus.platform:quarkus-maven-plugin:3.x.x:create \ -DprojectGroupId=com.example \ -DprojectArtifactId=billing-service \ -DclassName="com.example.billing.BillingResource" \ -Dpath="/billing" ``` ### 3. Applies Core Extensions - **Guarantee essential extensions** are baked in from the first second: - `hibernate-orm-panache` for data access - `quarkus-rest` for REST endpoints - **Add extensions during creation**: ```bash mvn io.quarkus.platform:quarkus-maven-plugin:create \ ... \ -Dextensions="hibernate-orm-panache,quarkus-rest,jdbc-postgresql" ``` - This prevents the agent from creating legacy or blocking code templates down the line. ### 4. Validates Context - **Transition to Testing**: Once scaffolded, immediately verify that the out-of-the-box generated test suite runs cleanly. - **Validation command**: `./mvnw test` - **Expected outcome**: All generated tests pass without modification, confirming the scaffold is valid and ready for development. ### Post-Scaffold Checklist - [ ] Project structure follows standard Maven layout (`src/main/java`, `src/test/java`) - [ ] `application.properties` contains Dev Services configuration (auto-configured for PostgreSQL) - [ ] At least one REST endpoint exists with a corresponding test - [ ] `./mvnw test` passes cleanly - [ ] `./mvnw quarkus:dev` starts without errors Testing and Local Infrastructure Never manually configure Testcontainers or hardcode local JDBC connections inside application.properties for local development.Rely 100% on Quarkus Dev Services. The PostgreSQL container is automatically spun up during ./mvnw quarkus:dev or @QuarkusTest. Verification Protocol Before declaring a task complete, you MUST: Run ./mvnw compile to ensure zero compilation or annotation processor failures.Run ./mvnw test and confirm all integration tests pass cleanly. Note: Find the solution repository: https://github.com/danieloh30/agents-md-for-java-quarkus.git Shell ### Sample Demo Walkthrough: Put it to the Test To see the power of this setup, let’s imagine a standard demo repository structured as follows: agents-md-for-java-quarkus/src/main/java/com/example/billing/ |____com | |____example | | |____billing | | | |____Invoice.java | | | |____BillingResource.java | | | |____InvoiceItem.java |____pom.xml |____README.md <-- For humans |____AGENTS.md <-- For the AI Agents The Experiment You open this repository inside an AI-native workspace and issue a vague, autonomous prompt: "Add a new REST endpoint to fetch a developer by their specialty, write a test for it, and verify that the app works." Without AGENTS.md The agent might look at pom.xml, realize it's a Java app, and write a legacy, blocking JAX-RS endpoint. It might attempt to spin up a Docker container inside the test via a manual DockerClient or throw an error because it doesn't know how to supply a PostgreSQL URL. With AGENTS.md Reads context: The agent parses AGENTS.md instantly. It recognizes that it must write a reactive Uni<Developer> endpoint using Panache’s Active Record pattern.Generates code: It appends a clean, reactive finder method directly onto the Developer entity.Executes environment: Instead of guessing how to launch your app, it executes ./mvnw quarkus:dev.Leverages dev services: It sees that Quarkus handles the database automatically. It writes a clean @QuarkusTest integration test, triggers the validation, checks the terminal logs, and corrects its own syntax if a compilation check fails. By defining the boundaries upfront, you prevent the agent from writing code that compiles but violates your team's architectural standards. Conclusion: Treat Context as Code Providing an AI agent with free rein over an enterprise Java codebase without boundaries is like letting a junior developer deploy to production on day one without code reviews. By adopting AGENTS.md alongside the rapid developer feedback loops built natively into Quarkus, you bridge the gap between human intent and machine execution. Spend 10 minutes writing an AGENTS.md file today, and unlock massive productivity gains for the agentic future of software development. Check out more from my series here.
Compliance-reporting teams keep spreadsheets in the loop for a practical reason: a workbook lets domain experts inspect assumptions, formulas, source rows, and intermediate values without reading a line of application code. That transparency is genuinely useful, and it's a big part of why replacing Excel outright so often fails to stick. The trouble starts once that workbook becomes part of a repeatable, audited reporting process — a regulatory filing, an IFRS report, a periodic compliance submission. At that point, a shared Excel file isn't enough on its own. What's actually needed is version control, validation, an audit trail, a review step, and a reliable way to connect the spreadsheet's logic to the systems downstream. The spreadsheet itself isn't the problem. It's a review surface domain experts genuinely need. The problem is treating it as a loose file sitting outside the application. The goal isn't to eliminate spreadsheets, but to preserve the spreadsheet experience while letting the application govern how it's used. This article walks through an architecture that keeps the workbook where domain experts can see it, but moves execution — validation, calculation, output generation, logging — into a Java application. The scenario is inspired by a real-world IFRS reporting project, and the same architecture applies to regulatory reporting, statutory filings, actuarial review, and other spreadsheet-driven compliance workflows. The pattern itself doesn't require a specific product: it works with any spreadsheet engine that can load a workbook and expose read/write access to Java, and parts of it apply even if you only use a file library like Apache POI at the edges. Three Ways Teams Usually Respond Rewrite everything in Java. Engineering gets control, tests, and CI. But the calculation logic moves away from the people who understand it. Every threshold change, every new currency, every adjusted formula now goes through a sprint. Sometimes that's correct — if the rules are stable and nobody inspects formulas, do this. For living, business-owned logic, it breeds shadow spreadsheets. Leave the desktop spreadsheet alone. Finance keeps full flexibility. The organization keeps none of the guarantees: no version control, no audit trail, no way to prove which file produced the submitted numbers. Use a file library only at the edges. Java imports the workbook, exports the results. Better — but the correction loop still happens in desktop Excel: download, fix locally, re-upload, re-validate, repeat. Every round trip is an audit gap. Now there is a fourth option: embed the workbook directly into the web application. Domain experts continue working in a familiar spreadsheet interface, while the application governs when users can edit data, when validation runs, which outputs become visible, and how every operation is logged. The rest of this article is about what that looks like in practice. The Big Idea: One Workbook, Two Roles In this pattern, the workbook plays two roles at the same time: For users, it is the interface. They inspect rows, correct values, maintain rule tables, and review generated outputs in a familiar grid.For the application, it is a runtime artifact. Java loads a known template, reads specific sheets and regions, runs validation, writes outputs, and records every run. The design decision that makes this work: Java never wanders through the workbook looking for data. It reads and writes only through agreed sheets and regions — a contract. Finance owns what's inside the regions: values, formulas, rules. Engineering owns the boundary and everything behind it: execution, permissions, persistence, export. Let's see the two stages of a typical reporting workflow through this lens. Stage 1: Let Users Fix Data Issues Without Leaving the App Reporting source data almost never arrives clean. A currency code says US instead of USD. An FX rate is missing. A service fee breaks a policy limit. The template for this stage has two sheets. Input CSV holds the source rows users can inspect and correct. ETL Rule holds the validation rules — as an ordinary spreadsheet table with columns like Field, Check, and Allowed Values. A rule row might say: currency must be one of USD, EUR. Finance can read and change these rules without asking anyone. When the user clicks Run Validation, the application takes over. To make this concrete: the examples in this article use Keikai Spreadsheet, a Java-based spreadsheet UI component, to embed the workbook in the browser and read and write it from Java — though the same three-step logic applies with any comparable engine. Conceptually, the Java service reads the data rows, reads the rule rows, and checks every row against every rule: Java List<SourceRow> rows = sheetReader.readTable(workbook, "Input CSV"); List<Rule> rules = ruleParser.parse(sheetReader.readTable(workbook, "ETL Rule")); for (SourceRow row : rows) for (Rule rule : rules) rule.check(row).ifPresent(report::add); Notice what this is not: the rules are not hard-coded in Java. Java only knows how to read the rule table and apply generic checks. The actual business knowledge — which currencies are allowed, what a valid fee looks like — stays in the workbook where its owners can see it. One detail carries most of the user experience: every validation error records which cell failed — sheet, row, and column. That lets the UI show a panel saying “policy P-1024, field currency, value US, expected USD or EUR” with a link that jumps the user straight to the offending cell. They fix it in the grid, click run again, and validation passes. Compare that to the traditional loop — download, fix in Excel, upload, pray. Here, nothing leaves the system, and every edit can be logged with user, timestamp, old value, and new value. Stage 2: Generate Outputs Under Application Control Once the data is clean, the second stage produces the actual reporting outputs: journal entries, impact tables, export-ready CSV sheets. The input is a policy sheet with assumptions (premium totals, fees, FX rates) plus a rule table that maps accounting events to journal lines. Before the run, the application shows only the input sheet — output sheets stay hidden, because they don't exist meaningfully yet. When the user triggers generation, the same Keikai-backed workbook is read and written from Java: it reads the inputs, computes the metrics, builds the journal rows, and writes them back into the workbook: Java PolicyInput policy = policyReader.read(workbook, "Policy Input"); Metrics metrics = deriveMetrics(policy); // plain Java arithmetic List<JournalRow> rows = journalBuilder.build(readJournalRules(workbook), metrics); sheetWriter.replaceTable(workbook, "Journal Entries", rows); revealSheets(workbook, "Journal Entries", "Report Impact", "Journal CSV"); The interesting part is the last line. Sheet visibility is an application decision: outputs appear only after a successful run, so a reviewer can never mistake stale output for fresh output. The reviewer then sees everything in one place — assumptions, rules, generated journals, report impact — in the same grid, and the export button produces a file the application has logged and versioned. deriveMetrics itself is deliberately simple — a handful of multiplications and subtractions. In a real system it may be far more complex, or it may even delegate back to formulas in the workbook. The architecture doesn't change: inputs go into agreed regions, outputs come from agreed regions, and Java owns the trigger. The Part Everyone Skips: The Workbook Is Now an API The moment Java code depends on a sheet named ETL Rule with a header called Allowed Values, the workbook has stopped being a document. It has become an interface — and interfaces break when they're changed casually, without review. The fix is to make the contract explicit and test it. Distinguish two kinds of change: Value changes – a new allowed currency, an adjusted threshold, a reviewed formula edit. These live inside the contract. Finance can make them without touching Java.Structural changes – renaming a sheet, deleting a header, moving an output table three columns right. These are API changes and should be reviewed like one. Then write this test: Java @Test void templateSatisfiesReportingContract() { Workbook wb = engine.load("reporting-template.xlsx"); assertSheetExists(wb, "Input CSV", "ETL Rule", "Journal Entries"); assertHeaders(wb, "ETL Rule", "Field", "Check", "Allowed Values"); } It looks almost too simple to matter, but most real-world workbook integration failures are exactly this mundane — a renamed sheet or a deleted header, discovered the night before a regulatory filing is due. Catching it in CI, before any template goes live, is what makes the difference. Finally, log runs, not just files: template version, who ran it, validation status, output row counts, a hash of the inputs. When someone asks “why does this quarter's filing look wrong?”, you answer from the run log instead of from archaeology on a shared drive. For compliance teams, these controls turn the workbook from an informal file into evidence the organization can explain. A reviewer can trace which template version produced a number, which source data was used, who ran the process, whether validation passed, and which output was exported. If a template structure changes, the contract test shows whether the workbook still satisfies the application’s required sheets and headers before it reaches production. In other words, the system does not just calculate results; it records the evidence needed to defend how those results were produced. When to Consider a Simpler Approach This approach pays off when the workbook is a genuine shared language between domain experts and developers — something both sides actually read, edit, and rely on. If the rules rarely or never need to change, and nobody inspects formulas, plain Java is simpler to test and operate. And if the workbook is really just a transfer format between systems, a straightforward import/export covers it. Takeaways The compliance-reporting spreadsheet doesn't have to be rewritten or worked around. Put it inside the application and split ownership along a clear line: The workbook owns what users must see and maintain: source rows, rule tables, assumptions, reviewable outputs.The application owns execution: validation, generation, sheet visibility, permissions, logging, export.The contract between them — named sheets, headers, regions — is documented, tested in CI, and changed only with review. Do that, and the workbook stops being an unversioned file nobody can fully account for. It becomes a governed part of the application — the place where domain experts and the system finally agree on the numbers.
In the first article, we got started with Jeffrey Microscope and learned to read a single flamegraph — the timeseries, search, tooltips, and the allocation and wall-clock variants. This time we build directly on that foundation and tackle one of Jeffrey's most powerful features for real-world performance work: the differential flamegraph, which compares two recordings and shows you precisely what changed between them. A single flamegraph tells you where your application spends its time. But the questions that matter most in practice are comparative: Did my optimization actually help?What did this refactor make slower?Where did the extra allocations come from? Staring at two flamegraphs side by side and trying to spot the difference by eye is slow and error-prone — the graphs are large, and the interesting change is often a few frames buried deep in the stack. Jeffrey Microscope's differential flamegraph solves this by overlaying two recordings into a single graph and coloring every frame by how it changed: Red – where the primary profile spends more than the baseline (a regression).Green – where it spends less (an improvement).Deeper shades – brand-new and fully-removed frames, called out distinctly. In this article, we'll take the two recordings from the previous post — the optimized direct serialization path and the garbage-heavy DOM path — set one as a secondary profile, and let the differential view pinpoint exactly which methods account for the difference. We start exactly where the first article left off. Open the optimized recording, jeffrey-persons-direct-serde-cpu.jfr.lz4, and head to the Visualization tab — this is our primary profile, the same CPU flamegraph we explored last time. On its own, it shows where the direct serialization path spends its time, but to turn it into a comparison we need a second recording to diff it against. That's what the Secondary Profile slot in the top bar is for — currently marked NOT SET. In the next step we'll point it at the DOM-based recording and unlock the Differential view in the sidebar. Supported Events Types With the secondary set, the Differential page mirrors the Primary one — a card per event type — but each now shows both sides at once. The value on the left is the baseline (the secondary profile), the value on the right is the primary, and the badge is the relative change from one to the other: a red +N% means the primary has more of that event than the baseline (grew), a green −N% means it has less (shrank). This lets you gauge the overall shift before opening a single graph — whether the change is a rounding-error wobble or a real regression worth investigating. Jeffrey supports differential flamegraphs for every sample-based event it can render normally: Execution Samples – total CPU work. More samples means more time spent on-CPU (37.3K → 39.7K, +6.4% here).Wall-Clock Samples – elapsed time including waiting and blocking, which can move independently of CPU (5.0M → 4.4M, −12.4%).Allocation Samples – memory pressure; switch Use Total Allocation to compare bytes rather than sample count and see the true allocation cost (27.47 GiB → 30.45 GiB, +10.9%).CPU-Time Samples and Method Traces – empty here, but diff identically when the recordings contain them. Each of these numbers is just the headline; the flamegraph below breaks the same delta down frame by frame, so you can see which methods drove it. Click View Flamegraph on the Execution Samples card to open the differential CPU view. Reading the Differential Flamegraph Opening the differential view feels familiar — same timeseries, search, and tooltip as a normal flamegraph — but everything now encodes two profiles at once: The summary bar at the top reports the totals side by side: baseline 35,472 vs primary 39,668, a net +4,196 (+11.83%) flagged as REGRESSED. That's the headline — the primary run did more on-CPU work overall.The timeseries overlays both recordings as two lines — Primary in blue, Secondary (baseline) in red — so you can see where in time the profiles diverge, not just that they differ.The flamegraph colors encode the per-frame change: pale pink/green for frames that shifted a little, and saturated deep red/deep green for frames that exist in only one profile — brand-new work versus work that disappeared entirely. The payoff is in the last two screenshots. Because the optimized and unoptimized paths run through differently-named classes, the diff renders them as a matched pair: the deep-red EfficientPersonService.getNPersons subtree (new in the primary) sitting right next to the deep-green InefficientPersonService subtree (gone from the primary). You're literally seeing the code swap, top to bottom. And hovering a shared frame quantifies it precisely — the tooltip on PersonController.getNPersons shows baseline 854 → primary 525, an IMPROVED −329 (−38.52%) for that endpoint's own path. The differential CPU flamegraph overlays both recordings: the timeseries plots the primary (blue) against the secondary baseline (red), and the summary bar reports baseline 35,472 → primary 39,668, a net +4,196 (+11.83%) marked REGRESSED. The merged flamegraph colors every frame by its change. The shared Tomcat, Coyote, and Spring layers stay mostly pale pink — small shifts — while the summary bar keeps the overall +11.83% delta in view. The flamegraph also captures the JVM's own threads, not just your request path — the CompileBroker / C2Compiler stacks on the left are JIT compilation, and garbage-collection activity shows up the same way. Comparing them across the two recordings tells you whether either run triggered extra spikes in JIT or GC work, a common hidden cost when one version allocates more or churns more code. Deeper into the stack, the two implementations separate out: saturated red columns mark work that is new in the primary profile, while the deep-green columns are paths that existed only in the baseline and disappear in the primary. The optimized EfficientPersonService path (red, added) sits beside the removed InefficientPersonService path (green). Hovering the shared PersonController.getNPersons frame quantifies the change exactly: baseline 854 → primary 525, an IMPROVED −329 (−38.52%). Summary From here, try the same workflow on the Wall-Clock and Allocation differential flamegraphs — the steps are identical, and each reveals a different dimension of the change: time spent waiting, and bytes allocated. Thank you for reading! To go deeper, visit the Jeffrey pages, or reach out to me directly on LinkedIn — I'd love to hear your feedback. And stay tuned: in the next article, we'll step away from flamegraphs and explore one of Jeffrey's JVM Internals views to dig into what the runtime does under the hood.
Java Flight Recorder (JFR) captures an enormous amount of detail about what your application is doing — but raw JFR files are only as useful as the tools you have to explore them. Jeffrey is an open-source JFR analyzer that specializes in turning JFR events into interactive visualizations, and Jeffrey Microscope is its standalone, single-user deployment: a self-contained application that lets you import recordings and dig into flamegraphs, timeseries, and other views right in your browser. Getting started takes a minute: Standalone JAR – download the latest microscope.jar from the GitHub releases page and start it with java -jar microscope.jar (Java 25 or newer).Docker – skip the setup entirely with docker run -it --network host petrbouda/microscope.Sample recordings – if you want to explore the tool before profiling your own application, the petrbouda/microscope-examples image ships with sample recordings preloaded (docker run -it --network host petrbouda/microscope-examples). In this article, we'll use Jeffrey Microscope to analyze JFR flamegraphs and walk through how they help you find where your application actually spends its time. Let's set up a hands-on environment. Download the latest microscope.jar from the GitHub releases page and launch it (Java 25 or newer): Shell java -jar microscope.jar Open it in your browser, then grab some recordings to analyze — Jeffrey maintains a companion repository of real JFR recordings captured from various serialization and profiling scenarios: Shell git clone https://github.com/petrbouda/jeffrey-recordings The files ship as compressed .jfr.lz4, which Jeffrey Microscope reads natively. Drag one onto the Drop Recordings zone on the dashboard — the upload starts automatically, and within a few seconds you have a profile ready to explore. For this walkthrough, we'll focus on two recordings that profile the same piece of code — an HTTP endpoint that serializes and deserializes JSON — with one deliberate difference between them: jeffrey-persons-direct-serde-cpu.jfr.lz4 – the optimized path. JSON is serialized directly to and from Java objects, with additional caching in place.jeffrey-persons-dom-serde-cpu.jfr.lz4 – the unoptimized path. JSON is routed through a DOM representation (JsonNode) before being converted to Java objects, intentionally creating extra garbage along the way. Because both recordings exercise the same endpoint under the same workload, they make an ideal before-and-after pair for generating flamegraphs and differential graphs, as we show later. Exploring the Primary Flamegraphs Let's start with the optimized recording. Click jeffrey-persons-direct-serde-cpu.jfr.lz4 to open its profile, then head to the Visualization tab and select Primary under Flamegraphs in the sidebar. Jeffrey inspects the recording and presents a card for every flamegraphable event type it found — each ready to render on its own: Execution Samples (jdk.ExecutionSample) – CPU profiling via perf_events, the most relevant card for a CPU profile like this one.Wall-Clock Samples (profiler.WallClockSample) – wall-clock time, including waiting.Allocation Samples (jdk.ObjectAllocationInNewTLAB) – memory allocation, weighted by object count or total bytes.Java Monitor Blocked, Java Thread Park, Java Monitor Wait – lock-contention and thread-parking events. Each card shows the event type, its source (Async-Profiler or the JDK), the sample count, and a few rendering options — for example, Use Thread-mode to split the graph by thread, or Use Total Allocation to weight the allocation flamegraph by bytes rather than sample count. Click View Flamegraph on the Execution Samples card to see where the CPU time goes. Timeseries Above the flamegraph, Jeffrey plots the selected event across the recording's timeline, so you can see how activity changes over the run — warm-up, steady state, and spikes all stand out. Drag the handles on the range selector below to narrow the window, and the flamegraph rebuilds from only the samples in that interval. Flamegraph Each box is a stack frame, its width proportional to the samples that captured it, stacking upward toward the methods running on-CPU. Wide boxes are where time goes. Read top to bottom to follow the full call path from entry point down into your own code. Click any frame to zoom into that subtree. Search The search box highlights every frame matching your query and reports what share of the profile those matches account for — a fast way to answer "how much time is really in my code?" and to locate a method however deep it sits. The Frame Tooltip Hovering a frame shows far more than a sample count: total vs self samples (time through the frame vs directly in it), its bytecode index and source line, and a compilation breakdown — JIT-compiled, C1-compiled, or inlined — revealing how the method was actually executed. Open in IDE, and View Source jump straight to the code, once Microscope is paired with the Jeffrey IntelliJ plugin. Copy for AI The Copy for AI button exports the current view — stacks, weights, and hot paths — as a compact Markdown summary, copied to your clipboard or downloaded as .md. Paste it into e.g. Claude Code and let the AI optimize your code based on runtime profiles from flamegraphs. Other Flamegraphs Everything above applies to more than just CPU. Back on the Primary page, you can open the Allocation and Wall-Clock flamegraphs the same way — same navigation, search, tooltip, and range selector — but each answers a different question: Wall-Clock – where wall-clock time is spent, including waiting, rather than just on-CPU work.Allocation – where memory is allocated. Two rendering options are worth trying: Use Thread-mode – splits the graph by thread, showing per-thread call trees instead of one merged view. Handy when a single thread dominates or misbehaves. Use Total Allocation – switches the allocation graph from sample count to weight: each frame is sized by the number of bytes allocated rather than how many samples hit it, so a rarely-sampled path that allocates large objects shows up at its true cost. Weighting by the event's own measure instead of sample count often paints a very different — and more actionable — picture. Summary In this article, we set up Jeffrey Microscope and walked through reading a flamegraph — the timeseries and range selector, search, the frame tooltip, the Copy for AI export, and the allocation and wall-clock variants. That's already enough to find where an application spends its time and to start optimizing with confidence. Thank you for reading! To go deeper, visit the Jeffrey pages, or reach out to me directly on LinkedIn — I'd love to hear your feedback. And stay tuned: in the next article, we'll put these two recordings side by side and show how Jeffrey's Differential flamegraph pinpoints exactly what changed between the optimized and unoptimized code.
Muhammed Harris Kodavath
Senior Technical Manager,
Baptist Health South Florida
Rahul Tewari
Software Engineer Expert,
UPMC
Otavio Santana
Award-winning Software Engineer and Architect,
OS Expert
Daniel Oh
Senior Principal Developer Advocate,
IBM