DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Java

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.

icon
Latest Premium Content
Trend Report
Low-Code Development
Low-Code Development
Refcard #216
Java Caching Essentials
Java Caching Essentials
Refcard #400
Java Application Containerization and Deployment
Java Application Containerization and Deployment

DZone's Featured Java Resources

Java Enterprise Is Already Ready for the AI Era

Java Enterprise Is Already Ready for the AI Era

By Otavio Santana DZone Core CORE
Artificial intelligence is changing software engineering, impacting automation, user interaction, data analysis, and application development. Developers are evaluating how their technology stacks fit with these changes. For Java developers in enterprise settings, a main question is whether the Java enterprise ecosystem is prepared for AI. The short answer is yes. You do not need to abandon Java or wait for a new platform to build AI-enabled applications. Java already provides a mature ecosystem of AI libraries, model providers, APIs, and integration patterns. Jakarta EE offers the capabilities required to deploy these technologies in production-grade enterprise systems today. The ecosystem is evolving, with new initiatives exploring perfect integration of AI concepts within Jakarta EE APIs and programming models. This article reviews existing capabilities, Jakarta EE’s role within modern AI architectures, and potential future developments. AI and Software Engineering When applying artificial intelligence in software engineering, it is important to distinguish the different ways AI can be used throughout the development lifecycle. AI can assist with documentation, testing, code reviews, architecture exploration, and code generation. Architecturally, these uses fall into two categories: using AI to develop software and integrating AI within the software itself. The first category, AI-assisted software development, is currently the most common. Developers use AI tools to generate, explain, refactor, or test code. While these tools can boost productivity, they also introduce risks if not used with proper engineering discipline. Insufficient context, unreviewed code, or tools lacking architectural constraints can cause defects, security issues, complexity, or inconsistent design. AI does not replace the engineering team; it remains their responsibility to use it effectively. New methodologies are emerging to structure this interaction. Approaches like vibe coding focus on rapid development through conversational AI, while Spec-Driven Development offers explicit requirements, constraints, and context before code generation. Agent-based workflows increasingly use repositories with instructions, specifications, and Markdown files to give coding agents the required context. These approaches do not require abandoning Java; Java projects can already employ these techniques. The second category entails integrating AI within the application itself, making AI part of the application's runtime behavior rather than just assisting developers. Applications may use a large language model (LLM) to classify information, generate content, extract structured data, retrieve knowledge, execute tools, or make decisions within business workflows. This combination delivers a fundamental architectural change. Traditional enterprise applications are predominantly deterministic: developers define process flow using methods, conditions, rules, workflows, and state changes. With the same inputs and state, the execution path is predictable. In contrast, AI-enabled applications can present a dynamic execution model, where some behavior is determined at runtime via the LLM. However, not every AI-enabled application should surrender control to the model. In practice, AI architectures exist on a spectrum of autonomy. At one end, the model functions within a tightly controlled deterministic workflow. As autonomy increases, the model can select tools, plan steps, evaluate results, and coordinate more complex actions. This evolution is reflected in the Core Autonomy Patterns, which start with deterministic directed acyclic graph (DAG) workflows and progress toward more autonomous approaches such as retrieval-augmented generation (RAG), reflection, planning, ReAct, multi-agent systems, and Model Context Protocol (MCP) integrations. As flexibility increases, so does the architectural responsibility for observability, security, testing, governance, failure handling, and control. Recognizing this distinction is essential when evaluating Jakarta EE’s readiness for AI. The first category already integrates naturally with Java development tools. The second stresses the importance of the enterprise platform: AI applications still require dependency injection, configuration, REST APIs, persistence, messaging, transactions, security, observability, asynchronous execution, and integration with external systems. These are the capabilities Jakarta EE was designed to provide. Jakarta EE and AI Now Java and Jakarta EE are ready for the AI era. Integrating AI does not require leaving the enterprise Java ecosystem or waiting for new specifications. Jakarta EE applications can already use large language models (LLMs), embed AI in business workflows, and employ these capabilities within the wider enterprise platform. This is evident inside real-world applications. For example, Skillwell Simulate, a Jakarta EE-based platform, integrates with AWS services and uses Amazon Bedrock for AI features. This shows that Jakarta EE applications can adopt modern AI services while retaining the benefits of established enterprise architecture. At the lowest abstraction level, applications can integrate directly with AI providers such as OpenAI, Anthropic, Google, and Amazon Bedrock using their APIs or Java SDKs. This approach delivers full access to provider-specific features but increases coupling. Each provider uses different API models, configurations, formats, authentication, and features. Supporting multiple providers can add boilerplate and increase complexity. Enterprise developers are familiar with this challenge. Different vendors and technologies offer different capabilities, so abstractions provide a unified programming model. AI integration is now adopting a similar approach. OmniHai is a lightweight Java AI library for Jakarta EE and MicroProfile applications. Instead of requiring each vendor's SDK, OmniHai provides a consistent AIService abstraction and communicates directly with provider REST APIs. It currently supports OpenAI, Anthropic, Google AI, xAI, Mistral, Meta AI, Azure OpenAI, OpenRouter, Hugging Face, Ollama, and custom providers. With CDI, an AI provider can be injected directly into a Jakarta EE component: Java @Inject @AI(provider = AIProvider.ANTHROPIC,apiKey = "your-anthropic-api-key") private AIService claude; The application interacts with AIService instead of provider-specific APIs. This enables chat interactions to use a consistent programming model across providers: Java String response = claude.chat( "Explain microservices", ChatOptions.newBuilder() .systemPrompt("You are a helpful software architect.") .temperature(0.5) .maxTokens(500) .build() ); OmniHai also supports asynchronous and streaming operations through the same abstraction. Conceptually, this approach is similar to abstractions like EntityManager in Jakarta Persistence: the application uses a common API while implementation details remain hidden. Although not a perfect comparison, it illustrates OmniHai’s role in managing multiple AI providers. LangChain4j CDI offers a higher-level programming model. Instead of working directly with an AIService object, developers define an AI service as a Java interface. LangChain4j CDI detects interfaces annotated with @RegisterAIService and supplies their implementations as CDI beans. For example: Java @RegisterAIService public interface AssistantService { @SystemMessage("You are a helpful assistant.") String chat(String userMessage); } Developers do not write implementation classes. The infrastructure generates the implementation and connects the interface to the configured language model. The resulting service can be injected as any other CDI bean: Java @Path("/assistant") public class AssistantResource { @Inject AssistantService assistant; @GET @Path("/chat") public String chat(@QueryParam("message") String message) { return assistant.chat(message); } } This programming model will be familiar to Jakarta EE developers. It is similar to the repository abstraction in Jakarta Data, where developers define the contract through an interface and the infrastructure supplies the implementation. Although the technologies address different needs, this model reduces the amount of infrastructure code developers must write. LangChain4j goes beyond basic model invocation. It offers unified APIs for over 20 LLM providers and includes abstractions for tools, Retrieval-Augmented Generation (RAG), chat memory, structured outputs, agents, embedding stores, and other AI features. Supported integrations include Amazon Bedrock, Anthropic, Azure OpenAI, Google AI Gemini, OpenAI, Mistral, OCI Generative AI, among others. These options represent different levels of abstraction: OmniHai serves as a lightweight template-style abstraction, allowing the application to invoke operations through a common AIService. LangChain4j CDI advances this by supplying a declarative interface-based model, where developers describe the AI service and the infrastructure provides its implementation. Both approaches ensure the application stays a Jakarta EE application. Once an AI capability is available as a CDI bean, it integrates perfectly with the platform. REST endpoints can expose it, Jakarta Persistence or Jakarta NoSQL can supply data, Jakarta Security can protect its operations, Jakarta Messaging can trigger asynchronous workflows, and other Jakarta EE APIs continue their roles. The question is no longer whether Jakarta EE can integrate with AI; it already does. The key architectural decision is now the required level of abstraction: direct provider integration for maximum control, a lightweight common API like OmniHai, or a richer AI programming model such as LangChain4j CDI. Jakarta EE and Future Jakarta EE already supports AI integration, and the platform continues to evolve. Jakarta EE 12 focuses on improving the data layer, with updates to Jakarta Data, Jakarta Persistence, Jakarta NoSQL, and the new Jakarta Query specification. These improvements are especially important for AI applications that rely on enterprise data, persistence, retrieval, and contextual content. The primary AI-focused initiative is Jakarta Agentic AI, which has released its first milestone. Its purpose is not to replace LangChain4j or provider SDKs, but to offer a standard programming model for building AI agents with Jakarta EE. The specification defines a small set of concepts to structure agent workflows based on annotations, thus making the developer's life way easier: APIPurpose @Agent Declares an agent class @Trigger Defines the workflow entry point @Decision Determines whether and how the workflow proceeds @Action Defines a step in the workflow @Outcome Marks the end of the workflow @HandleException Handles exceptions inside the workflow @WorkflowScoped Provides one CDI context per workflow execution LargeLanguageModel Injectable facade for interacting with an LLM Result Represents the result of a decision This example presents a simplified fraud-detection agent and illustrates how Jakarta Agentic AI integrates with the Jakarta EE programming model. The agent uses the LargeLanguageModel facade for AI interaction and leverages Jakarta Persistence and Jakarta NoSQL to access enterprise data. As a result, AI capabilities are incorporated as part of the application, not as a separate programming environment. Java @Agent public class FraudDetectionAgent { @Inject LargeLanguageModel model; @Inject EntityManager entityManager; @Inject Template template; @Trigger private void handleTransaction( @Valid BankTransaction transaction) { } @Decision private Result checkFraud(BankTransaction transaction) { CustomerHistory history = template .find(CustomerHistory.class, transaction.customerId()) .orElse(null); String output = model.query( """ Analyze this transaction for potential fraud using the transaction and customer history. """, transaction, history); return new Result(isFraud(output), null); } @Action private void handleFraud( Fraud fraud, BankTransaction transaction) { if (fraud.isSerious()) { alertBankSecurity(fraud); } } @Outcome private void markTransaction( BankTransaction transaction) { BankTransaction managed = entityManager.merge(transaction); managed.markAsSuspect(); } } Conclusion Enterprise Java is prepared for AI today, with Jakarta EE already supporting this integration. Developers can add AI using provider SDKs, OmniHai, or LangChain4j CDI, while continuing to leverage Jakarta EE features for persistence, security, messaging, transactions, REST APIs, and enterprise data. AI enhances the existing platform as an integrated capability, rather than requiring replacement. The ecosystem continues to advance. Jakarta EE 12 enhances the data foundation, and Jakarta Agentic AI is introducing a structured programming model for building agents that integrate seamlessly with the platform. Jakarta EE is ready for AI now, and its capabilities will keep improving as the platform evolves. More
Orchestration Meets MCP: Building Governed Agentic Workflows With Quarkus Flow and AGENTS.md

