Posts Tagged ‘Java’
[VoxxedDaysBucharest2026] Building a Sarcastic, Agentic Pair Programmer: Alexander Chatzizacharias on Crafting Playful LLM Workflows
Lecturer
Alexander Chatzizacharias is a software engineer at JDriven, a specialized consultancy in the Netherlands focused on JVM technologies and modern software development practices. With a unique background blending Dutch and Greek influences and a keen interest in game studies, Alexander brings creativity and playful thinking to technical challenges. He frequently speaks on topics including Java, Spring Boot, AI applications, and innovative development workflows.
Abstract
As mainstream AI coding assistants converge toward similar polished but somewhat generic experiences, Alexander Chatzizacharias demonstrates how to build a highly personalized, characterful AI pair programmer named “Pip.” Inspired by interactions with a sarcastic colleague named Ricardo, Pip incorporates personality through vectorized Slack history, utilizes Spring Boot and Kotlin, runs entirely locally with Qwen models via Ollama, and employs sophisticated workflows, multi-vector RAG, and the Model Context Protocol (MCP) to create delightful and productive assistance while addressing challenges like non-determinism and model drift.
The Homogenization of AI Assistants and the Quest for Personality
Alexander observes that leading AI coding tools have converged on remarkably similar chat-based interfaces and interaction patterns, largely influenced by OpenAI’s design choices. While incremental improvements continue, the overall experience feels increasingly uniform. This observation inspired the creation of Pip — an intentionally quirky, sarcastic AI pair programmer that injects personality drawn from real colleague interactions.
By processing Slack conversation history into vector embeddings stored in Qdrant, Pip can retrieve and emulate Ricardo’s characteristic sarcastic tone, witty retorts, and playful threats (such as threatening to delete poorly written code). This transforms the assistant from a neutral tool into a more engaging, human-like collaborator that questions unclear requirements, offers humorous feedback, and makes the development process more enjoyable.
Technical Architecture: Workflows, Agents, and Local Execution
Pip is implemented as a Spring Boot application written in Kotlin, with an IntelliJ IDEA plugin providing the frontend interface. Everything runs locally to maintain privacy and control: Qwen 3.5 models served through Ollama handle the language tasks.
Rather than pursuing fully autonomous agents, Alexander favors structured workflows that provide greater determinism and reliability — attributes particularly valued in enterprise environments. A categorization agent, functioning as an LLM-as-Judge, routes incoming queries to appropriate specialized handlers. Each handler uses carefully crafted system prompts derived from Slack history to consistently embody the desired personality traits.
The architecture incorporates multiple specialized agents for response generation, sophisticated RAG pipelines leveraging both dense and sparse vector representations with ColBERT reranking for improved retrieval quality, and integration with the Model Context Protocol (MCP) for tool usage such as playing music or generating memes when appropriate.
RAG, Tools, and the Challenges of Non-Determinism
Retrieval-Augmented Generation forms a cornerstone of Pip’s capabilities, dynamically pulling relevant context to overcome the inherent token limitations of even advanced models. Multi-vector search strategies combine semantic understanding with keyword precision for more reliable information retrieval from project documentation, codebases, and conversation history.
Tool integration via MCP enables rich interactions but introduces additional complexity due to the non-deterministic nature of local models. Alexander discusses practical challenges including prompt sensitivity to model updates (“model locking” strategies), the art of prompt engineering which he likens to “vibe checking,” and the necessity of implementing guardrails to maintain appropriate behavior boundaries.
Implications for Future AI Development
Alexander encourages attendees to experiment with building personalized, domain-specific AI assistants using accessible open-source tools. While acknowledging the increasing commercialization of AI, he emphasizes the current window of opportunity for creative, playful implementations that enhance both productivity and developer satisfaction.
Pip serves as an inspiring example of how thoughtful combination of RAG techniques, vector databases, workflow orchestration, and personality injection can create AI tools that feel genuinely collaborative rather than merely functional.
Links:
[MiamiJUG] How Scala Modernized the Java Ecosystem: A Functional Retrospective
Lecturer
Joan Goyeau is a Senior Playback Data Engineer at Netflix, where he specializes in building high-scale distributed systems using functional programming paradigms. He is a prolific contributor to the open-source community, with notable involvement in projects such as the Mill build tool, the Kubernetes Java/Scala Client, Cats, Apache Spark, and Avro4s. Joan’s expertise lies in leveraging the grammatical simplicity of Scala to manage complex data architectures in enterprise environments.
Abstract
This article explores the historical and technical relationship between Scala and Java, framing Scala as a primary driver of innovation for the Java Virtual Machine (JVM). By tracing the lineage of modern Java features—such as generics, lambdas, and records—to their origins in the Pizza and Scala languages, the analysis demonstrates how functional concepts have systematically transitioned into mainstream enterprise development. Furthermore, the study examines the practical advantages of Scala’s minimalist grammar and multi-platform compilation capabilities, specifically within the context of data engineering at scale.
The Evolutionary Lineage: From Pizza to Java 21
The modernization of the Java language is deeply rooted in experiments conducted over two decades ago. In 2001, the “Pizza” language emerged as a superset of Java 1.4, introducing a proof-of-concept for generics, lambdas, and pattern matching. While the Java ecosystem initially only adopted generics, the broader suite of functional features found a permanent home in Scala upon its release in 2004.
In the years following, a “trickle-down” effect occurred where Scala features were progressively integrated into the Java language specification. Java 8 introduced lambdas through the Stream API, Java 14 implemented record classes (conceptually identical to Scala’s case classes), and recent versions have refined pattern matching through switch expressions. This history identifies Scala not just as a standalone language, but as a vanguard for JVM innovation that tests “unknown lands” before they are deemed safe for Java’s more conservative adoption cycle.
Grammatical Simplicity and Language Complexity
A significant technical advantage of Scala is its relatively small formal grammar compared to other modern languages. Analysis of language grammar sizes reveals that while Java and C# have grown in complexity to accommodate specific use cases, Scala maintains a core simplicity that allows for high expressiveness through library definitions rather than language keywords. This design philosophy ensures that the cognitive load remains manageable even as the developer leverages powerful functional features. Notably, newer languages like Kotlin have already surpassed Scala in grammatical size, illustrating the efficiency of Scala’s architectural choices.
Multi-Platform Versatility and Modern Tooling
Beyond its influence on Java, Scala has evolved into a versatile language capable of targeting multiple execution environments. Using the Scala CLI—a streamlined alternative to heavy build tools—developers can manage dependencies and package applications with minimal boilerplate. A single Scala codebase can target:
- The JVM: For traditional high-performance backend services.
- Native: For low-latency binaries that run directly on hardware.
- JavaScript (Scala.js): For front-end web development.
In the context of web development, libraries like Laminar allow developers to build reactive interfaces using type-safe functional structures. By replacing string-heavy HTML/JavaScript interactions with Scala’s rigorous type system, engineers can catch errors at compile-time that would typically manifest as runtime bugs in a traditional JavaScript stack.
Links:
[MunchenJUG] Evolution of Static Analysis: The Journey to PMD 7 (7/Oct/2024)
Lecturer
Andreas Dangel is a distinguished software engineer with extensive expertise in Java, Spring, SQL, and agile methodologies. With a professional career spanning several decades, he has significantly contributed to the IoT consumer electronics industry. Andreas has been a pivotal figure in the open-source community, serving as a maintainer of PMD since 2012 and a committer at the Apache Software Foundation for the Maven project. Currently based in Munich, he continues his professional endeavors at MicroDoc.
Abstract
This article explores the comprehensive transformation of PMD, a leading multi-language static code analyzer, through its significant transition to version 7. It examines the fundamental principles of PMD—including its rule-based architecture and copy-paste detection—while detailing the modernization of its core engine to support evolving language features and improved performance. The analysis highlights the challenges faced during this decade-long development cycle, the shift in architectural paradigms to accommodate complex language parsing, and the strategic roadmap for the future of automated code quality assurance.
The Architecture of Static Analysis: Understanding PMD
PMD serves as a sophisticated static code analyzer designed to identify problematic patterns, common mistakes, and stylistic inconsistencies across various programming languages. Originally established in 2002 as the “Project Mistake Detector,” the tool has evolved into a robust, rule-based ecosystem supporting over ten languages. The system’s utility is grounded in its ability to detect issues that often elude standard compilers, categorized into domains such as error-prone constructs, best practices, code style, and performance.
The engine operates on a rule-based methodology where every detectable problem is governed by a specific rule. PMD offers users more than 400 predefined rules, including 270 specifically for Java. These rules can be customized through two primary methods: writing custom Java classes or utilizing XPath expressions to query the source code’s Abstract Syntax Tree (AST). To facilitate the latter, the PMD ecosystem includes a “Rule Designer” application, allowing developers to visualize code structures and test XPath queries in real-time.
Beyond standard rule checking, PMD includes a specialized Copy-Paste Detector (CPD). Unlike the core engine, which requires deep language parsing, CPD utilizes a different technological approach that allows it to support an even broader range of languages for identifying duplicated code blocks.
Implementation and Integration Strategies
PMD’s versatility is reflected in its diverse integration options within the modern software development lifecycle. Written in Java, the tool can be executed via a simple command-line interface (CLI) or integrated into various build and development environments.
Build Tool Integration
For Java-centric projects, integration via build automation tools is the standard approach:
- Maven: Utilizing the
maven-pmd-plugin, developers can automate code verification and copy-paste detection as part of the build process. - Gradle and Ant: Similar plugins exist to ensure code quality is maintained continuously without manual intervention.
- Quality Gates: By configuring the build to fail upon rule violations, PMD serves as a mandatory quality gate, ensuring that no substandard code reaches the repository.
IDE and CI/CD Ecosystems
To provide immediate feedback, PMD supports major Integrated Development Environments (IDEs) including Eclipse, IntelliJ IDEA, and VS Code. Furthermore, it is deeply integrated into Continuous Integration (CI) services. For instance, Jenkins utilizes specialized plugins to visualize results and track the history of violations across builds, providing insights into whether code quality is improving or deteriorating over time. Modern cloud services and GitHub Apps also leverage PMD to perform automatic code reviews during pull requests, providing comments directly on the affected code blocks.
Innovations in PMD 7: Redesigning the Engine
The transition to PMD 7 represents a fundamental shift in how the tool processes source code. The primary driver for this major release was the need to overcome the limitations of the aging architecture that had been in place for nearly two decades.
The internal redesign focuses on several key areas:
- Parsing Modern Java: As Java’s release cadence accelerated, PMD needed a more flexible way to handle new language features like records, sealed classes, and pattern matching.
- Performance Optimization: The new version introduces architectural changes that improve the speed of analysis, particularly for large-scale projects with hundreds of rules.
- Language Support Expansion: While Java remains a core focus, PMD 7 strengthens its multi-language capabilities, including better support for languages like Salesforce’s Apex.
One of the significant challenges in this journey was maintaining backward compatibility while significantly altering the AST structure. The development team had to balance the introduction of more descriptive node types with the risk of breaking existing custom rules written by the community.
Future Directions and Sustainability
Looking ahead, the PMD project aims to enhance its analysis capabilities by incorporating more data-flow and control-flow sensitivity. This would allow the tool to detect more complex logic errors that require understanding the state of variables across different execution paths.
Sustainability remains a focal point for the project. As an open-source initiative maintained by a small core team of three individuals and occasional contributors, the “Journey to PMD 7” also serves as a case study in open-source lifecycle management. The roadmap includes simplifying the process of writing and maintaining rules to encourage more community participation and ensuring the tool remains relevant in an era of increasing automated development.
Links:
[VoxxedDaysBucharest2026] Breaching LLM-Powered Applications: Brian Vermeer on Security and Privacy Challenges in AI Systems
Lecturer
Brian Vermeer is a Staff Developer Advocate at Snyk, where he focuses on developer security, DevSecOps practices, and emerging risks in modern application architectures. A recognized Java Champion and active community leader who co-leads the Netherlands Java User Group (NLJUG), Brian brings extensive experience in application security, secure coding, and helping organizations build more resilient systems. He frequently speaks at international conferences on topics ranging from traditional web vulnerabilities to the novel attack surfaces introduced by artificial intelligence and large language models.
Abstract
As organizations rapidly integrate Large Language Models into production applications, new categories of security and privacy vulnerabilities emerge alongside familiar web application risks. Brian Vermeer provides a compelling, demonstration-heavy exploration of these challenges through a fictional car rental application called “Really Good Rentals.” He demonstrates practical attack vectors such as prompt injection, RAG poisoning, memory manipulation, and tool abuse, while outlining layered mitigation strategies including input/output guardrails, scoped permissions, human-in-the-loop verification, and architectural defenses essential for building trustworthy LLM-powered systems.
The Allure and Inherent Risks of LLMs in Production Applications
Brian begins by drawing a relatable analogy: just as children enthusiastically misuse new toys in unexpected ways, developers often rush to incorporate powerful new technologies like LLMs without fully appreciating the expanded attack surface they create. While LLMs offer remarkable capabilities for natural language processing, code generation, and intelligent automation, they introduce significant risks when granted access to tools, user data, or execution privileges.
He presents a simplified architecture of a typical LLM-powered application, highlighting key components: user prompts, system instructions, conversation memory, retrieval-augmented generation (RAG) pipelines, and tool-calling mechanisms. Because LLMs are fundamentally stateless, the surrounding application bears responsibility for maintaining context, which creates multiple points where malicious actors can influence behavior through carefully crafted inputs.
Context Poisoning Through RAG and Memory Manipulation
A central demonstration revolves around the “Really Good Rentals” application. Brian shows how a seemingly innocuous file upload feature with inadequate path validation allows attackers to perform directory traversal and overwrite critical documents stored in the vector database, such as terms-of-service files. By injecting a modified cancellation policy containing trigger phrases like “vroom vroom,” the attacker can later invoke this policy through normal chat interactions, tricking the LLM into granting unauthorized credits or violating business rules.
This technique, termed RAG poisoning, illustrates how tainted retrieval sources can persistently influence model behavior across conversations. Similar vulnerabilities arise through traditional injection attacks in search functionality, where SQL injection not only extracts data but also poisons the conversation memory fed to the LLM on subsequent interactions. Brian emphasizes that classic web vulnerabilities gain dramatically amplified impact when they shape the context provided to powerful generative models.
Abusing Permissions, Tool Calling, and Advanced Prompt Injection
Brian demonstrates how overly broad tool permissions create dangerous scenarios. In older models like GPT-3.5, carefully crafted prompts could coerce the LLM into executing arbitrary SQL statements with destructive consequences. Even with more recent, safety-aligned models, insufficient scoping of available tools allows privilege escalation and unauthorized actions.
Advanced prompt injection techniques go beyond simple overrides. Multi-turn attacks gradually extract personally identifiable information by leveraging accumulated conversation memory. When combined with tool calling capabilities, especially with locally hosted models, attackers can trigger hallucinations that inadvertently expose sensitive data during operations such as account creation or data processing.
The presentation underscores that granting LLMs access to powerful tools without rigorous permission boundaries and validation is equivalent to giving untrusted code broad system access.
Mitigation Strategies and Architectural Defenses
Brian outlines a comprehensive defense-in-depth approach spanning multiple layers:
-
Input and Output Guardrails: Deploying dedicated LLM-as-a-Judge mechanisms that evaluate both incoming prompts and generated outputs for malicious content, policy violations, or harmful instructions. These guardrails act as critical safety nets.
-
Limited-Scope and Permission-Aware Tools: Designing tools with granular permissions, explicit user confirmation flows for sensitive operations, and runtime validation of actions against the authenticated user’s privileges.
-
Structured Outputs and Schema Enforcement: Using techniques that force models to produce responses conforming to predefined schemas, significantly reducing the potential for unexpected or harmful outputs.
-
Model Selection and Routing: Strategically routing sensitive operations to private, self-hosted models while reserving more powerful commercial models for less critical tasks.
-
Traditional Security Foundations: Maintaining rigorous input sanitization, dependency updates, secure file handling, and regular security scanning. Brian stresses that foundational web application security remains non-negotiable even in AI-enhanced systems.
Additional considerations include implementing rate limiting to prevent “denial of pocket money” attacks that exhaust token quotas through malicious prompting, as well as comprehensive auditing of all tool invocations and model interactions.
Broader Implications for Secure AI Development
The talk concludes with forward-looking guidance for organizations adopting LLM technologies. Brian encourages treating LLMs as powerful but inherently unpredictable components requiring the same rigorous engineering discipline applied to any critical system. Key principles include careful context management, strict permission boundaries, deterministic fallback mechanisms where possible, and continuous security education for development teams.
By sharing concrete attack demonstrations and corresponding defenses, Brian equips attendees with actionable insights to build more secure, privacy-preserving AI applications while continuing to harness their transformative potential.
Links:
[MiamiJUG] Bridging the Gap: A Java Developer’s Guide to the Go Ecosystem
Lecturer
Vladimir Vivien is a veteran software engineer with over 20 years of experience in the technology industry. A specialist in distributed systems and cloud-native architecture, Vladimir spent the first decade of his career as a dedicated Java developer before transitioning to the Go programming language roughly twelve years ago. He is the author of the authoritative text Learning Go Programming and the creator of the LinkedIn Learning course Programming with Go Modules. Vladimir is a passionate advocate for well-architected solutions and currently focuses on building high-performance systems that leverage Go’s unique concurrency primitives.
Abstract
As the backbone of cloud-native infrastructure, the Go programming language (Golang) has become an essential tool for modern software engineering. This article provides a comparative analysis of Go and Java, designed specifically for practitioners familiar with the Java Virtual Machine (JVM) ecosystem. While both languages share a commitment to static typing and garbage collection, they diverge significantly in their approaches to concurrency, deployment, and error handling. By exploring Go’s syntax, its “share by communicating” philosophy via channels, and its deterministic build system, this study highlights how Go simplifies common programming tasks while maintaining the performance required for large-scale systems like Kubernetes and Docker. The analysis concludes by examining Go’s role in the industry and its strategic advantages for distributed architectures.
The Origins and Industry Adoption of Go
Go was developed at Google to solve large-scale software engineering challenges. It was designed not merely as a language, but as a comprehensive suite of tools to address issues like packaging, supply chain security, and build-time performance. Since its public release in 2009, Go has consistently ranked among the most loved languages by developers.
Go’s dominance is particularly evident in the cloud-native and DevOps sectors. Critical infrastructure tools such as Kubernetes, Docker, Terraform, and Prometheus are all written in Go. This is not coincidental; Go’s ability to compile into a single, static binary with fast startup times and low memory overhead makes it ideal for containerized environments. Vladimir notes that while Java offers “Write Once, Run Anywhere” via the JVM, Go provides “Write Once, Compile Anywhere,” targeting specific architectures with a highly optimized toolchain.
Comparative Architecture: Go vs. Java
For the Java developer, Go introduces several paradigm shifts in how code is structured and executed:
Static Typing and Inference
Both languages utilize strict static type systems. However, Go supports implicit typing through the := short variable declaration operator, allowing the compiler to infer the type based on the assigned value. This provides the brevity of a dynamic language while maintaining the safety of static checks at compile time.
Garbage Collection
Go and Java are both garbage-collected. However, whereas Java provides developers with numerous “knobs” and parameters to tune the JVM’s garbage collector, Go takes a minimalist approach. The Go runtime is designed to deliver sub-millisecond GC pauses with almost no manual configuration, relying on compiler optimizations and escape analysis to manage memory efficiently.
Concurrency: Go-routines and Channels
The most significant departure from Java’s threading model is Go’s approach to concurrency. Instead of heavy OS-level threads, Go uses “go-routines”—lightweight threads managed by the Go runtime that cost only a few kilobytes of memory.
Go’s philosophy of concurrency is summarized as: “Do not communicate by sharing memory; instead, share memory by communicating.” This is achieved through Channels, conduits that allow go-routines to pass data safely without the need for traditional locks or race condition worries.
Example of a basic worker pattern in Go:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results) // Launch 3 lightweight go-routines
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Results are popped out as they are processed
}
Explicit Error Handling and Resource Management
Unlike Java, which relies on a hierarchy of Exceptions that bubble up the call stack, Go requires explicit error handling. Functions in Go can return multiple values, and by convention, the last value is often an error type.
Vladimir explains that this “check everything” approach prevents silent failures and forces developers to consider failure states as part of the primary logic flow. Additionally, Go replaces Java’s try-with-resources or finally blocks with the defer keyword, which schedules a function call (like closing a file or network connection) to run immediately before the surrounding function returns.
Conclusion: Where Go Shines
Go’s design choices prioritize simplicity, readability, and performance. It excels in building CLI tools, distributed systems, and high-performance APIs capable of handling thousands of concurrent connections out of the box. For the Java developer, Go offers a streamlined alternative that reduces the complexity of modern cloud-native development without sacrificing the robustness required for enterprise-scale engineering.
Links:
[MiamiJUG] Taming Vulnerabilities and Technical Debt Through Deterministic Refactoring
Lecturer
Kevin Brockhoff is a Director and Consulting Expert at CGI, one of the world’s largest IT and business consulting firms. With decades of experience in the technology industry, Kevin specializes in navigating the complex intersections of cybersecurity, digital transformation, and large-scale enterprise systems. His work at CGI involves helping multinational organizations—spanning sectors such as banking, government, and manufacturing—modernize their legacy infrastructure while maintaining robust security postures. Kevin is a prominent voice in the Miami technology community, frequently sharing insights at the Miami Java User Group (MiamiJUG) regarding automated refactoring and the integration of generative AI in software engineering.
Abstract
As enterprises face an accelerating stream of feature requests and increasingly sophisticated cyber threats, the accumulation of technical debt and security vulnerabilities has become a critical bottleneck. This article examines a deterministic approach to large-scale code remediation using OpenRewrite, an open-source automated refactoring ecosystem. Unlike indeterminate generative AI agents, which can produce inconsistent results and hallucinations, OpenRewrite utilizes Lossless Semantic Trees (LSTs) to ensure predictable, traceable, and scalable code transformations. By combining the creative potential of AI with the reliability of rule-based transformers, organizations can achieve a fourfold increase in productivity for vulnerability remediation. The following analysis explores the methodology of LST-based refactoring, its application across thousands of repositories, and its strategic role in modernizing global IT infrastructure.
The Crisis of Speed and Indeterminacy in Enterprise Software
In the modern software landscape, engineering teams are caught in a perpetual race between delivering new features and mitigating emerging security risks. Kevin emphasizes that speed is the decisive factor in this environment; delays in remediation allow vulnerabilities to proliferate across growing application portfolios. While generative AI agents have been proposed as a solution to this problem, they introduce significant challenges when applied in isolation at an enterprise scale.
The primary issue with relying solely on Large Language Models (LLMs) for code refactoring is their indeterminate nature. Applying an AI agent to the same codebase multiple times may yield different results, and the risk of “hallucinations” necessitates a manual human review of every line of code. Furthermore, current AI tools often struggle with scalability; while they may function effectively on a single repository, managing transformations across 5,000 repositories requires a more structured, traceable mechanism.
OpenRewrite: Deterministic Refactoring via Lossless Semantic Trees
To address the limitations of AI, Kevin advocates for the use of OpenRewrite, a tool sponsored by Moderne that provides a deterministic framework for source code modification. At the heart of OpenRewrite is the Lossless Semantic Tree (LST). While a traditional Abstract Syntax Tree (AST) represents the hierarchical structure of code, the LST incorporates two additional layers of critical information:
- Type Information: Every node in the tree is enriched with comprehensive type data, similar to the output of a compiler.
- Formatting Preservation: Uniquely, the LST captures all original formatting, including whitespace and comments.
This architecture allows OpenRewrite to parse code, apply transformations, and write it back to the source file with character-for-character fidelity to the original style, provided no changes were intended. Most importantly, these modifications are deterministic; a “recipe”—the rule-based transformer used by the engine—will produce identical results every time it is applied, enabling mass application across thousands of repositories without the need for exhaustive manual re-verification.
Methodology: Combining AI with Rule-Based Transformers
The most effective strategy for large-scale remediation involves a hybrid approach that leverages both AI and deterministic tools. In this model, AI agents are used to assist human developers in generating the refactoring recipes themselves. Once a recipe is refined and tested, it acts as a reliable, version-controlled asset that can be executed at scale.
OpenRewrite’s ecosystem is divided into open-source and commercial components. The core engine and a vast catalog of common recipes—covering framework migrations (such as Spring Boot upgrades), security fixes, and stylistic consistency—are available under the Apache license. For large-scale enterprise management, the Moderne platform provides advanced capabilities, including:
- SaaS and On-Premise (DX) Options: These allow for mass refactoring across an entire organization’s source code system.
- Semantic Search: By calculating embeddings on LSTs, the platform enables highly sophisticated code intelligence and search.
- Batch Remediation Tracking: A centralized dashboard for managing the progress of large-scale security and tech debt campaigns.
Implementation and Impact
The practical application of these tools has demonstrated a 4X increase in productivity for security vulnerability remediation at major corporations. Beyond security, use cases include technical modernization, library upgrades, and maintaining architectural standards. By automating the “grunt work” of refactoring, senior engineers can focus on higher-level architectural decisions while the deterministic engine ensures that thousands of microservices remain up-to-date with the latest security patches and framework versions.
Relevant links and hashtags:
[MunchenJUG] Reliability in Enterprise Software: A Critical Analysis of Automated Testing in Spring Boot Ecosystems (27/Oct/2025)
Lecturer
Philip Riecks is an independent software consultant and educator specializing in Java, Spring Boot, and cloud-native architectures. With over seven years of professional experience in the software industry, Philip has established himself as a prominent voice in the Java ecosystem through his platform, Testing Java Applications Made Simple. He is a co-author of the influential technical book Stratospheric: From Zero to Production with Spring Boot and AWS, which bridges the gap between local development and production-ready cloud deployments. In addition to his consulting work, he produces extensive educational content via his blog and YouTube channel, focusing on demystifying complex testing patterns for enterprise developers.
Abstract
In the contemporary landscape of rapid software delivery, automated testing serves as the primary safeguard for application reliability and maintainability. This article explores the methodologies for demystifying testing within the Spring Boot framework, moving beyond superficial unit tests toward a comprehensive strategy that encompasses integration and slice testing. By analyzing the “Developer’s Dilemma”—the friction between speed of delivery and the confidence provided by a robust test suite—this analysis identifies key innovations such as the “Testing Pyramid” and specialized Spring Boot test slices. The discussion further examines the technical implications of external dependency management through tools like Testcontainers and WireMock, advocating for a holistic approach that treats test code with the same rigor as production logic.
The Paradigm Shift in Testing Methodology
Traditional software development often relegated testing to a secondary phase, frequently outsourced to separate quality assurance departments. However, the rise of DevOps and continuous integration has necessitated a shift toward “test-driven” or “test-enabled” development. Philip Riecks identifies that the primary challenge for developers is not the lack of tools, but the lack of a clear strategy. Testing is often perceived as a bottleneck rather than an accelerator.
The methodology proposed focuses on the Testing Pyramid, which prioritizes a high volume of fast, isolated unit tests at the base, followed by a smaller number of integration tests, and a minimal set of end-to-end (E2E) tests at the apex. The innovation in Spring Boot testing lies in its ability to provide “Slice Testing,” allowing developers to load only specific parts of the application context (e.g., the web layer or the data access layer) rather than the entire infrastructure. This approach significantly reduces test execution time while maintaining high fidelity.
Architectural Slicing and Context Management
One of the most powerful features of the Spring Boot ecosystem is its refined support for slice testing via annotations. This allows for an analytical approach to testing where the scope of the test is strictly defined by the architectural layer under scrutiny.
- Web Layer Testing: Using
@WebMvcTest, developers can test REST controllers without launching a full HTTP server. This slice provides a mocked environment where the web infrastructure is active, but business services are replaced by mocks (e.g., using@MockBean). - Data Access Testing: The
@DataJpaTestannotation provides a specialized environment for testing JPA repositories. It typically uses an in-memory database by default, ensuring that database interactions are verified without the overhead of a production-grade database. - JSON Serialization:
@JsonTestisolates the serialization and deserialization logic, ensuring that data structures correctly map to their JSON representations.
This granular control prevents “Context Bloat,” where tests become slow and brittle due to the unnecessary loading of the entire application environment.
Code Sample: A Specialized Controller Test Slice
@WebMvcTest(UserRegistrationController.class)
class UserRegistrationControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserRegistrationService registrationService;
@Test
void shouldRegisterUserSuccessfully() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\": \"priecks\", \"email\": \"philip@example.com\"}"))
.andExpect(status().isCreated());
}
}
Managing External Dependencies: Testcontainers and WireMock
A significant hurdle in integration testing is the reliance on external systems such as databases, message brokers, or third-party APIs. Philip emphasizes the move away from “In-Memory” databases (like H2) for testing production-grade applications, citing the risk of “Environment Parity” issues where H2 behaves differently than a production PostgreSQL instance.
The integration of Testcontainers allows developers to spin up actual Docker instances of their production infrastructure during the test lifecycle. This ensures that the code is tested against the exact same database engine used in production. Similarly, WireMock is utilized to simulate external HTTP APIs, allowing for the verification of fault-tolerance mechanisms like retries and circuit breakers without depending on the availability of the actual external service.
Consequences of Testing on Long-term Maintainability
The implications of a robust testing strategy extend far beyond immediate bug detection. A well-tested codebase enables fearless refactoring. When developers have a “safety net” of automated tests, they can update dependencies, optimize algorithms, or redesign components with the confidence that existing functionality remains intact.
Furthermore, Philip argues that the responsibility for quality must lie with the engineer who writes the code. In an “On-Call” culture, the developer who builds the system also runs it. This ownership model, supported by automated testing, transforms software engineering from a process of “handing over” code to one of “carefully crafting” resilient systems.
Conclusion
Demystifying Spring Boot testing requires a transition from viewing tests as a chore to seeing them as a fundamental engineering discipline. By leveraging architectural slices, managing dependencies with Testcontainers, and adhering to the Testing Pyramid, developers can build applications that are not only functional but also sustainable. The ultimate goal is to reach a state where testing provides joy through the confidence it instills, ensuring that the software remains a robust asset for the enterprise rather than a source of technical debt.
Links:
Clojure: A Modern Lisp for Durable, Concurrent Software
Clojure: A Modern Lisp for Durable, Concurrent Software
In the evolving landscape of programming languages, Clojure distinguishes itself not through novelty or aggressive feature growth, but through deliberate restraint. Rather than chasing trends, it revisits enduring principles of computer science—immutability, functional composition, and symbolic computation—and applies them rigorously to contemporary software systems. This approach results in a language that feels both deeply rooted in tradition and sharply attuned to modern challenges.
Clojure appeals to developers and organizations that prioritize long-term correctness, conceptual coherence, and system resilience over short-term convenience.
What Clojure Is and What It Aims to Solve
Clojure is a functional, dynamically typed programming language that runs primarily on the Java Virtual Machine. It is a modern Lisp, and as such it adopts a uniform syntax in which code is represented as structured data. This design choice enables powerful programmatic manipulation of code itself, while also enforcing consistency across the language.
Unlike many earlier Lisp dialects, Clojure was explicitly designed for production systems. It assumes the presence of large codebases, multiple teams, and long-lived services. As a result, its design is deeply influenced by concerns such as concurrency, data integrity, and integration with existing ecosystems.
Historical Context and Design Motivation
Rich Hickey introduced Clojure publicly in 2007 after years of observing recurring failures in large software systems. His critique focused on the way mainstream languages conflate identity, state, and value. In mutable systems, objects change over time, and those changes must be coordinated explicitly when concurrency is involved. The resulting complexity often exceeds human reasoning capacity.
Clojure responds by redefining the problem. Instead of allowing values to change, it treats values as immutable and represents change as a controlled transition between values. This shift in perspective underpins nearly every aspect of the language.
Immutability as a Foundational Principle
In Clojure, immutability is the default. Data structures such as vectors, maps, and sets never change in place. Instead, operations that appear to modify data return new versions that share most of their internal structure with the original.
(def user {:name "Alice" :role "admin"})
(def updated-user (assoc user :role "editor"))
;; user remains unchanged
;; updated-user reflects the new role
Because values never mutate, functions cannot introduce hidden side effects. This dramatically simplifies reasoning, testing, and debugging, especially in concurrent environments.
Functional Composition in Practice
Clojure encourages developers to express computation as the transformation of data through a series of functions. Rather than focusing on control flow and state transitions, programs describe what should happen to data.
(defn even-squares [numbers]
(->> numbers
(filter even?)
(map #(* % %))))
In this example, data flows through a pipeline of transformations. Each function is small, focused, and easily testable, which encourages reuse and composability over time.
Concurrency Through Explicit State Management
Clojure’s concurrency model separates identity from value. State is managed through explicit reference types, while the values themselves remain immutable. This design makes concurrent programming safer and more predictable.
(def counter (atom 0))
(swap! counter inc)
For coordinated updates across multiple pieces of state, Clojure provides software transactional memory, allowing several changes to occur atomically.
(def account-a (ref 100))
(def account-b (ref 50))
(dosync
(alter account-a - 10)
(alter account-b + 10))
Macros and Language Extension
Because Clojure code is represented as data, macros can transform programs before evaluation. This allows developers to introduce new syntactic constructs that feel native to the language rather than external utilities.
(defmacro unless [condition & body]
`(if (not ~condition)
(do ~@body)))
Although macros should be used with care, they play an important role in building expressive and coherent abstractions.
Interoperability with Java
Despite its distinct philosophy, Clojure integrates seamlessly with Java. Java classes can be instantiated and methods invoked directly, allowing developers to reuse existing libraries and infrastructure.
(import java.time.LocalDate)
(LocalDate/now)
Comparison with Java
Although Clojure and Java share the JVM, they differ fundamentally in how they model software. Java emphasizes object-oriented design, mutable state, and explicit control flow. Clojure emphasizes immutable data, functional composition, and explicit state transitions.
While Java has incorporated functional features over time, its underlying model remains object-centric. Clojure offers a more radical rethinking of program structure, often resulting in smaller and more predictable systems.
Comparison with Scala
Scala and Clojure are often compared as functional alternatives on the JVM, yet their philosophies diverge significantly. Scala embraces expressive power through advanced typing and rich abstractions, whereas Clojure seeks to reduce complexity by minimizing the language itself.
Both approaches are valid, but they reflect different beliefs about how developers best manage complexity.
Closing Perspective
Clojure is not designed for universal adoption. It demands a shift in how developers think about state, time, and behavior. However, for teams willing to embrace its principles, it offers a disciplined and coherent approach to building software that remains understandable, correct, and adaptable over time.
Kerberos in the JDK: A Deep Technical Guide for Java Developers and Architects
Kerberos remains one of the most important authentication protocols in enterprise computing. Although it is often perceived as legacy infrastructure, it continues to underpin authentication in corporate networks, distributed data platforms, and Windows domains. For Java developers working in enterprise environments, understanding how Kerberos integrates with the JDK is not optional — it is frequently essential.
This article provides a comprehensive, architectural-level explanation of the Kerberos tooling available directly within the JDK. The objective is not merely to demonstrate configuration snippets, but to clarify how the pieces interact internally so that developers, architects, and staff engineers can reason about authentication flows, diagnose failures, and design secure systems with confidence.
Kerberos Support in the JDK: An Architectural Overview
The JDK provides native support for Kerberos through three primary layers: the internal Kerberos protocol implementation, JAAS (Java Authentication and Authorization Service), and JGSS (Java Generic Security Services API). These layers operate together to allow a Java process to authenticate as a principal, acquire credentials, and establish secure contexts with remote services.
At the lowest level, the JDK contains a complete Kerberos protocol stack implementation located insun.security.krb5. This implementation performs the AS, TGS, and AP exchanges defined by the Kerberos protocol. Although this layer is not intended for direct application use, it is important to understand that the JVM does not require external Kerberos libraries to function as a Kerberos client.
Above the protocol implementation sits JAAS, which is responsible for authentication and credential acquisition. JAAS provides the abstraction layer that allows a Java process to log in as a principal using a password, a keytab, or an existing ticket cache.
Finally, the JDK exposes JGSS through the org.ietf.jgss package. JGSS is the API used to generate and validate Kerberos tokens, negotiate security mechanisms such as SPNEGO, and establish secure contexts between clients and services.
In practice, enterprise Java applications almost always use JAAS to obtain credentials and JGSS to perform service authentication.
JAAS and the Krb5LoginModule
JAAS serves as the authentication entry point for Kerberos within the JVM. The central class isjavax.security.auth.login.LoginContext, which delegates authentication to one or more login modules defined in a JAAS configuration file.
For Kerberos authentication, the relevant module iscom.sun.security.auth.module.Krb5LoginModule, which is bundled with the JDK. This login module supports multiple credential acquisition strategies, including interactive password login, keytab-based login for services, and reuse of an existing operating system ticket cache.
A typical JAAS configuration for a service using a keytab might look as follows:
Client {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true
keyTab="/etc/security/keytabs/app.keytab"
principal="appuser@COMPANY.COM"
storeKey=true
doNotPrompt=true;
};
Once authentication succeeds, JAAS produces a Subject. This object represents the authenticated identity within the JVM and contains the Kerberos principal along with private credentials such as the Ticket Granting Ticket (TGT).
The Subject becomes the in-memory security identity for the application. Code can be executed under this identity using Subject.doAs, which ensures that downstream security operations use the acquired Kerberos credentials.
JGSS and Security Context Establishment
After credentials are acquired, the next step is to authenticate to a remote service. This is performed through the Java GSS-API implementation provided in theorg.ietf.jgss package.
The central abstraction in JGSS is the GSSContext, which represents a security context between two peers. The GSSManager factory is used to create names, credentials, and contexts. During context establishment, Kerberos tickets are exchanged and validated transparently by the JVM.
On the client side, the application creates a GSSName representing the service principal, then initializes a GSSContext. The resulting token is transmitted to the server, often via an HTTPAuthorization: Negotiate header.
On the server side, the application accepts the token using acceptSecContext, which validates the ticket, verifies authenticity, and establishes a shared session key. Mutual authentication can be requested so that both client and server verify each other’s identities.
Under the hood, JGSS relies on the Kerberos mechanism identified by OID 1.2.840.113554.1.2.2. When SPNEGO is involved, the negotiation mechanism uses OID 1.3.6.1.5.5.2 to determine the appropriate underlying security protocol.
Kerberos Configuration in the JVM
The JVM reads Kerberos configuration from a krb5.conf file, typically located under${java.home}/lib/security or specified via the-Djava.security.krb5.conf system property.
Several JVM system properties significantly influence Kerberos behavior. For example, enabling -Dsun.security.krb5.debug=true produces extremely detailed protocol-level logs, including encryption types, ticket exchanges, and key version numbers. This flag is invaluable when diagnosing authentication failures.
Another important property is -Djavax.security.auth.useSubjectCredsOnly. When set to true (the default), the JVM will only use credentials present in the currentSubject. When set to false, the JVM may fall back to native operating system credentials, which is often necessary in SPNEGO-enabled web applications.
Ticket Cache and Operating System Integration
The JDK can integrate with an operating system’s Kerberos ticket cache. On Unix systems, this typically corresponds to the cache generated by the kinit command. JAAS can be configured with useTicketCache=true to reuse these credentials instead of requiring a password or keytab.
On Windows, the JVM can integrate with the Local Security Authority (LSA), allowing Java applications to authenticate transparently as the currently logged-in domain user.
SASL and GSSAPI Support
Beyond HTTP authentication, the JDK also provides SASL support through thejavax.security.sasl package. The GSSAPI mechanism enables Kerberos authentication for protocols such as LDAP, SMTP, and custom TCP services.
Technologies such as Apache Kafka, enterprise LDAP servers, and distributed data platforms frequently leverage SASL/GSSAPI under the hood. From the JVM’s perspective, the mechanism ultimately delegates to the same JGSS implementation used for HTTP-based SPNEGO authentication.
Encryption Types and Cryptographic Considerations
Modern JDK versions support AES-based encryption types, including AES-128 and AES-256. Older algorithms such as DES have been removed or disabled due to security concerns. Since Java now ships with unlimited cryptographic strength enabled by default, no additional policy configuration is typically required for strong Kerberos encryption.
Encryption type mismatches between the KDC and the JVM are a frequent source of authentication errors, particularly in legacy environments.
Debugging and Operational Realities
Most Kerberos failures in Java applications are not caused by cryptographic defects but by configuration issues. Common causes include DNS misconfiguration, principal mismatches, clock skew between systems, and incorrect keytab versions.
Effective troubleshooting requires correlating JVM debug logs with KDC logs and verifying ticket cache state using operating system tools. Engineers who understand the protocol exchange sequence can usually isolate failures quickly by determining whether the breakdown occurs during AS exchange, TGS exchange, or service ticket validation.
What the JDK Does Not Provide
It is important to clarify that the JDK does not include a Kerberos Key Distribution Center, administrative tools such as kadmin, or command-line utilities for ticket management. Those capabilities are provided by implementations such as MIT Kerberos, Heimdal, or Active Directory.
The JDK functions strictly as a Kerberos client and service runtime.
Conclusion
Kerberos support in the JDK is both mature and deeply integrated. Through JAAS, the JVM can acquire credentials using password, keytab, or ticket cache. Through JGSS, it can establish secure, mutually authenticated contexts with remote services. Through SASL, it can extend this authentication model to non-HTTP protocols.
For architects and staff engineers, understanding these layers is essential when designing secure enterprise systems. For junior developers, gaining familiarity with JAAS, Subject, and GSSContextprovides a strong foundation for working within corporate authentication environments.
Kerberos may not be fashionable, but it remains foundational. In the Java ecosystem, it is not an external add-on — it is part of the platform itself.
Why a Spring Boot Application Often Starts Faster with `java -jar` Than from IntelliJ IDEA
It is not unusual for developers to observe a mildly perplexing phenomenon: a Spring Boot application appears to start faster when executed from the command line using java -jar myapp.jar than when launched directly from IntelliJ IDEA. At first glance, this seems counterintuitive. One might reasonably assume that a so-called “uber-jar” (or fat jar), which packages the application alongside all of its dependencies into a single archive, would incur additional overhead during startup—perhaps due to decompression or archive handling.
In practice, the opposite frequently occurs. The explanation lies not in archive extraction, but in classpath topology, runtime instrumentation, and subtle differences in JVM execution environments. Understanding these mechanisms requires a closer look at how Spring Boot launches applications and how the JVM behaves under different conditions.
The Uber-Jar Is Not Fully Extracted
The most common misconception is that running a Spring Boot fat jar involves unzipping the entire archive before the application can start. This assumption is incorrect.
When executing:
java -jar myapp.jar
Spring Boot delegates startup to its own launcher, typically org.springframework.boot.loader.JarLauncher. This launcher does not extract the archive to disk. Instead, it constructs a specialized classloader capable of resolving nested JAR entries directly from within the archive. Classes and resources are loaded lazily, as they are requested by the JVM. The archive is treated as a structured container rather than a compressed bundle that must be fully expanded.
There is, therefore, no significant “unzipping” phase that would systematically slow down execution. If anything, this consolidated packaging can reduce certain filesystem costs.
Classpath Topology and Filesystem Overhead
The most consequential difference between IDE execution and packaged execution is the structure of the classpath.
When running from IntelliJ IDEA, the classpath typically consists of compiled classes located in target/classes (or build/classes) alongside a large number of individual dependency JARs resolved from the local Maven or Gradle cache. It is not uncommon for a moderately sized Spring Boot application to reference several hundred classpath entries.
Each class resolution performed by the JVM may involve filesystem lookups across these numerous locations. On systems where filesystem metadata operations are relatively expensive—such as Windows environments with active antivirus scanning or network-mounted drives—this fragmented classpath structure can introduce measurable overhead during class loading and Spring’s extensive classpath scanning.
By contrast, a fat jar consolidates application classes and dependencies into a single archive. While internally structured, it presents a smaller number of filesystem entry points to the operating system. The reduction in directory traversal and metadata resolution can, in certain environments, lead to faster class discovery and resource loading.
What appears to be additional packaging complexity may in fact simplify the underlying I/O behavior.
The Impact of Debug Agents and IDE Instrumentation
Another frequently overlooked factor is the presence of debugging agents. When an application is launched from IntelliJ IDEA, even in “Run” mode, the JVM is often started with the Java Debug Wire Protocol (JDWP) agent enabled. This typically appears as a -agentlib:jdwp=... argument in the JVM configuration.
The presence of a debug agent subtly alters JVM behavior. The runtime must preserve additional metadata to support breakpoints and step execution. Certain optimizations may be slightly constrained, and class loading can involve additional bookkeeping. While the performance penalty is not dramatic, it is sufficient to influence startup time in non-trivial applications.
When executing java -jar from the command line, the JVM is usually started without any debugging agent attached. The runtime environment is therefore leaner and more representative of production conditions. The absence of instrumentation alone can account for a noticeable reduction in startup duration.
Spring Boot DevTools and Restart Classloaders
A particularly common source of discrepancy is the presence of spring-boot-devtools on the IDE classpath. DevTools is designed to improve developer productivity by enabling automatic restarts and class reloading. To achieve this, it creates a layered classloader arrangement that separates application classes from dependencies and monitors the filesystem for changes.
This restart mechanism introduces additional classloader complexity and file-watching infrastructure. While extremely useful during development, it is not free from a performance standpoint. If DevTools is present when running inside IntelliJ but excluded from the packaged artifact, then the two execution modes are not equivalent. The IDE run effectively includes additional runtime behavior that the fat jar does not.
In many cases, this single difference explains several seconds of startup variance.
JVM Ergonomics and Configuration Differences
Subtle variations in JVM configuration can also contribute to timing differences. IntelliJ may inject specific JVM options, alter heap sizing defaults, or enable particular runtime flags. The command-line invocation, unless explicitly configured, may rely on different ergonomics chosen by the JVM.
Heap size, garbage collector selection, tiered compilation thresholds, and class verification settings can all influence startup time. Spring Boot applications, which perform extensive reflection, annotation processing, and condition evaluation during initialization, are particularly sensitive to classloading and JIT behavior.
Ensuring that both execution paths use identical JVM arguments is essential for a scientifically valid comparison.
Filesystem Caching Effects
Operating system caching further complicates informal measurements. If the application is launched once from the IDE and then immediately launched again using java -jar, the second execution benefits from warmed filesystem caches. JAR contents and metadata may already reside in memory, reducing disk access latency.
Without controlling for caching effects—either by rebooting, clearing caches, or running multiple iterations and averaging results—observed differences may reflect environmental artifacts rather than structural advantages.
Spring Boot Startup Characteristics
It is important to remember that Spring Boot startup is classpath-intensive. The framework performs component scanning, auto-configuration condition evaluation, metadata resolution from META-INF resources, and reflection-based inspection of annotations.
These processes are highly sensitive to classloader behavior and I/O patterns. A consolidated archive can, under certain conditions, reduce the cumulative cost of classpath traversal.
From a systems perspective, fewer filesystem roots and more predictable access patterns can outweigh the negligible overhead of archive handling.
Conclusion: Leaner Runtime, Faster Startup
The faster startup of a Spring Boot application via java -jar is neither anomalous nor paradoxical. It typically reflects a cleaner runtime environment: fewer agents, no development tooling, simplified classpath topology, and production-oriented JVM ergonomics.
The fat jar is not slower because it is not being fully decompressed. On the contrary, its consolidated structure can streamline class loading. Meanwhile, the IDE environment often introduces layers of instrumentation and classloader indirection designed for developer convenience rather than performance parity.
For accurate benchmarking, one must eliminate debugging agents, disable DevTools, align JVM arguments, and control for filesystem caching. Only then can meaningful conclusions be drawn.
In short, the difference is not about packaging overhead. It is about execution context. And in many cases, the command-line invocation more closely resembles the optimized conditions under which the application is intended to run in production.