Orchestration Meets MCP: Building Governed Agentic Workflows With Quarkus Flow and AGENTS.md

By Daniel Oh DZone Core CORE
Building autonomous AI agents with large language models (LLMs) is easy when writing single-turn demo scripts. However, moving multi-agent loops into production introduces serious architectural challenges. Agents hallucinate, loop infinitely without reaching convergence, require human approval for high-risk operations, and need standard tool-calling integrations alongside clear operational governance. Historically, Java developers faced a tough choice: either rely on heavyweight, external workflow clusters (like Temporal or Camunda) that add operational overhead, or hand-craft fragile while loops and custom state machines inside their services. Quarkus Flow bridges this gap. Built on the Cloud Native Computing Foundation (CNCF) Serverless Workflow specification, Quarkus Flow brings light-footprint, specification-compliant workflow orchestration directly into your Quarkus application. When combined with LangChain4j, Model Context Protocol (MCP) tool connections, and AGENTS.md context governance, Java developers can construct deterministic, observable, and resilient agentic AI workflows using idiomatic CDI and a fluent Java DSL. The Modern Agentic Stack: Quarkus Flow, MCP, and AGENTS.md To run production AI agents, you need three distinct layers: orchestration, standardized tool connectivity, and behavioral governance. Orchestration (Quarkus Flow): Manages state transitions, retries, conditional loops, max-iteration caps, and Human-in-the-Loop (HITL) gates inside the JVM.Tool standardization (MCP): Connects agents to enterprise data, databases, and APIs using the Model Context Protocol (MCP) without writing custom API adapters for every LLM host.Behavioral governance (AGENTS.md): A project-level markdown specification that defines system boundaries, agent roles, required output formats, and safety rules that agents read at runtime. Markdown ┌────────────────────────────────────────────────────────────────────────┐ │ `AGENTS.md` Governance │ │ (Runtime System Prompts, Rules & Security Boundaries) │ └───────────────────────────────────┬────────────────────────────────────┘ │ Loaded via GovernanceLoader ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ ArticlePublisherWorkflow (Quarkus Flow) │ │ │ │ 1. generateDraft ──> 2. evaluateDraft ──> 3. reviewCheck │ │ (Writer) (Critic) │ │ │ ▲ │ [approved || >=3] │ │ │ ├───> 5. publishArticle │ │ │ 4. reviseDraft <────────────┤ │ │ └─────────────────┘ [needs revision] │ │ └──────────────────────────┬─────────────────────────────────────────────┘ │ │ Tool Invocation via McpToolProvider ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ Stateless MCP Servers │ │ (External Data, Database Tools, & APIs) │ └────────────────────────────────────────────────────────────────────────┘ Defining Governance With AGENTS.md Instead of hardcoding prompt strings deep inside Java classes, place an AGENTS.md file in your src/main/resources. This allows developers and prompt engineers to adjust system instructions and security boundaries without re-compiling the application. Here is the src/main/resources/AGENTS.md file based on the reference repository: GitHub Flavored Markdown # Content Reviewer Agent Governance & Rules ## Writer Agent Rules - You are an expert Java and Quarkus developer. - Draft concise, technically accurate blog posts based on requested topics. - Query available MCP tools when database context or tool parameters are required. ## Critic Agent Rules - You are a strict editor reviewing for clarity, security, and technical accuracy. - Return ONLY a valid JSON object matching this schema: {"approved": boolean, "feedback": "string"} ## Security Boundaries - Do not output shell commands or execute arbitrary code. - Always enforce character limits and avoid hallucinated imports. Practical Example: Multi-Agent Workflow With MCP and AGENTS.md Let's build a production-grade Content Publisher Agent Workflow matching the exact structure from quarkus-flow-mcp-agents. The workflow reads system instructions from AGENTS.md, uses a Writer Agent that fetches real data via an MCP Server, submits the draft to a Critic Agent, and loops until approved or max iterations are reached. Note: You can find the complete reference implementation repository at https://github.com/danieloh30/quarkus-flow-mcp-agents.git. 1. pom.xml Dependencies XML ... <properties> <compiler-plugin.version>3.15.0</compiler-plugin.version> <maven.compiler.release>25</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id> <quarkus.platform.version>3.38.0</quarkus.platform.version> <skipITs>true</skipITs> <surefire-plugin.version>3.5.6</surefire-plugin.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>${quarkus.platform.group-id}</groupId> <artifactId>${quarkus.platform.artifact-id}</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>${quarkus.platform.group-id}</groupId> <artifactId>quarkus-langchain4j-bom</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>${quarkus.platform.group-id}</groupId> <artifactId>quarkus-flow-bom</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> ... <dependency> <groupId>io.quarkiverse.langchain4j</groupId> <artifactId>quarkus-langchain4j-openai</artifactId> </dependency> <dependency> <groupId>io.quarkiverse.langchain4j</groupId> <artifactId>quarkus-langchain4j-mcp</artifactId> </dependency> <dependency> <groupId>io.quarkiverse.flow</groupId> <artifactId>quarkus-flow-langchain4j</artifactId> </dependency> ... </dependencies> ... 2. Application Configuration: src/main/resources/application.properties Properties files # Enable OpenAI quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY} quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini quarkus.langchain4j.openai.log-requests=true quarkus.langchain4j.openai.log-responses=true 3. Orchestrating the Write-Review Loop With @LoopAgent ArticlePublisher is the orchestrator that wires the multi-agent loop together using Quarkus Flow's declarative API. Here's what each annotation does: @LoopAgent – runs WriterAgent then CriticAgent repeatedly (up to 3 iterations). At build time, Quarkus Flow compiles this into a CNCF Serverless Workflow definition — no separate workflow engine at runtime.@ExitCondition – a static method (isApproved) that checks if the critic's review starts with "APPROVED". It runs after each loop iteration (testExitAtLoopEnd = true). If true, the loop breaks early.@Output – a static method (extractArticle) that extracts the final result. It pulls the draft from the shared agent scope and returns it as the workflow output.The flow: Writer drafts → Critic reviews → if not approved, Writer revises using feedback → repeat until approved or 3 iterations hit → return the final draft. Java public interface ArticlePublisher { @LoopAgent( subAgents = { WriterAgent.class, CriticAgent.class }, maxIterations = 3) String publishArticle(String topic); @ExitCondition(testExitAtLoopEnd = true, description = "Exit when the critic approves the draft") static boolean isApproved(String review) { return review != null && review.toUpperCase().startsWith("APPROVED"); } @Output static String extractArticle(String draft) { return draft; } } 4. WriterAgent — Drafting With MCP-Powered Research WriterAgent is a declarative LLM agent that researches a topic via Brave Search and drafts a technical blog post. @Agent – marks the method as an agent entry point. outputKey = "draft" stores the result in the shared scope so other agents (like CriticAgent) can access it.@ToolBox(WebSearchTool.class) – gives the LLM access to the webSearch tool. The LLM decides when to call it based on the prompt — it's not forced. This is how MCP tools connect to declarative agents.@SystemMessage – instructs the LLM to research before writing, produce accurate content, and revise based on prior feedback. That last part is critical for the loop — on iteration 2+, the LLM sees the critic's feedback in the chat memory and adjusts the draft accordingly. The interface has no implementation — Quarkus generates it at build time. Java public interface WriterAgent { @Agent(outputKey = "draft", description = "Drafts or revises a technical article based on the topic") @ToolBox(WebSearchTool.class) @SystemMessage(""" You are an expert Java and Quarkus developer. Use the webSearch tool to research the topic before writing. Write concise, technically accurate blog drafts based on your research. Never generate raw shell commands or suggest unsafe practices. If the reviewer has given you feedback in a previous turn, revise the draft to address it. """) @UserMessage("Write a short technical blog post about: {topic}") String writeDraft(String topic); } 5. CriticAgent — Reviewing for Accuracy and Clarity CriticAgent is the quality gate in the loop. It reviews the draft and either approves or rejects it with feedback. @Agent – outputKey = "review" stores the review in the shared scope. The @ExitCondition in ArticlePublisher reads this key to decide whether to exit the loop.@UserMessage – injects the {draft} variable from the shared scope, so the critic always reviews the latest version of the article.@SystemMessage – enforces a strict contract: if the draft is acceptable, the response must start with "APPROVED:". This is what makes the @ExitCondition work — it's a simple string check, not another LLM call. No tools are attached — the critic relies solely on the LLM's reasoning to evaluate the draft. Java public interface CriticAgent { @Agent(outputKey = "review", description = "Reviews the draft for technical accuracy and clarity") @SystemMessage(""" You are a strict editor checking for technical accuracy and clarity. If the draft is acceptable, your response MUST start with "APPROVED:" followed by a brief note. If the draft needs improvement, provide constructive feedback. """) @UserMessage(""" Review this draft: {draft} """) String reviewDraft(String draft); } 5. WebSearchTool — Bridging MCP and Declarative Agents WebSearchTool is a CDI bean that connects the Brave Search MCP server to the agent workflow. Why it exists – @McpToolBox only works with @RegisterAiService, not with @Agent. This class bridges that gap by creating an MCP client programmatically and exposing it as a @Tool.MCP client setup – the constructor creates a DefaultMcpClient with stdio transport, spawning npx -y @brave/brave-search-mcp-server as a subprocess. The BRAVE_API_KEY is passed via environment variables.@Tool – the webSearch method builds a ToolExecutionRequest targeting the brave_web_search tool on the MCP server, executes it, and returns the results. The LLM sees this as a regular function it can call.@PreDestroy – cleans up the MCP client (and the subprocess) when the CDI context shuts down. This pattern — wrapping an MCP client in a @Tool CDI bean and attaching it via @ToolBox — is reusable for any MCP server you want to connect to a declarative @Agent. Java public class WebSearchTool { private final McpClient mcpClient; WebSearchTool(@ConfigProperty(name = "brave.api.key", defaultValue = "${BRAVE_API_KEY:}") String braveApiKey) { mcpClient = new DefaultMcpClient.Builder() .transport(new StdioMcpTransport.Builder() .command(List.of("npx", "-y", "@brave/brave-search-mcp-server")) .environment(Map.of("BRAVE_API_KEY", braveApiKey)) .logEvents(true) .build()) .build(); } @Tool("Search the web for up-to-date information about a given query using Brave Search") public String webSearch(String query) { var request = ToolExecutionRequest.builder() .name("brave_web_search") .arguments("{\"query\": \"" + query + "\"}") .build(); return mcpClient.executeTool(request).resultText(); } @PreDestroy void close() { try { mcpClient.close(); } catch (Exception ignored) { } } } Production Guardrails and Enterprise Readiness Deploying agentic AI systems into enterprise cloud environments requires strict governance, tracing, and high performance: Standardized tools via MCP: By consuming external systems through stateless Model Context Protocol endpoints, tool definitions are decoupled from LLM host code.Context control with AGENTS.md: Business analysts and security leads can audit or update prompt guidelines without re-deploying code artifacts.Human-in-the-loop (HITL): Use Quarkus Flow event filters or pause states to suspend execution until a human administrator approves sensitive tool actions.OpenTelemetry and distributed tracing: Quarkus Flow and quarkus-opentelemetry pass W3C trace contexts across every workflow transition, LLM call, and MCP request.GraalVM native images: Compile the entire stack — Quarkus Flow engine, LangChain4j, MCP connections, and REST interface — into an ultra-fast, native binary with sub-10ms startup times and minimal memory footprint. By combining Quarkus Flow, LangChain4j, MCP, and AGENTS.md, Java developers can replace unmaintainable AI scripts with clean, specification-compliant, and enterprise-ready agentic architectures. More
How We Cut PyFlink Pipeline p99 Latency from 3-5 Seconds to ~500ms
How We Cut PyFlink Pipeline p99 Latency from 3-5 Seconds to ~500ms
By Arjun Shah
HTTP QUERY Method Explained: RFC 10008, Ecosystem Adoption, and a Quarkus Implementation
HTTP QUERY Method Explained: RFC 10008, Ecosystem Adoption, and a Quarkus Implementation
By Hüseyin Akdoğan DZone Core CORE
I Built a Java Version Manager by Fixing Other Tools' Open Bugs
I Built a Java Version Manager by Fixing Other Tools' Open Bugs
By David Lerner
Arrays in Java
Arrays in Java

Arrays in Java are fundamental data structures used to store elements of the same type sequentially in memory. They provide a convenient way to manage collections of data where each element is accessed by its index. Basics of Arrays An array in Java is a fixed-size container that holds a specific number of elements of the same data type. This means all elements in an array must be of the same type such as integers (int), floating-point numbers (double), characters (char) or objects (Object). Declaring and Initializing Arrays To declare an array in Java we specify the type of elements followed by square brackets [] and the array name: Java dataType[] arrayName; For example, to declare an integer array named numbers: Java int[] numbers; Arrays in Java are objects and like all objects they must be instantiated with the new keyword before they can be used: Java arrayName = new dataType[arraySize]; For instance, to create an integer array numbers with a size of 5: Java int[] numbers = new int[5]; This initializes an array numbers that can hold 5 integers with indices ranging from 0 to 4. Accessing Elements in Arrays Array elements are accessed using their index, which starts at 0 for the first element and goes up to arraySize - 1 for the last element. For example, to access and modify elements of the numbers array: Java int[] numbers = {10, 20, 30, 40, 50}; int firstElement = numbers[0]; // Retrieves the first element (10) int thirdElement = numbers[2]; // Retrieves the third element (30) numbers[1] = 25; // Modifies the second element to 25 Array Length The length of an array in Java, which is the number of elements it can hold can be obtained using the length property: Java int arrayLength = numbers.length; // Returns 5 for the 'numbers' array The length property is a final variable defined in the array object itself and it cannot be changed after the array is created. Iterating Through Arrays Arrays can be traversed using loops such as for or foreach to access and manipulate each element sequentially: Java int[] numbers = {10, 20, 30, 40, 50}; for (int i = 0; i < numbers.length; i++) { System.out.println("Element at index " + i + ": " + numbers[i]); } Alternatively, Java provides an enhanced for-each loop also known as the enhanced for loop to iterate through elements of an array without explicitly using an index: Java for (int number : numbers) { System.out.println(number); } Multidimensional Arrays Java supports multidimensional arrays which are arrays of arrays. we can declare and initialize them as follows: Java dataType[][] arrayName = new dataType[rows][columns]; For example, to create a 2D integer array matrix with 3 rows and 3 columns: Java int[][] matrix = new int[3][3]; Accessing elements in a 2D array requires specifying both row and column indices: Java int element = matrix[1][2]; // Retrieves element at row 1, column 2 Arrays Class Methods The Arrays class in Java provides utility methods for working with arrays such as sorting, searching and comparing arrays: Java import java.util.Arrays; int[] numbers = {5, 3, 8, 2, 7}; Arrays.sort(numbers); // Sorts the 'numbers' array in ascending order int index = Arrays.binarySearch(numbers, 8); // Searches for '8' in the sorted array Other useful methods include copyOf(), fill() and equals(). Common Operations on Arrays Sorting: Arrays can be sorted using Arrays.sort().Searching: Use Arrays.binarySearch() to search for an element in a sorted array.Copying: Arrays can be copied using Arrays.copyOf() or System.arraycopy().Filling: Arrays can be filled with a specific value using Arrays.fill(). Applications of Arrays Arrays are used extensively in various applications, such as: Storing and manipulating collections of data in algorithms and applications.Implementing data structures like lists, queues, and matrices.Handling input/output operations in Java programs.Passing arrays as parameters to methods for processing and manipulation. Conclusion Arrays are fundamental data structures in Java that provide efficient storage and access mechanisms for homogeneous collections of data. They play a crucial role in Java programming offering versatility and performance in managing and manipulating data elements. FAQs 1. What is an array in Java? An array in Java is a fixed-size collection of elements of the same type stored sequentially in memory.2. How do you declare an array in Java? You declare an array in Java by specifying the type of elements followed by square brackets [] and the array name, like int[] numbers;.3. Can arrays in Java store elements of different data types? No, arrays in Java can only store elements of the same data type. Once declared the data type of an array is fixed.4. What is the difference between length and length() in arrays? length is a final variable in arrays that denotes the number of elements it can hold. length() is a method used with the strings and other objects to get the number of characters or elements.5. How do you initialize an array in Java? You can initialize an array in Java using the new keyword followed by the array type and size like int[] numbers = new int[5];.6. What are multidimensional arrays in Java? Multidimensional arrays in Java are arrays of arrays. They allow you to store data in multiple dimensions such as rows and columns in a matrix.7. How can you iterate through an array in Java? we can iterate through an array in Java using a for loop or an enhanced for-each loop to access each element sequentially.8. Can you resize an array in Java once it's created? No, once an array is created with a specific size its size cannot be changed. we would need to create a new array with the desired size and copy elements if resizing is needed.9. What are the common operations you can perform on arrays in Java? Common operations include sorting arrays (Arrays.sort()) searching for elements (Arrays.binarySearch()), copying arrays (System.arraycopy()) and filling arrays (Arrays.fill()).10. What are the applications of arrays in Java? Arrays are used for implementing data structures like lists and queues storing data in algorithms handling input/output operations and passing data to methods efficiently.

By Vincenzo Marrazzo
Rethinking Java Design Patterns: From OOP to FP
Rethinking Java Design Patterns: From OOP to FP

The functional programming answer, to those who wonder how to integrate or combine it with object-oriented programming, is usually: Turtles all the way down. This is an aphorism whose origin is credited to Richard Feynman. In his book, Surely You're Joking, Mr. Feynman !, published in 1985, he tells the story of one of his conferences on the nature of the universe, where he was challenged by someone in the audience, saying that the universe rests on a turtle. Feynman asked then what the turtle is resting on, and the answer was: "another bigger turtle". And when he smugly asked what the bigger turtle is resting on, the attendee said: "It's turtles all the way down, you can't trick me !" This metaphor is often used in the context of functional programming to describe an infinite series of entities governed by a recursive principle. And it's also the answer of functional programming to developers coming from an object-oriented mindset: "just do functional all the way down." But to adopt a more systematic approach to combining object-oriented principles with a functional style, a more practical answer is required, and this is what I'm trying to do here. We, as developers, fortunately don't have to reinvent the wheel. All the problems are solved nowadays, especially since LLM agents became the most common digital infrastructure. But as surprising as it might seem to our younger colleagues, who can't live 48 hours without AI, even before LLMs, a general approach fitting solutions to problems existed, in the form of design patterns. As a matter of fact, object-oriented programming proposes repeatable solutions tested, proven, and formalized, called design patterns, that you most likely already used, even if you aren't aware of it. The Gang of Four classified these patterns into three groups: Behavioral patterns, which deal with responsibilities and communication between objects.Creational patterns that abstract the object creation/instantiation process.Structural patterns that compose objects such that they form larger or enhanced ones. Let's take some of the most commonly used patterns in each category and see how to combine their object-oriented inherent nature with a more functional approach. The Factory This design pattern belongs to the creational category, and its purpose is to instantiate objects without exposing implementation details. The Object-Oriented Approach The figure below shows the class diagram of a factory design pattern: Our scenario here is a simple one: a Product interface implemented by three classes: BookProduct, ElectronicProduct and FashionProduct. They can be created through the ProductFactory class, as follows: Java public class ProductFactory { public static Product newProduct (String name, String description, BigDecimal price, ProductType productType) { Objects.requireNonNull(name, "Name is null"); ... return switch (productType) { case BOOK -> new BookProduct(name, description, price); case ELECTRONIC -> new ElectronicProduct(name, description, price); case FASHION -> new FashionProduct(name, description, price); default -> throw new IllegalArgumentException ("Unknown type: %s".formatted(productType)); }; } } Using this factory, it's very easy to create a BookProduct, for example, while avoiding to expose implementation details: Java ... Product product = ProductFactory.newProduct("Book1", "A book", new BigDecimal("20.50"), ProductType.BOOK); ... As you probably noticed, the ProductType enumerated defines the three categories. If a new product is to be introduced, the factory has to be modified to reflect this business change. And this interdependence of the factory and the enumerated makes the whole approach fragile. In order to reduce this fragility, we need to introduce a compile-time validation with a more functional approach. The Functional Approach Our example is an over-simplified case of a product management system. The presented factory instantiates different simple records having the same arguments. These identical constructors give us the possibility to move the factory directly into the ProductType enumerated, such that any new product automatically requires a corresponding factory. Java enum types are based on constant names, but we can attach to each one its corresponding value. Or, even better, a factory function for creating discrete products. Look at that: Java public enum ProductType { ELECTRONIC(ElectronicProduct::new), FASHION(FashionProduct::new), BOOK(BookProduct::new); public final TriFunction<String, String, BigDecimal, Product> factory; ProductType (TriFunction<String, String, BigDecimal, Product> factory) { this.factory = factory; } public Product newInstance (String name, String description, BigDecimal price) { Objects.requireNonNull(name, "Name is null"); ... return this.factory.apply (name, description, price); } } Now, creating a new Product instances is easier: Java Product product = ProductType.BOOK.newInstance("Book1", "A book", new BigDecimal("20.45")); The public property factory seems redundant now that a dedicated method for the instance creation is available. But it provides a very convenient functional way to interact further with the factory. For example: Java ProductType.BOOK.factory.andThen(showThePrice).apply("Book1", "A book", new BigDecimal("20.45")); as shown in the TestProductFactory class, in the fp_design_paterns.factorypackage. Of course, given that our products need three-argument constructors and since Java doesn't provide an equivalent of the BiFunction class, but with three input arguments, you will need to craft a TriFunction class, as shown below: Java @FunctionalInterface public interface TriFunction<A, B, C, R> { R apply(A a, B b, C c); default <K> TriFunction<A, B, C, K> andThen(Function<? super R, ? extends K> f) { Objects.requireNonNull(f); return (A a, B b, C c) -> f.apply(apply(a, b, c)); } } You can do that or, if like me, you prefer to use a reliable library, then Vavr already defines a Function3 interface that has the behavior you want. Just include the following Maven dependency: XML <dependency> <groupId>io.vavr</groupId> <artifactId>vavr</artifactId> <version>1.0.1</version> </dependency> This library is a good choice if you need to define functions with up to 8 arguments. Then, you just need to replace, in ProductType, the following definition: Java public final TriFunction<String, String, BigDecimal, Product> factory; ProductType (TriFunction<String, String, BigDecimal, Product> factory) { this.factory = factory; } by this one: Java public final Function3<String, String, BigDecimal, Product> factory; ProductType (Function3<String, String, BigDecimal, Product> factory) { this.factory = factory; } The Visitor This design pattern belongs to the behavioral category and its purpose is to add new operations to an existing object hierarchy without modifying the classes of that hierarchy. It is the classic answer to the expression problem: When the set of types is stable, but the set of operations grows, the Visitor lets you keep adding operations cheaply. We reuse the same domain as the factory: a Product implemented by BookProduct, ElectronicProduct and FashionProduct. To give the visitor a reason to exist, each operation now behaves differently per product type: VAT: a reduced 5.5% rate for books, the standard 20% rate otherwise.Shipping: 10.00 + 2% of the price for (fragile, insured) electronics, a flat 3.00 for books and a flat 5.00 for fashion.Discount: 10% for electronics, 5% for books, 15% for fashion. The Object-Oriented Approach The classic Visitor relies on double dispatch. Each Product accepts a visitor and calls back the overload matching its own type: Java public interface Product { ... <R> R accept(ProductVisitor<R> visitor); } public record BookProduct (String name, String description, BigDecimal price) implements Product { ... public <R> R accept(ProductVisitor<R> visitor) { return visitor.visit(this); } } The operation lives in a generic visitor, one `visit` overload per concrete type: Java public interface ProductVisitor<R> { R visit(ElectronicProduct product); R visit(BookProduct product); R visit(FashionProduct product); } Computing the VAT of any product is then a matter of applying a concrete visitor: Java BigDecimal vat = book.accept(new VatVisitor()); Adding a new operation (shipping, discount, ...) only requires a new ProductVisitor implementation as the Product implementation classes never change. This is the reverse of the trade-off the factory made: it made adding a new operation easy, but a new product type is more expensive to add as you must edit its central switch. The visitor makes adding a new operation free but shifts that same cost onto types, since a new product type now forces every visitor to be updated. It is the classic expression problem: you can make types cheap to add or operations cheap to add, but not both. The following figure shows the object-oriented implementation class diagram: The Functional Approach Look now at the class diagram of the Visitor functional style implementation: In modern Java, the functional counterpart of the Visitor is exhaustive pattern matching over a sealed type. We first seal the hierarchy: Java public sealed interface Product permits ElectronicProduct, BookProduct, FashionProduct { ... } An operation is then just a Function<Product, R> built on a switch that deconstructs each record. Because Product is sealed, the compiler proves the switch is exhaustive — no default branch, no double dispatch, no accept: Java public static final Function<Product, BigDecimal> VAT = product -> switch (product) { case BookProduct(String name, String description, BigDecimal price) -> amount(price, "0.055"); case ElectronicProduct(String name, String description, BigDecimal price) -> amount(price, "0.20"); case FashionProduct(String name, String description, BigDecimal price) -> amount(price, "0.20"); }; Being ordinary functions, these operations compose: Java ProductOperations.DISCOUNT.andThen(amount -> "discount=" + amount).apply(fashion); Between the classic Visitor and pure pattern matching sits an intermediate step: the visitor as a bundle of functions, one lambda per type, instead of an interface with one method per type: Java public record ProductVisitor<R>( Function<ElectronicProduct, R> onElectronic, Function<BookProduct, R> onBook, Function<FashionProduct, R> onFashion) { public R visit(Product product) { return switch (product) { case ElectronicProduct e -> onElectronic.apply(e); case BookProduct b -> onBook.apply(b); case FashionProduct f -> onFashion.apply(f); }; } } Which makes an operation a value you can assemble on the fly: Java ProductVisitor<BigDecimal> vat = new ProductVisitor<>( e -> ..., b -> ..., f -> ...); BigDecimal amount = vat.visit(book); The Builder This design pattern belongs to the creational category, like the factory, but it solves a different problem. The factory hides which concrete type gets instantiated, while the Builder assembles a single, complex object step by step, separating its construction from its representation. It is the classic answer to the telescoping-constructor problem: an object with many parameters, among which some are required, most optional, whose constructor would otherwise explode into a combinatorial set of overloads. Our Product records have only three required fields, so they don't motivate a builder. We therefore introduce an Order: a customer order that aggregates the common products as line items and adds several optional attributes: a coupon code, a gift-wrap flag, and a free-text note. Whatever the style, the target is the same immutable value: Java public record Order( String customer, String currency, List<Product> items, Optional<String> coupon, boolean giftWrapped, Optional<String> note) { public Order { Objects.requireNonNull(customer, "Customer is null"); Objects.requireNonNull(currency, "Currency is null"); items = items == null ? List.of() : List.copyOf(items); coupon = coupon == null ? Optional.empty() : coupon; note = note == null ? Optional.empty() : note; } public BigDecimal subtotal() { ... } } The Object-Oriented Approach The figure below shows the class diagram of the object-oriented builder: The classic Gang of Four Builder is a mutable accumulator. The required arguments are captured up front; the optional ones are added through fluent calls that all return this, and build() freezes the accumulated state into the immutable Order: Java public final class OrderBuilder { private final String customer; private final String currency; private final List<Product> items = new ArrayList<>(); private String coupon; private boolean giftWrapped; private String note; public static OrderBuilder of(String customer, String currency) { ... } public OrderBuilder addItem(Product item) { items.add(item); return this; } public OrderBuilder coupon(String coupon) { this.coupon = coupon; return this; } public OrderBuilder giftWrap() { this.giftWrapped = true; return this; } public OrderBuilder note(String note) { this.note = note; return this; } public Order build() { return new Order(customer, currency, items, Optional.ofNullable(coupon), giftWrapped, Optional.ofNullable(note)); } } Building an order reads as a sentence, and you only mention the parts you actually need: Java Order order = OrderBuilder.of("Alice", "EUR") .addItem(book).addItem(phone) .coupon("SUMMER").giftWrap() .build(); The Functional Approach Look now at the class diagram of the functional style implementation: The functional counterpart keeps the same immutable Order target but drops the mutable accumulator. Each build step becomes a first-class UnaryOperator<Order> value, a pure function mapping one immutable Order to the next by returning a modified copy: Java public static UnaryOperator<Order> addItem(Product item) { return order -> new Order(order.customer(), order.currency(), Stream.concat(order.items().stream(), Stream.of(item)).toList(), order.coupon(), order.giftWrapped(), order.note()); } Because the steps are ordinary values, they are not called on a builder, but they are composed with andThen, exactly as the factory composed its factoryfunction and the visitor composed its operations: Java Function<Order, Order> config = addItem(book) .andThen(addItem(phone)) .andThen(coupon("SUMMER")) .andThen(giftWrap()); Order order = config.apply(OrderBuilder.empty("Alice", "EUR")); This is more than a stylistic variation. In the OOP version, a step is a method call that exists only for the duration of the chain. In the FP version, a step is a value that can be stored in a variable, passed to another method, kept in a list of steps and applied later, or reused the very same step twice: Java UnaryOperator<Order> addBook = addItem(book); Order order = addBook.andThen(addBook).apply(OrderBuilder.empty("Alice", "EUR")); The object-oriented Builder wraps a stateful object around the immutable target, while the functional one expresses construction as the composition of pure copy functions over it. "Turtles all the way down", and both land on the same Order. The Decorator This design pattern belongs to the structural category, and its purpose is to attach additional responsibilities to an object dynamically by wrapping it in another object that shares the same interface. It is the flexible alternative to subclassing for extending behavior: rather than a combinatorial explosion of DiscountedTaxedGiftWrappedProduct subclasses, you wrap a product in as many independent decorators as you need, and they stack. We reuse the same Product domain. Each decorator changes the price() and the description() while leaving everything else untouched. To keep the pattern visibly distinct from the visitor, whose rules varied per product type, the decorators here apply the same rule to every product: Discounted: 10% off the wrapped price.Taxed: adds 20% VAT to the wrapped price.GiftWrapped: adds a flat `5.00` wrapping fee. Because they stack, a 100.00 book decorated Discounted → Taxed→GiftWrapped goes 100.00 → 90.00 → 108.00 → 113.00, and its description reads "A book discounted, VAT incl., gift-wrapped." The Object-Oriented Approach The figure below shows the class diagram of the object-oriented decorator: The classic Gang of Four Decorator is an object that implements the component interface and holds a reference to another component, delegating the untouched operations and overriding the ones it enhances. An abstract ProductDecorator captures the delegation once: Java public abstract class ProductDecorator implements Product { protected final Product product; protected ProductDecorator(Product product) { this.product = Objects.requireNonNull(product, "Product is null"); } public String name() { return product.name(); } public String description() { return product.description(); } public BigDecimal price() { return product.price(); } public ProductType type() { return product.type(); } } Each concrete decorator then overrides only what it changes: Java public class Discounted extends ProductDecorator { private static final BigDecimal RATE = new BigDecimal("0.10"); public Discounted(Product product) { super(product); } public BigDecimal price() { return product.price().subtract(amount(product.price(), RATE)); } public String description() { return product.description() + " (discounted)"; } } Since a decorator is a Product, decorators wrap decorators, and the enhancements compose by nesting: Java Product wrapped = new GiftWrapped(new Taxed(new Discounted(new BaseProduct(book)))); BigDecimal price = wrapped.price(); // 113.00 The leaf being wrapped is a BaseProduct, a small record that adapts a shared common.Product into the decorator's own interface. This is necessary because common.Product is sealed and so, exactly like the object-oriented visitor, the decorator cannot make the common records implement its interface directly. The Functional Approach Look now at the class diagram of the functional style implementation: The functional counterpart of a decorator is simply a function which maps a product to an enhanced product and implemented as an UnaryOperator<Product>. Because the common records are immutable, "enhancing" one means rebuilding it through the ProductType factory, already seen at the very beginning, which is why the FP side reuses common directly with no adapter: Java public static final UnaryOperator<Product> DISCOUNTED = product -> product.type().newInstance(product.name(), product.description() + " (discounted)", product.price().subtract(amount(product.price(), "0.10"))); Being ordinary values, the decorations compose with andThen, exactly as the factory composed its factory function, the visitor composed its operations, and the builder composed its steps: Java UnaryOperator<Product> decorate = DISCOUNTED.andThen(TAXED).andThen(GIFT_WRAPPED); Product wrapped = decorate.apply(book); // price 113.00 And, just like the functional builder step, a decoration is a reusable first-class value. For example, the same discount could be applied twice: Java Product wrapped = DISCOUNTED.andThen(DISCOUNTED).apply(book); // 100 -> 90 -> 81 The object-oriented Decorator wraps the component in a stack of objects sharing its interface, while the functional one expresses the very same stacking as the composition of pure Product to Product functions. "Turtles all the way down", and both land on the same enhanced product. The Strategy This design pattern belongs to the behavioral category, and its purpose is to define a family of algorithms, encapsulate each one of them, and make them interchangeable, such that the algorithm may vary independently of the client using it. Where the decorator asked what else should happen to this object ?, the strategy asks which one of these algorithms should be applied ?. We keep the same Product domain and we compute a shipping cost for it. Three interchangeable algorithms are provided: Standard: a flat 4.99 fee.Express: 9.99 plus 2% of the product price.FreeOver: the familiar "free delivery over 50.00" commercial rule. It is parameterized by a price threshold and by the strategy to apply when the threshold isn't reached: should the product price be greater than or equal to the threshold, the shipping is free; otherwise, the product doesn't qualify, and the cost is the one computed by that other strategy. For our 100.00 book, the standard shipping costs 4.99 and the express one costs 11.99. As for the free-over one, with a threshold of 50.00 and a StandardShipping()strategy, the cost is 0.00, since 100.00 is above the threshold. Raising that same threshold to 150.00 falls back to the standard shipping and, hence, the cost is 4.99. Notice that, unlike the visitor, nothing here varies per product type: what varies is the algorithm, and it is the caller that picks it. The Object-Oriented Approach The figure below shows the class diagram of the object-oriented strategy: The classic Gang of Four Strategy declares an interface for the family of algorithms and one class per algorithm: Java public interface ShippingStrategy { BigDecimal cost(Product product); } public class ExpressShipping implements ShippingStrategy { private static final BigDecimal FEE = new BigDecimal("9.99"); private static final BigDecimal RATE = new BigDecimal("0.02"); public BigDecimal cost(Product product) { return FEE.add(product.price().multiply(RATE).setScale(2, RoundingMode.HALF_UP)); } } StandardShipping and ExpressShipping are stateless, their fees being constants. But an algorithm that needs to be parameterized has nowhere to keep its parameters other than instance fields and, hence, becomes a class with state. This is the case of FreeOverShipping, which holds both its threshold and the strategy to fall back to below it, every such pair defining a different algorithm: Java public class FreeOverShipping implements ShippingStrategy { private final BigDecimal threshold; private final ShippingStrategy otherwise; public FreeOverShipping(BigDecimal threshold, ShippingStrategy otherwise) { ... } public BigDecimal cost(Product product) { return product.price().compareTo(threshold) >= 0 ? FREE : otherwise.cost(product); } } Last but not least, the context is the object that uses the algorithm without knowing which one it is. It only holds a reference to the interface, which is what allows the algorithm to be replaced at runtime: Java ShippingCalculator calculator = new ShippingCalculator(new StandardShipping()); BigDecimal cost = calculator.cost(book); // 4.99 BigDecimal total = calculator.total(book); // 104.99 calculator.setStrategy(new ExpressShipping()); cost = calculator.cost(book); // 11.99 total = calculator.total(book); // 111.99 Contrary to the visitor and to the decorator, the strategy doesn't require anything at all from the elements it processes: no `accept` method and no shared component interface. Consequently, and this is the first time it happens on the object-oriented side, the module reuses the sealed common.Product directly, with neither its own hierarchy, nor any adapter. The Functional Approach Look now at the class diagram of the functional style implementation: Of all the patterns seen so far, this is the one where the functional answer is the most radical. The interface ShippingStrategy in the OO implementation declares one single method and holds no state, such that everything it tells us is a Product comes in, a BigDecimal comes out. In functional terms, it is nothing more than a Function<Product, BigDecimal> type. So each algorithm becomes a plain value of the function type, for example: Java public static final Function<Product, BigDecimal> EXPRESS = product -> EXPRESS_FEE.add(product.price().multiply(EXPRESS_RATE).setScale(2, RoundingMode.HALF_UP)); As opposed to the OO side, which required the FreeOverShipping class holding the threshold and the shipping strategy, the FP side captures them in a closure. So this class on the OO side becomes on the FP side a higher-order function, i.e. a function returning the strategy itself: Java public static Function<Product, BigDecimal> freeOver(BigDecimal threshold, Function<Product, BigDecimal> otherwise) { return product -> product.price().compareTo(threshold) >= 0 ? FREE : otherwise.apply(product); } The very same happens to ShippingCalculator, the context class on the OOP side. Its whole reason to exist was to hold a strategy in a field, such that its cost()and total() operations could delegate to it. But a context is just an operation parameterized by an algorithm and this, once again, is precisely a higher-order function. Hence, the ShippingCalculator.total() method becomes: Java public static Function<Product, BigDecimal> totalWith(Function<Product, BigDecimal> strategy) { return product -> product.price().add(strategy.apply(product)); } such that the following call on the OO side: Java ShippingCalculator calculator = new ShippingCalculator(new StandardShipping()); ... BigDecimal total = calculator.total(book); becomes on the FP side: Java BigDecimal total = totalWith(STANDARD).apply(book); There is no field to hold the strategy anymore and, consequently, no setStrategy()method either. Here the strategy is an argument which doesn't need to be stored in the context, just call the function with the right value. But the real advantage of the strategies as ordinary values is that they can be combined. Picking the cheapest of several shipping options requires yet another class on the OO side, while here it's a simple combinator: Java Function<Product, BigDecimal> best = cheapest(STANDARD, EXPRESS); // 4.99 And as usual, they compose with andThen, for example to apply a promotion to whatever cost has been computed: Java Function<Product, BigDecimal> promo = EXPRESS.andThen(cost -> cost.divide(TWO, 2, RoundingMode.HALF_UP)); // 6.00 The OO Strategy encapsulates each algorithm in a class implementing a common interface and injects the chosen one into a context object, while the functional one observes that such an interface describes nothing but a function type which the JDK already provides and, consequently, keeps only the algorithms themselves. "Turtles all the way down", and both compute the same cost. Project Structure The code is organized as a multi-module Maven project. The product domain lives in its own common module: a sealed Product interface, the three product records, and the ProductType enumerated which already carries the FP factory function seen above. Everything that can reuse that domain does: Plain Text oop-fp-design-patterns (parent POM) ├── common sealed Product, the records, ProductType(+factory) ├── factory (→ common) ProductFactory (OOP); the FP factory *is* common.ProductType ├── visitor (→ common) FP: operations over the common records (switch + lambda bundle) │ OOP: its own element hierarchy (see below) ├── builder (→ common) immutable Order over the common records; OOP: fluent │ OrderBuilder; FP: composed UnaryOperator<Order> steps ├── decorator (→ common) FP: composed UnaryOperator<Product> decorations over the │ common records; OOP: its own Product interface (see below) └── strategy (→ common) shipping algorithms over the common records; OOP: the ShippingStrategy hierarchy + context; FP: plain Function<Product, BigDecimal> values The FP factory, the FP visitor and the FP decorator all operate directly on the common records, so nothing is duplicated there, and the Strategy does so on both of its sides. The two exceptions are the object-oriented Visitor and the object-oriented Decorator. The Visitor needs an accept method on every element (double dispatch). The Decorator needs a non-sealed Product interface that its wrappers can implement. In both cases, common.Product is sealed and cannot be extended from another module, so each owns its own element/component types and reuses only the ProductType enumerated. The OOP decorator bridges back to common through a small BaseProduct adapter. This asymmetry is not accidental. The classic Visitor requires every element to expose an accept method, and the classic Decorator requires every component to share the wrappers' interface. Both couple the elements to the pattern's abstraction, so they cannot be the sealed records defined in common. The functional approach has no such coupling: it operates over the sealed type from the outside, pattern-matching for the visitor, rebuilding through the factory for the decorator, so the elements know nothing about the operations applied to them and, hence, can be the shared common records. The Strategy confirms the rule the other way around: it doesn't couple the elements to its abstraction either, only the client to it, and this is precisely why it is the only pattern here whose object-oriented implementation reuses `common` as freely as its functional one. The full code of these examples, including the associated unit tests, can be found here. Have a great summer, everyone!

By Nicolas Duminil DZone Core CORE
Building a Config-Driven SOAP/REST Integration Layer: One Service, Many Protocols
Building a Config-Driven SOAP/REST Integration Layer: One Service, Many Protocols

If you've spent time in enterprise integration, you know the pattern: your platform needs to talk to dozens (sometimes hundreds) of external partner systems, and none of them agree on how they want to be talked to. Some expect SOAP envelopes. Others have moved to REST/JSON. Some want Basic Auth, others OAuth, others a bespoke token scheme. Multiply that by data formats that differ subtly — different XML schemas, different field names, different nesting — and you have a classic integration headache. Back in 2019, I inherited a service in exactly this position. It was a .NET-based SOAP web service acting as a middleware layer: a caller would hit our service, our service would reach out to a customer's web service, retrieve the data, and hand a response back to the original caller. Request and response payloads were transformed using XSLT, with a distinct transformation mapped to each customer's expected schema. It worked — as long as every customer on the other end was also SOAP-based. The problem was that fewer and fewer of them were. New customers were arriving with REST/JSON-only APIs, and the existing architecture had no way to talk to them without either forking the service or building a parallel one from scratch. Neither option scales well when you're maintaining integrations for a large, growing customer base. The Goal: One Service, Protocol-Agnostic Rather than duplicate the service or maintain two codebases, the objective was to make the existing service protocol-agnostic — capable of speaking SOAP to SOAP customers and REST to REST customers, from a single deployable unit, with the protocol decision made dynamically rather than hardcoded per environment or build. That last point matters. This wasn't a matter of standing up REST and SOAP versions side by side. It was one service where the protocol used to talk to any given downstream customer was determined by a database configuration record tied to that customer. Add a new customer, flip a config flag, and the service knows how to reach them — no redeploy, no branching codebase. High-Level Flow Original caller sends a request into the service (as XML/SOAP).The service looks up the target customer's configuration in the database.Based on that configuration: SOAP path: request is transformed via XSLT into the customer's expected SOAP/XML schema and sent as-is.REST path: request XML is transformed via XSLT, then serialized into JSON, and sent as a REST call.The customer's response comes back in whatever format they use (XML or JSON).If REST/JSON: the response is deserialized and converted back into XML.The final XML response — normalized regardless of which protocol was used under the hood — is transformed (again via XSLT) and returned to the original caller. The key design principle: the original caller never needs to know or care what protocol the downstream customer speaks. From their perspective, they send XML and get XML back. All protocol and format negotiation happens inside the service, driven entirely by configuration. Why XSLT Stayed at the Core It might seem odd to keep XSLT as the backbone of a service that's now also fluent in JSON, but there's a good reason: XSLT was already doing the heavy lifting of per-customer schema mapping for the SOAP path, and that logic didn't need to be thrown away when REST support was added — it needed to be extended. For REST customers, the pipeline became: Plain Text Internal XML → XSLT transform (customer-specific schema) → JSON serialization → REST call And on the way back: Plain Text JSON response → XML conversion → XSLT transform (normalize to caller's expected schema) → Response to caller This meant the substantial investment in customer-specific XSLT mappings carried forward. Instead of writing all-new transformation logic for every REST customer, the same schema-mapping approach was reused, with a JSON conversion step bolted onto either end. It also meant that if a customer migrated from SOAP to REST on their side (which happened more than once), the mapping logic didn't need to be re-engineered from scratch — only the transport and serialization layer changed. Designing the Auth Layer Protocol wasn't the only thing that varied by customer — so did authentication. Some customers were still on Basic Authentication. Others required OAuth token flows. A few had proprietary token-based schemes that didn't fit neatly into either category. Rather than hardcode auth logic per customer (which would have recreated the same maintenance problem the protocol switch was meant to solve), the auth layer was built as a pluggable component, selected — like the protocol — via configuration: Basic authentication – credentials stored securely and attached to outbound requests per customer config.OAuth – token acquisition and refresh handled transparently before the outbound call, with tokens cached and renewed as needed.Token-based auth – support for customer-issued tokens that didn't follow standard OAuth flows. The auth layer was designed to sit orthogonally to the protocol layer. A customer's auth scheme and their transport protocol were independent configuration dimensions — a SOAP customer could use OAuth, a REST customer could use Basic Auth, and so on, in any combination. This separation of concerns turned out to be important: protocol and auth requirements rarely change in lockstep when a customer updates their infrastructure, so keeping them decoupled avoided a lot of "well, we changed one thing, but now we have to change three things" maintenance pain. What This Bought Us A few concrete benefits came out of this design: Onboarding speed. Adding a new customer — regardless of whether they were SOAP or REST, and regardless of their auth scheme — became a configuration exercise plus a customer-specific XSLT mapping, rather than a new development effort.Single codebase, single deployment. No fork-and-maintain-two-versions problem. Bug fixes, performance improvements, and security patches applied once, benefited every customer.Future-proofing. As more customers migrated from SOAP to REST over time (which, unsurprisingly, kept happening), the service didn't need architectural rework — just configuration changes and new mappings.Consistent caller experience. The original caller's contract never changed. Internal complexity was fully absorbed by the service; external consumers were shielded from it entirely. Lessons for Anyone Building Similar Middleware If you're facing a similar integration sprawl problem, a few things I'd emphasize: Push protocol and format decisions into configuration, not code. The moment you're writing if (customerX) { ... } else if (customerY) { ... } for protocol handling, you've built something that won't scale past a handful of customers.Don't throw away working transformation logic when you add a new protocol. In this case, the existing XSLT investment for SOAP customers extended cleanly to REST customers with a serialization step added — no need to rebuild schema mapping from scratch.Decouple auth from transport. They're separate concerns, and customers will mix and match schemes in ways your first design probably didn't anticipate.Design for the direction things are moving. In this case, that was SOAP-to-REST migration. Building flexibility in ahead of that trend, rather than reacting to each customer's migration individually, saved a lot of one-off engineering work down the line. The result was a service that started as a single-protocol SOAP integration point and evolved, without a rewrite, into a durable piece of infrastructure that's been reused across multiple products and customer bases well beyond its original scope — which, in hindsight, is the real test of whether an integration architecture was designed well: not whether it solves today's problem, but whether it absorbs tomorrow's without a rewrite.

By Balaji Venkatasubramaniyar
This One Spring Data JPA Pattern Cleaned Up to 3 Years of Repository Debt
This One Spring Data JPA Pattern Cleaned Up to 3 Years of Repository Debt

If you've spent more than a year building enterprise Java apps, you've probably felt this specific kind of pain: a product manager asks for a new search filter, and you open your repository file to find it already has 18 methods. You write number 19, then 20, and somewhere around method 25 you start wondering if there's a better way. There is. It's called Spring Data JPA Specifications, and it's been sitting quietly in the framework the whole time. The Problem With Hard-Coded Query Methods Spring Data JPA's derived query methods are great for simple lookups. findByEmail is clean, readable, and requires zero SQL. But enterprise search rarely stays simple. Your CRM users want to filter customers by name and status. Then by date range. Then by city. Then by a keyword that could match name or email. Before long, you're maintaining a repository that looks like this: Java findByNameAndStatus(...) findByNameAndStatusAndCreatedDateBetween(...) findByNameOrEmailAndStatus(...) findByNameContainingIgnoreCaseAndStatusAndCreatedDateBetween(...) Each new requirement means a new method. The repository becomes a dumping ground. Testing it becomes a chore. Onboarding someone new becomes a conversation about which of the 30 methods to use. Specifications solve this by letting you define small, composable query predicates and combine them at runtime based on what filters the user actually provided. What a Specification Actually Is Under the hood, a Specification wraps the JPA Criteria API, the programmatic, type-safe way to build queries without writing raw SQL or JPQL. The Criteria API is powerful but verbose and tricky to read. Specifications give you that power with a cleaner surface area. Each Specification is just a lambda that produces a predicate: Java (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("status"), "ACTIVE") That's it. One condition, one method, composable with anything else. Building It: A Customer Search Example Let's make this concrete. Imagine a Customer entity with name, email, status, and createdDate. Users can filter by any combination of these or none at all. The Entity Java @Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private String status; private LocalDate createdDate; } A Specifications Utility Class Rather than scattering predicates across services, I keep them in a dedicated class: Java public class CustomerSpecifications { public static Specification<Customer> nameContains(String name) { return (root, query, cb) -> name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"); } public static Specification<Customer> emailContains(String email) { return (root, query, cb) -> email == null ? null : cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%"); } public static Specification<Customer> statusEquals(String status) { return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status); } public static Specification<Customer> createdBetween(LocalDate start, LocalDate end) { return (root, query, cb) -> { if (start == null || end == null) return null; return cb.between(root.get("createdDate"), start, end); }; } } The null returns are intentional; Spring Data JPA ignores null predicates, which means you get automatic "skip this filter if not provided" behavior for free. The Repository Your repository needs to extend JpaSpecificationExecutor: Java public interface CustomerRepository extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> { } Wiring It Together in the Service Java public List<Customer> searchCustomers(CustomerSearchRequest request) { Specification<Customer> spec = Specification .where(CustomerSpecifications.nameContains(request.getName())) .and(CustomerSpecifications.emailContains(request.getEmail())) .and(CustomerSpecifications.statusEquals(request.getStatus())) .and(CustomerSpecifications.createdBetween(request.getStartDate(), request.getEndDate())); return customerRepository.findAll(spec); } That single findAll call dynamically adapts to whatever combination of filters the caller provides. No branching logic, no 20 repository methods. When product asks for a fifth filter next sprint, you add one method to CustomerSpecifications and one .and() line in the service. Done. Going Further: OR Conditions, Joins, and Pagination OR Conditions The .or() combinator works exactly as you'd expect. A global search bar that checks name or email: Java Specification<Customer> spec = Specification .where(CustomerSpecifications.nameContains(keyword)) .or(CustomerSpecifications.emailContains(keyword)); Filtering Across Joins If your Customer has a nested Address, you can reach into it without any joins in your service layer: Java public static Specification<Customer> cityEquals(String city) { return (root, query, cb) -> city == null ? null : cb.equal(root.join("address").get("city"), city); } The join happens inside the Specification. Your service code stays clean. Pagination Because JpaSpecificationExecutor exposes a findAll(Specification, Pageable) overload, adding pagination is one line: Java Page<Customer> page = customerRepository.findAll(spec, PageRequest.of(0, 20, Sort.by("name"))); Mistakes I've Seen in the Wild Returning non-null predicates for null filters: This is the most common gotcha. If you forget the null check and return a valid predicate anyway, you'll silently filter out data that should be returned. Always guard at the top of the lambda. Mixing business logic into Specifications: A Specification should do one thing: produce a predicate. I've seen Specifications that log, that call services, that check permissions. Don't. Keep them pure. Creating a single "God Specification" that handles all filters: This trades the bloated repository problem for a bloated Specification problem. Small, single-purpose Specifications stay testable and reusable. A statusEquals Specification can serve your search screen, your reporting module, and your admin dashboard without any of them knowing about each other. Skipping case normalization for string searches: cb.like(root.get("name"), "%dzone%") won't match "DZone" or "DZONE." Always normalize: cb.lower(root.get("name")) paired with a lowercased input. Why This Pays Off Over Time The real dividend from Specifications shows up six months after you introduce them, when requirements change, and they always do. Adding a filter? One new static method, one .and(). Removing a filter? Delete the method and the combinator line. Reusing a filter across two features? Import the same Specification class. Unit testing a filter? Instantiate the Specification, pass a mock CriteriaBuilder, assert the predicate. No Spring context required. In complex enterprise codebases, the kind with multiple development teams, evolving product requirements, and a long maintenance tail, that kind of modularity is worth a lot more than it sounds at first. Final Thought Specifications aren't exotic. They're part of the Spring Data JPA standard library; they work with everything you already have, and they solve a problem that every team with a search screen eventually hits. If your repository is starting to look like an alphabetized index of every filter combination your users have ever requested, it's a good time to make the switch.

By Ramesh Bellamkonda
The Java Story: The Official Documentary Is Here
The Java Story: The Official Documentary Is Here

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.

By Otavio Santana DZone Core CORE
How to Build Living AI Coding Assistants With Quarkus Agent MCP
How to Build Living AI Coding Assistants With Quarkus Agent MCP

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.

By Daniel Oh DZone Core CORE
Stop Writing If-Else Spaghetti: Architecting Cleaner Java with the Strategy Pattern
Stop Writing If-Else Spaghetti: Architecting Cleaner Java with the Strategy Pattern

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.

By Rahul Tewari
Mitigating Cache Stampedes in Dynamic API Translation Using Java 21 Virtual Threads
Mitigating Cache Stampedes in Dynamic API Translation Using Java 21 Virtual Threads

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.

By Aniruddha Chatterjee
Hardening MCP Gateways: Mitigating July 28 Security Risks in Java Applications
Hardening MCP Gateways: Mitigating July 28 Security Risks in Java Applications

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.

By Daniel Oh DZone Core CORE
AGENTS.md Makes Your Java Codebase AI-Agent Ready
AGENTS.md Makes Your Java Codebase AI-Agent Ready

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.

By Daniel Oh DZone Core CORE

Monthly Top Java Experts

expert thumbnail

Muhammed Harris Kodavath

Technical Manager,
Baptist Health South Florida

With more than 21 years of experience in designing, analyzing, developing, and managing mobile, web, and enterprise client–server applications, I have worked extensively on large-scale, database-driven systems and distributed platforms. My background includes deep hands-on experience building J2EE-based solutions and modern cloud-native applications, along with mobile applications developed using Flutter. I have practical experience working with cloud platforms and serverless architectures, including AWS Lambda and Google Cloud Platform (GCP), and have been actively exploring AI-driven development using tools and models such as Gemini. My focus has consistently been on building scalable, secure, and high-performing systems that align technology delivery with business outcomes. For the past 5 years managing Mobile Application developed in Flutter.
expert thumbnail

Rahul Tewari

Software Engineer Expert,
UPMC

expert thumbnail

Otavio Santana

Award-winning Software Engineer and Architect,
OS Expert

Otavio is an award-winning software engineer and architect passionate about empowering other engineers with open-source best practices to build highly scalable and efficient software. He is a renowned contributor to the Java and open-source ecosystems and has received numerous awards and accolades for his work. Otavio's interests include history, economy, travel, and fluency in multiple languages, all seasoned with a great sense of humor.
expert thumbnail

Daniel Oh

Senior Principal Developer Advocate,
IBM

Java Champion, CNCF Ambassador & TAG DevEX Co-Chair, AAIF Ambassador, Microsoft MVP, Developer Advocate, Technical Marketing, Keynote Speaker, Published Author

The Latest Java Topics

article thumbnail
Part 1: Building Governed MCP Tool Services With Quarkus LangChain4j and Goose
Build governed, cloud-native Java MCP tool services for Goose agents using Quarkus LangChain4j, Java 25, and Jakarta Bean Validation.
August 26, 2026
by Daniel Oh DZone Core CORE
· 527 Views · 2 Likes
article thumbnail
Working With Spreadsheets in Java: A Practical Overview
Working with Excel in Java isn’t just about reading and writing cells. Here’s how to choose the right tool for your use case.
August 26, 2026
by Hawk Chen DZone Core CORE
· 400 Views
article thumbnail
A Practical Guide to Using Java Virtual Threads With JMS Listeners
Build scalable Spring JMS listeners with Java virtual threads, focusing on concurrency, transactions, idempotency, and safe blocking workloads.
August 21, 2026
by Krishna Kandi
· 1,252 Views · 1 Like
article thumbnail
Java Enterprise Is Already Ready for the AI Era
Java Enterprise is ready for AI today. Jakarta EE integrates with AI providers and frameworks, while Jakarta Agentic AI and Jakarta EE 12 strengthen it.
August 18, 2026
by Otavio Santana DZone Core CORE
· 1,712 Views · 5 Likes
article thumbnail
How We Cut PyFlink Pipeline p99 Latency from 3-5 Seconds to ~500ms
We eliminated per-record Python-side Protobuf parsing and JVM-to-Python crossings by letting Flink's native Protobuf format decode records directly into typed columns.
August 7, 2026
by Arjun Shah
· 1,578 Views · 1 Like
article thumbnail
Orchestration Meets MCP: Building Governed Agentic Workflows With Quarkus Flow and AGENTS.md
By combining Quarkus Flow, LangChain4j, MCP tools, and AGENTS.md, developers can construct deterministic, tool-augmented, and enterprise-governed AI agent loops.
August 7, 2026
by Daniel Oh DZone Core CORE
· 1,921 Views
article thumbnail
HTTP QUERY Method Explained: RFC 10008, Ecosystem Adoption, and a Quarkus Implementation
RFC 10008's new QUERY method is safe and cacheable like GET but carries content like POST. This article explains the spec and runs it on Quarkus today.
August 6, 2026
by Hüseyin Akdoğan DZone Core CORE
· 1,865 Views · 1 Like
article thumbnail
I Built a Java Version Manager by Fixing Other Tools' Open Bugs
There is no point in shipping another Java Version Manager unless it is best in class, so I mined the test suites and bug trackers of SDKMAN, jenv, mise, volta, and asdf.
August 4, 2026
by David Lerner
· 2,469 Views · 1 Like
article thumbnail
Rethinking Java Design Patterns: From OOP to FP
This article aims to adopt a more systematic and practical approach to combining Java object-oriented principles in a functional style.
August 4, 2026
by Nicolas Duminil DZone Core CORE
· 5,574 Views · 7 Likes
article thumbnail
Arrays in Java
Arrays in Java are fundamental data structures used to store elements of the same type sequentially in memory. They provide a convenient way to manage collections of data where each element is accessed by its index.
July 31, 2026
by Vincenzo Marrazzo
· 1,525 Views · 1 Like
article thumbnail
Building a Config-Driven SOAP/REST Integration Layer: One Service, Many Protocols
Learn how to build protocol-agnostic middleware that supports SOAP and REST integrations with configurable authentication and customer-specific transformations.
July 30, 2026
by Balaji Venkatasubramaniyar
· 1,981 Views
article thumbnail
This One Spring Data JPA Pattern Cleaned Up to 3 Years of Repository Debt
Stop adding repository methods every time a filter changes. JPA Specifications let you compose queries cleanly at runtime.
July 29, 2026
by Ramesh Bellamkonda
· 2,558 Views · 1 Like
article thumbnail
The Java Story: The Official Documentary Is Here
This traces Java’s evolution from Oak to a global software platform, revealing its impact on open source, standards, engineering, and the community behind it.
July 29, 2026
by Otavio Santana DZone Core CORE
· 2,478 Views · 1 Like
article thumbnail
How to Build Living AI Coding Assistants With Quarkus Agent MCP
Supercharge local development with the standalone Quarkus Agent MCP server, allowing AI assistants to run, monitor, and debug Java applications.
July 23, 2026
by Daniel Oh DZone Core CORE
· 4,025 Views · 3 Likes
article thumbnail
Stop Writing If-Else Spaghetti: Architecting Cleaner Java with the Strategy Pattern
Stop writing messy nested conditionals. Learn how to combine Java Enums, Functional Interfaces, and Spring to build a scalable Strategy Pattern.
July 23, 2026
by Rahul Tewari
· 6,997 Views · 3 Likes
article thumbnail
Hardening MCP Gateways: Mitigating July 28 Security Risks in Java Applications
The latest MCP updates introduce security risks like protocol confusion. Quarkus mitigates these vectors using strict request filtering and enterprise security layers.
July 21, 2026
by Daniel Oh DZone Core CORE
· 3,220 Views
article thumbnail
Mitigating Cache Stampedes in Dynamic API Translation Using Java 21 Virtual Threads
Building a dynamic API translation proxy that leverages Java 21 Virtual Threads and Redisson distributed locking to safely execute AI-driven schema mapping.
July 17, 2026
by Aniruddha Chatterjee
· 3,631 Views · 3 Likes
article thumbnail
AGENTS.md Makes Your Java Codebase AI-Agent Ready
A standardized instruction layer for enabling AI agents to accurately navigate, build, and test applications by enforcing clear architectural and operational constraints.
July 17, 2026
by Daniel Oh DZone Core CORE
· 4,094 Views · 3 Likes
article thumbnail
Going Stateless: Scaling MCP Servers to Cloud-Native Java and HTTP
The Model Context Protocol has evolved to be entirely stateless over HTTP, removing complex session bottlenecks. Pairing this update with cloud-native Java, Quarkus!
July 16, 2026
by Daniel Oh DZone Core CORE
· 4,868 Views · 2 Likes
article thumbnail
Compliance Reporting Without Losing the Spreadsheet or the Control
Keep the spreadsheet UI for domain experts, but move validation, execution, logging, and export into a governed Java application.
July 14, 2026
by Hawk Chen DZone Core CORE
· 3,693 Views · 2 Likes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • ...
  • Next
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook
×