DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Image Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones Build AI Agents That Are Ready for Production
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
Build AI Agents That Are Ready for Production

Just dropped: New 2026 “Cloud-Native Foundations” Trend Report. See how teams are tackling complexity, cost & reliability.

Languages

Programming languages allow us to communicate with computers, and they operate like sets of instructions. There are numerous types of languages, including procedural, functional, object-oriented, and more. Whether you’re looking to learn a new language or trying to find some tips or tricks, the resources in the Languages Zone will give you all the information you need and more.

icon
Latest Premium Content
Refcard #357
NoSQL Migration Essentials
NoSQL Migration Essentials
Refcard #071
PostgreSQL Essentials
PostgreSQL Essentials
Refcard #029
MySQL Essentials
MySQL Essentials

DZone's Featured Languages Resources

dbt Meets Apache Flink: One Workflow for Data Engineers

dbt Meets Apache Flink: One Workflow for Data Engineers

By Kai Wähner DZone Core CORE
Data engineers managing batch SQL pipelines on Snowflake, BigQuery, and increasingly Databricks, and streaming pipelines on Apache Flink face a familiar problem: two toolchains, two skill sets, two CI/CD pipelines.dbt is now extending into stream processing. This post explains what that means in practice, why it matters for data engineering teams, and what a concrete implementation looks like with Apache Flink on Confluent Cloud. Data Streaming Meets the Lakehouse Data lakes promised to solve the enterprise data problem. The reality has been messier. Batch pipelines produce stale information, and analytical workloads run hours after the business event occurred. By the time a query runs, the window for action is often already closed. The lakehouse pattern has improved matters. Apache Iceberg has become the dominant open table format, supported across Snowflake, Databricks, BigQuery, and a growing number of query engines. Teams can run SQL analytics directly on data in object storage without duplicating it into a proprietary warehouse. But the lakehouse alone does not solve the real-time problem. Data still arrives as a batch, minutes or hours after the source event. That gap reflects a deeper architectural split. Data streaming with Apache Kafka and Flink is the operational layer: it handles critical SLAs, powers event-driven applications, and keeps business systems running in real time. The lakehouse is the analytical layer: it stores historical data for reporting, ML, and near real-time or batch analytics. These are two distinct workloads with different requirements regarding uptime, data loss, latency, and throughput. They need to coexist without forcing engineers to build and maintain two separate pipelines. How Kafka, Flink, and Iceberg Work Together That is what the combination of Apache Kafka, Apache Flink, and Apache Iceberg addresses. Kafka captures every event at the source and serves as the operational backbone for real-time systems. Flink processes and enriches data in motion, supporting both immediate operational decisions and the preparation of data for downstream analytics. Iceberg stores the result as a governed, queryable table for any analytical engine, whether that is Snowflake, BigQuery, or Databricks. A full treatment of this architecture, including schema evolution, compaction, and catalog integration, is covered here: Data Streaming Meets Lakehouse: Apache Iceberg for Unified Real-Time and Batch Analytics. The question is no longer whether streaming and lakehouse architectures can coexist. They already do. The question is how data engineering teams can work across both without maintaining separate toolchains. That is where dbt enters the picture. What Is dbt? dbt, the data build tool, is an open-source framework for SQL-based data transformation. A dbt model is a SQL SELECT statement saved as a file. dbt infers execution order from how models reference each other using ref(). The standard commands cover the full engineering workflow: dbt run executes the SQL against the target platform, dbt test validates data quality, and dbt docs generate produces a browsable documentation catalog. What made dbt successful is the discipline it brings to SQL work. Before dbt, transformation logic lived in scattered scripts and proprietary ETL tools. dbt replaced that with a code-first, version-controlled workflow with built-in lineage, testing, and documentation. Snowflake and BigQuery are where most dbt adoption lives today. Both are SQL-native and optimized for the ELT pattern dbt was built around. Redshift is a strong third platform in AWS environments. Databricks has seen growing dbt adoption more recently, driven by investments in serverless SQL Warehousing, but its roots are in Spark and Python, making it a newer entrant in the dbt ecosystem. dbt Labs crossed $100 million in ARR in early 2025, with over 5,000 paying customers. Around 90,000 dbt projects are running in production today. The Fivetran and dbt Labs merger, announced in October 2025, created a combined data infrastructure company with nearly $600 million in annual revenue — a clear signal that dbt has moved well beyond a popular open-source tool and into foundational enterprise data infrastructure. dbt Meets Apache Flink: One Workflow for Data Engineers Data engineering teams managing both batch and streaming today operate in two separate realities. Snowflake or BigQuery on one side: dbt models, version-controlled SQL, automated tests, generated docs. Apache Flink on the other: Terraform scripts, custom deployment code, or the Flink console. Skills and practices do not transfer between the two. That separation has a real cost. Streaming pipelines are harder to test, harder to document, and harder to hand over. Many teams compensate by keeping streaming logic minimal and pushing transformation work downstream into the warehouse, which reintroduces latency and undermines the point of streaming. The vision is straightforward: one SQL workflow for both. The engineer who builds dbt models on Snowflake or BigQuery should be able to apply the same approach to an Apache Flink streaming pipeline, without switching tools or rebuilding CI/CD from scratch. Two toolchains mean two testing strategies, two documentation systems, and two skill sets to hire and retain. Governance enforcement becomes inconsistent across the two environments. SQL is the shared foundation that makes this realistic. Flink SQL is mature and production-proven. Snowflake and BigQuery are SQL-native. Apache Iceberg tables are queryable via SQL across multiple engines. dbt wraps SQL with engineering discipline. The model files look the same. The ref() dependency resolution works the same way. Tests and documentation generation work through the same commands. Organizations do not need to hire separate Flink infrastructure specialists. The existing data engineering team can own both sides. Apache Iceberg connects the two worlds at the storage layer. A Flink pipeline writes structured, governed events into an Iceberg table in the organization's own S3 bucket. That same table is immediately readable by Snowflake, BigQuery, or Databricks without any additional ETL step. dbt can model data across the full pipeline: shaping it as it streams through Flink, and transforming it again when it lands in the warehouse for analytics. This is also a direct enabler of the Shift Left Architecture 2.0. The Shift Left approach moves data integration logic closer to the source, applying quality checks, enrichment, and governance in the streaming layer before data lands in the lakehouse. Until now, that required streaming-specific skills that most dbt-native teams did not have. dbt for Flink lowers that barrier considerably. The full architectural detail is covered here: The Shift Left Architecture 2.0: Operational, Analytical and AI Interfaces for Real-Time Data Products. Concrete Example: dbt on Confluent Cloud with Apache Flink The most concrete implementation available today is the dbt-confluent adapter, released by Confluent alongside the confluent-sql Python driver. Both are open source and available on PyPI and GitHub. Data engineers define streaming pipelines as dbt models and deploy them to Flink compute pools using the standard dbt run command. Getting started is a single step: pip install dbt-confluent Three materializations are supported: view for a virtual Flink SQL view over a Kafka topic, streaming_table for a continuous always-current result set, and streaming_source for defining a Kafka topic as a dbt source. Testing is deterministic, using Confluent Cloud's snapshot query capability to return bounded point-in-time results rather than silently passing on timeout. Documentation generation works through INFORMATION_SCHEMA integration, producing the same browsable catalog that Snowflake and BigQuery projects generate. The underlying confluent-sql driver is DB-API v2 compliant, meaning any compatible tool can connect directly to Confluent Cloud Flink: Airflow and Dagster for orchestration, Pandas for snapshot queries, Streamlit for live dashboards, and LangChain for AI agent workflows. For data engineers already working in dbt, this means the skills and practices built around Snowflake or BigQuery transfer directly to the streaming side of the architecture. The Data Engineer Owns Batch and Streaming with dbt The separation between batch and streaming engineering has always been more organizational than technical. Both worlds use SQL. Both require testing, documentation, and reliable deployment. The tools just never bridged the gap, so organizations staffed and operated two distinct engineering disciplines. dbt extending to Apache Flink changes that equation. The data engineer who runs dbt on Snowflake or BigQuery today can apply the same mental model, commands, and CI/CD pipeline to Flink streaming pipelines. No Flink infrastructure specialization required. They write SQL models, define tests, generate documentation, and deploy, exactly as they do for batch. The implication is straightforward. The investment in dbt skills and tooling now extends further into the architecture. Streaming can be adopted incrementally by the same data engineering teams already trusted for batch. One team, one tool, one governance standard, across both operational and analytical workloads. The Flink adapter for dbt is earlier in maturity compared to dbt on Snowflake or BigQuery, and teams should expect to work with an evolving ecosystem. But the foundation is solid, the direction is clear, and the core architectural components are already running in production at scale across multiple industries. The demand from data engineering teams is real and growing. More
Porting GPU Drivers to Rust on ARM64: The Hardest Trial for Kernel-Level Computing

Porting GPU Drivers to Rust on ARM64: The Hardest Trial for Kernel-Level Computing

By Aakash Chaudhary
Rust Has Entered the Kernel. Now Comes the Dangerous Part. A kernel driver does not fail politely. It does not throw a friendly exception, generate a neat stack trace, and ask whether you would like to restart. It corrupts memory, wedges hardware, leaks secrets, freezes the compositor, and leaves engineers spelunking through logs at 2 a.m. with the emotional range of a haunted printer. Nowhere is this more obvious than in GPU drivers. GPU drivers are some of the most complex pieces of kernel-level software in modern systems. They sit between userspace graphics APIs, memory managers, firmware, hardware queues, display engines, DMA buffers, synchronization fences, interrupts, power states, and error recovery paths. They are not “just drivers.” They are operating systems within the operating system. That is why Rust in GPU drivers matters. Rust support exists in the Linux kernel documentation today, but the kernel documentation is careful about its scope: Rust support is still primarily aimed at kernel developers and maintainers building abstractions, drivers, infrastructure, and tooling. It is not a blanket promise that every Rust kernel module is production-ready everywhere. That caution is healthy. Kernel engineering is allergic to magic, and rightly so. Rust does not sprinkle safety dust on MMIO registers. It does not fix firmware bugs. It does not turn a bad architecture into a good one. But Rust does attack one of the oldest and most expensive problems in systems software: memory unsafety. And if Rust can survive in GPU drivers on ARM64, it can survive almost anywhere. Why GPU Drivers Are the Real Test Most Rust-in-kernel discussions start too gently. They talk about simple drivers, toy modules, or safe wrappers around existing C APIs. Useful, yes. Convincing, not enough. The real test is graphics. A modern GPU driver must handle: - Device probing and initialization - Firmware loading and communication - Command submission queues - Shared memory between userspace, kernel, and device - DMA buffer ownership - Synchronization fences - Interrupt handling - Runtime power management - GPU reset and recovery - Userspace ABI compatibility - Performance under real graphical workloads This is where C’s sharp edges show up in full costume. A buffer may outlive the object that owns it, a command queue may still point to freed memory, or firmware may enter a state it should never reach. Reset and teardown paths add more chances for things to go wrong, especially if userspace still holds handles, an interrupt arrives at the wrong moment, or a fence is never signaled. These are not rare problems. They are normal driver engineering problems. The security motivation is also real. Google reported that memory-safety vulnerabilities accounted for 76% of Android vulnerabilities in 2019 and 24% in 2024 after shifting new development toward memory-safe languages. Google later reported major reductions in memory-safety vulnerability density for Rust code compared with Android’s C and C++ code, along with lower rollback rates and less code-review time for Rust changes. Do not overread that. Android is not the Linux DRM subsystem. A phone platform is not a GPU kernel driver. But the broader lesson is hard to ignore: when memory-safety bugs dominate the risk profile, changing the language of new code can change the shape of future vulnerability data. GPU drivers are exactly the kind of high-risk subsystem where that bet deserves serious attention. Why ARM64 Makes the Story More Important ARM64 is no longer just “the phone architecture.” It is in laptops, cloud servers, edge systems, automotive platforms, developer boards, AI devices, and embedded systems. On many ARM64 systems, the GPU is not a discrete PCIe card sitting at a safe distance. It is part of a tightly integrated SoC, sharing memory, power constraints, thermal limits, and firmware relationships with the rest of the system. That changes the stakes. A GPU driver bug on an ARM64 SoC can affect: - System memory safety - Display stability - Battery life - Thermal behavior - Compositor responsiveness - Application latency - AI and graphics workloads sharing the same memory fabric Rust support for AArch64 entered the Linux kernel development story as part of the broader Rust-for-Linux effort, and Linux kernel documentation now includes Rust materials for kernel developers working on Rust abstractions and drivers. That makes ARM64 GPU work more than a curiosity. It is a practical proving ground for the next decade of heterogeneous computing. The future machine is not CPU-only. It is CPU plus GPU plus NPU plus DSP plus video accelerator plus firmware-controlled subsystems. The kernel increasingly becomes an orchestration layer for compute fabrics. That means more shared memory, more queues, more firmware protocols, and more places for C lifetime bugs to hide like raccoons in ductwork. The Serious Case Study: Tyr for Arm Mali The strongest ARM64 GPU example today is Tyr, a Rust-based DRM driver for CSF-based Arm Mali GPUs. Tyr is especially interesting because it is not a random greenfield fantasy. It is a Rust port of Panthor, the C driver for the same class of hardware. Tyr is being developed as a joint effort involving Collabora, Arm, and Google engineers, and it aims to implement the same userspace API as Panthor for compatibility, so it can eventually be used as a drop-in replacement by PanVK, the Vulkan driver. That one design choice is the difference between serious engineering and conference glitter. Tyr is not trying to rewrite the entire graphics stack at once. It is trying to preserve the userspace contract while changing the kernel implementation language. That is exactly how infrastructure migration should be done. Change one major variable. Measure the result. Then decide. The target is also meaningful. CSF-based Arm Mali GPUs use a command-stream frontend where the driver must coordinate with firmware and hardware scheduling mechanisms. That naturally creates state-machine-heavy code, shared buffers, queues, and lifetime-sensitive resource handling. In other words: the kind of code where Rust’s ownership model is not academic. It is directly relevant. The Other Serious Case Study: Nova for NVIDIA GSP GPUs The second important example is Nova, a Rust-based driver for NVIDIA GPUs that use the GPU System Processor, or GSP. Nova is intended to become the successor to Nouveau for GSP-based NVIDIA GPUs in Linux and targets NVIDIA GPUs beginning with the GeForce RTX 20-series Turing family and newer. Nova is not primarily an ARM64 story, but it matters because it shows Rust entering serious DRM and GPU-driver territory, not just small demo modules. Together, Tyr and Nova point toward the same pattern: Rust is being explored where GPU drivers interact with firmware protocols, memory objects, queues, and kernel graphics APIs. This is the important architectural shift. As GPU firmware takes on more low-level responsibilities, host drivers often become protocol coordinators. They manage firmware boot, message queues, device objects, error states, recovery paths, memory handles, and userspace interfaces. That is a very Rust-shaped problem. Not because Rust is trendy. Trendy is how JavaScript frameworks reproduce. Rust is relevant because protocol state, resource ownership, and invalid transitions can often be modeled explicitly in the type system. The Core Engineering Idea: Make Illegal States Hard to Represent Here is the difference between a shallow Rust port and a serious one. A shallow port translates C into Rust line by line and celebrates because the file extension changed. A serious port rethinks dangerous state transitions. GPU drivers are full of implicit states: Buffer: Allocated -> Mapped -> Submitted -> Retired -> Freed Queue: Created -> Active -> Hung -> Recovering -> Destroyed Firmware: Absent -> Loaded -> Booting -> Running -> Failed Device: Probed -> Initialized -> Suspended -> Resuming -> Resetting -> Removed In C, these states are often spread across flags, pointers, locks, comments, and prayers. In Rust, they can be modeled more directly: enum BufferState { Allocated, Mapped, Submitted, Retired, } struct GpuBuffer<S> { handle: BufferHandle, size: usize, state: S, } struct Allocated; struct Mapped; struct Submitted; struct Retired; impl GpuBuffer<Allocated> { fn map(self) -> Result<GpuBuffer<Mapped>, DriverError> { // Map buffer into GPU-visible address space. Ok(GpuBuffer { handle: self.handle, size: self.size, state: Mapped, }) } } impl GpuBuffer<Mapped> { fn submit(self, queue: &mut CommandQueue) -> Result<GpuBuffer<Submitted>, DriverError> { queue.push(self.handle)?; Ok(GpuBuffer { handle: self.handle, size: self.size, state: Submitted, }) } } This is simplified, but the principle is powerful: make the dangerous lifecycle visible in the type system. In C, the rule might live in a comment: /* Do not free this buffer after submission until the fence signals. */ That comment is useful until someone edits a cleanup path six months later and accidentally turns it into historical fiction. Rust lets engineers encode more of that rule into APIs. It does not remove the need for review. It makes review more focused. Unsafe Rust Is Not a Loophole. It Is the Blast Radius. Kernel Rust still needs unsafe. Anyone claiming otherwise should be escorted away from the whiteboard. Drivers touch hardware. They read and write MMIO registers. They interact with C APIs. They manage DMA. They cross boundaries where the compiler cannot verify everything. The right goal is not “no unsafe code.” The right goal is small, explicit, audited unsafe code. Example: struct RegisterBlock { base: *mut u32, } impl RegisterBlock { unsafe fn read_raw(&self, offset: usize) -> u32 { core::ptr::read_volatile(self.base.add(offset)) } fn read_status(&self) -> DeviceStatus { let raw = unsafe { self.read_raw(STATUS_REGISTER_OFFSET) }; DeviceStatus::from_bits(raw) } } The outer driver should not scatter volatile pointer arithmetic everywhere. It should interact with typed operations such as: read_status() submit_queue() reset_engine() map_buffer() signal_fence() This is the real win: concentrate unsafety behind abstractions whose invariants can be documented, reviewed, and tested. Diffuse unsafety is archaeology. Concentrated unsafety is engineering. Research around Rust safety continues to focus on the fact that unsafe Rust and linked unsafe libraries can still compromise memory safety if not isolated or analyzed properly. That is directly relevant to kernel work. Rust is not a force field. It is a tool for shrinking the zone where humans must be perfect. Humans are bad at being perfect. That is why we invented compilers. What a Real Porting Plan Looks Like A credible GPU subsystem port should not start with “rewrite the driver.” That sentence is how you summon budget demons. A better plan looks like this: Phase 1: Choose a narrow subsystem Start where Rust gives clear leverage: - Buffer lifetime tracking - Command submission validation - Firmware message queues - Fence ownership - Reset and recovery state machines Do not begin with the entire DRM subsystem. That is not bravery. That is poor impulse control. Phase 2: Preserve the userspace API Tyr’s compatibility goal with Panthor’s userspace API is exactly the right instinct. If the userspace API remains stable, the migration can focus on kernel-internal safety and maintainability rather than forcing the whole graphics stack to change at once. Stable outside. Safer inside. That is the migration pattern. Phase 3: Wrap unsafe boundaries Every unsafe block should answer three questions: 1. What invariant must be true before this code runs? 2. Who guarantees that invariant? 3. How do we test that the invariant remains true? If the answer is “trust me,” the code is not ready. Trust is not a test strategy. Phase 4: Measure performance with real workloads A Rust GPU driver that is safer but introduces unacceptable frame-time spikes will not survive. Kernel developers care about safety, but they also care about latency, throughput, and not humiliating themselves in front of perf. A real benchmark plan should include: - Frame-time mean, p95, and p99 - Command submission latency - CPU cycles during graphics workloads - Context switches - Interrupt rate - GPU reset recovery time - Firmware boot time - Memory bandwidth - Power draw under sustained load - Thermal throttling behavior A basic harness might look like this: #!/usr/bin/env bash set -euo pipefail DRIVER="${1:?usage: ./bench.sh <driver-name>}" FRAMES="${2:-600}" OUT="results-${DRIVER}-$(date +%Y%m%d-%H%M%S)" mkdir -p "${OUT}" echo "Driver: ${DRIVER}" | tee "${OUT}/metadata.txt" uname -a | tee -a "${OUT}/metadata.txt" lscpu | tee "${OUT}/cpu.txt" sudo dmesg -C perf stat -d \ -o "${OUT}/perf.txt" \ -- ./gpu_workload_runner \ --driver "${DRIVER}" \ --frames "${FRAMES}" \ --json "${OUT}/frames.json" dmesg > "${OUT}/dmesg.log" echo "Benchmark complete: ${OUT}" That is not enough for a final paper-quality result, but it is the start of an honest engineering conversation. One run is a screenshot. Ten controlled runs are evidence. A bar chart with no methodology is decorative nonsense. The ARM64 Benchmark Matrix For an ARM64-focused evaluation, use a matrix like this: Hardware: - Rockchip RK3588 board, such as Rock 5B - Stable power supply - Active cooling - Fixed CPU governor - Fixed GPU governor, where available Software: - Same kernel baseline for C and Rust driver tests - Same Mesa version - Same compositor setting - Same Vulkan or OpenGL workload - Same thermal constraints Workloads: - Synthetic command submission stress test - Vulkan sample workload - Mesa or IGT graphics tests - Real application trace - Forced GPU reset and recovery test Metrics: - Mean frame time - p95 frame time - p99 frame time - CPU cycles - Context switches - Interrupts - GPU resets - Kernel warnings - Power draw This is where many articles fail. They show a chart but hide the setup. That is amateur hour. For DZone, the article can be powerful even without original benchmark results if it clearly presents the benchmark plan. But to become exceptional, it needs real numbers from a reproducible setup. No fake numbers. No “up to 5x faster” nonsense unless measured. The internet already has enough performance astrology. What Rust Will Not Fix Rust can reduce some classes of bugs, but it does not solve the hard parts of driver development by itself. It cannot compensate for poor hardware documentation, opaque firmware behavior, bad scheduling decisions, or flawed abstractions, and it does not make DRM any less complex. Low-level drivers will still require unsafe code, and deadlocks, design mistakes, and logic errors remain very much on the table. The Linux kernel documentation itself remains cautious: Rust support is still aimed at developers and maintainers working on abstractions, drivers, infrastructure, and tools, and it notes that Rust support is still under development, especially for certain configurations. That caution should be repeated, not buried. The correct argument is not: Rust makes kernel drivers safe. The correct argument is: Rust can reduce specific classes of memory and lifetime bugs in new kernel driver code, especially when unsafe hardware access is isolated behind reviewed abstractions. That is less flashy. It is also true. True wins. Why This Matters Beyond GPUs GPU drivers are a proxy for where kernel-level computing is headed. Modern systems are becoming accelerator orchestras. The CPU no longer owns the whole performance story. Work moves across GPUs, NPUs, DSPs, video encoders, SmartNICs, security processors, and firmware-managed islands. That means kernel software must manage: - Shared memory across devices - Complex queue lifetimes - Cross-device synchronization - Firmware protocols - Userspace-visible handles - Device reset semantics - Security boundaries around accelerators If Rust helps in GPU drivers, it can help in other accelerator drivers too. That includes: - AI accelerator drivers - Media encode and decode engines - Camera pipelines - SmartNIC offload paths - Storage acceleration - Embedded display controllers The real innovation is not “Rust replaces C.” That is a bumper sticker. The real innovation is selective memory-safe kernel development for high-risk hardware boundaries. That is a more mature thesis, and it is the one engineering leaders should care about. The Takeaway For application developers, this matters because kernel reliability eventually becomes application reliability. A browser tab, a video call, a game engine, a dashboard, a vision model, or an edge AI pipeline can all be ruined by a GPU stack that mishandles memory or fails recovery. For systems developers, this matters because Rust offers a practical way to encode ownership and state transitions that C leaves to discipline and code review. For engineering leaders, this matters because memory-safety work is not just a security initiative. It is a maintenance initiative. Safer new code can reduce future review burden, rollback risk, and vulnerability exposure, as Google’s Android reporting suggests. For kernel maintainers, this matters because the only acceptable Rust adoption path is incremental, measurable, and compatible with existing kernel development culture. The path is not revolution. It is disciplined infiltration. A Practical Checklist for Teams Before starting a Rust GPU driver effort, answer these questions: 1. What exact bug class are we trying to reduce? 2. Which subsystem has the worst lifetime complexity? 3. Can we preserve the userspace API? 4. Where must unsafe code exist? 5. Can unsafe code be isolated behind reviewed abstractions? 6. What real workload will prove performance? 7. What metric would make us abandon or redesign the port? 8. Who will maintain the Rust abstractions after the prototype hype fades? That last question is brutal and necessary. A prototype is easy. Maintenance is the boss fight. Conclusion: Rust Must Earn Its Place Where the Bugs Are Worst Rust in the Linux kernel should not be judged by toy modules. It should be judged where kernel bugs are expensive: drivers, firmware interfaces, DMA, shared memory, synchronization, and recovery paths. That is why GPU drivers on ARM64 are such an important proving ground. They combine modern hardware complexity with exactly the memory and lifecycle hazards Rust was designed to reduce. Tyr shows the most direct ARM Mali path: a Rust DRM driver for CSF-based Arm Mali GPUs, designed as a port of the C Panthor driver while aiming for userspace API compatibility. Nova shows that Rust GPU driver work is also moving into NVIDIA GSP territory, with ambitions to succeed Nouveau for supported modern NVIDIA GPUs. The lesson is not that Rust is perfect. The lesson is that the next generation of kernel-level computing is becoming too heterogeneous, too concurrent, and too security-sensitive to keep writing every new dangerous subsystem in C by default. Rust will not replace engineering discipline. It will punish the lack of it earlier. And in kernel development, earlier is everything. The Buffer That Came Back From the Dead The board had been running for nine hours. Same workload. Same scene. Same cursed GPU path that used to crash whenever the moon was wrong and the scheduler sneezed. In the old driver, the bug never arrived on command. It preferred drama. Sometimes frame 417. Sometimes frame 12,004. Sometimes only when the engineer walked away, because bugs respect neither science nor lunch. The Rust port failed too, at first. But it failed differently. Not with a corrupted pointer three layers below the crime scene. Not with a dead queue holding a ghost reference to a buffer that should have been buried. It failed at compile time, loudly, rudely, and before anyone had to read 900 lines of logs with the dead-eyed stare of a person reconsidering their career. The compiler pointed at the illegal transition. The engineer stared at it. The GPU kept rendering. For once, the monster had left footprints. More
How to Perform Response Verification in REST-Assured Java for API Testing: Part 2
How to Perform Response Verification in REST-Assured Java for API Testing: Part 2
By Faisal Khatri DZone Core CORE
When TypeScript Types Meet Untrusted AI: Building Type-Safe LLM Pipelines With Runtime Validation
When TypeScript Types Meet Untrusted AI: Building Type-Safe LLM Pipelines With Runtime Validation
By Bhanu Sekhar Guttikonda DZone Core CORE
Replacing JSON With Protobuf in Your Microservice Mesh: A Zero-Downtime Migration Blueprint
Replacing JSON With Protobuf in Your Microservice Mesh: A Zero-Downtime Migration Blueprint
By Bansidhar kadiya
How to Correctly Implement ‘Sneaky Throws’ in Java
How to Correctly Implement ‘Sneaky Throws’ in Java

If you ask Java developers about the concept of ‘Sneaky Throws,’ I am almost sure there will be a couple of opinions that are quite differently expressed, but similar in their meaning. Some will sum it up as being able to throw checked exceptions without declaring them explicitly; others will amend that it means writing functional-style code (lambdas) and being allowed to call methods that throw checked exceptions. Most probably, it will be surely mentioned that there’s a Lombok annotation called exactly @SneakyThrows that solves the problem immediately when put on a method. Last but not least, to outline it in a more pragmatic manner, the concept allows tricking the Java compiler into treating checked exceptions as runtime exceptions. All of these are valid points of view, and to clarify the concept, this article aims to provide a straightforward yet useful approach to handling methods that throw checked exceptions. Let’s jump right in and imagine the following situation. The team is requested to enhance the currently delivered application and implement new functionalities. This obviously happens on a ‘sprint-ly’ basis. Nevertheless, the project has been successfully developed for quite a while now; it also deals with legacy code, and moreover, developers are interacting with other parts of code that were written, let’s say, in a less fortunate manner. Such an example is the class below. Java public class TwoDigitsInteger { private final Integer value; public TwoDigitsInteger(Integer value) { this.value = value; } public boolean isValid() throws NotSetException { if (value == null) { throw new NotSetException("Number value not set."); } return value >= 10 && value <= 99; } public Integer getValue() throws NotSetException { if (value == null) { throw new NotSetException("Number value not set."); } return value; } } Just as its name suggests, it models a two-digit integer number. Instances of this class are immutable; the value is set upon construction, and it declares two methods, one for reading the value — getValue() — and another one for validating it — isValid(). We’re not going to further elaborate on the quality of the code, as it helps in the experiment done. The main issue here, the plot of this article, is the fact that both methods declare a NotSetException as they might throw it under certain circumstances, and even that might be fine unless this Exception hadn’t been a checked one. Java public class NotSetException extends Exception { public NotSetException(String message) { super(message); } } One option (and definitely the one worth taking into account) is to profit and consider the moment a good opportunity to refactor this ‘legacy’ code and at least make the Exception a runtime one. A few unit tests can be written (in case these are missing), then the implementation improved, and focus can be moved on the newly requested features. Nevertheless, for the sake of the experiment in this article, it’s assumed the TwoDigitsInteger class is kept as it currently is and the Exception remains checked. Exception Function Let’s consider a very simple scenario: there is a collection of TwoDigitsIntegers and the intent is to create a string expression that outlines the sum of the numbers. Java List<TwoDigitsInteger> numbers = List.of(new TwoDigitsInteger(10), new TwoDigitsInteger(25), new TwoDigitsInteger(37)); If writing the code as in the test below, Java @Test void sumExpression() { String result = numbers.stream() .map(TwoDigitsInteger::getValue) .map(String::valueOf) .collect(Collectors.joining("+")); Assertions.assertEquals("10+25+37", result); } the Java compiler will complain, saying — Unhandled exception: com.hcd.utilities.NotSetException – as the getValue() method declares a checked Exception and obviously it cannot be used inside a stream. To solve the issue, a try-catch is needed, which makes the code quite difficult to read (and ugly). Not to mention that we’re modifying the state of the joiner as we loop the collection. Java @Test void sumExpression1() { StringJoiner joiner = new StringJoiner("+"); for (TwoDigitsInteger number : numbers) { try { joiner.add(String.valueOf(number.getValue())); } catch (NotSetException e) { throw new RuntimeException(e); } } String result = joiner.toString(); Assertions.assertEquals("10+25+37", result); } In order to overcome this and allow having a fluent API even in situations where checked Exceptions are present, the following ExceptionFunction interface is created. Java @FunctionalInterface public interface ExceptionFunction<T, R, E extends Exception> { R apply(T t) throws E; } It is general enough; it represents a function that accepts one argument (of type T), produces a result (of type R) and when applied, an Exception subclass (of type E) might be thrown. Implementers shall define a single method, which effectively applies the function. Additionally, the following class is defined. Java public final class ExceptionWrapper { public static <T, R, E extends Exception> Function<T, R> apply(ExceptionFunction<T, R, E> function) { return t -> { try { return function.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } ExceptionWrapper() { throw new UnsupportedOperationException("No need to be called."); } } When the ExceptionWrapper#apply() method is called, in case an Exception is thrown, it is wrapped into a RuntimeException one and thrown further irrespective of the type of the initial one (the checked Exception case is obviously covered as well, so we’re good). The ExceptionFunction passed as a parameter represents the initial call that is wrapped to overcome the problem. The previously discussed test is modified to use the ExceptionWrapper#apply() method. Not only does it now compile and run successfully, but the code readability is definitely improved. Java @Test void sumExpression() { String result = numbers.stream() .map(ExceptionWrapper.apply(TwoDigitsInteger::getValue)) .map(String::valueOf) .collect(Collectors.joining("+")); Assertions.assertEquals("10+25+37", result); } Exception Predicate Let’s now consider another straightforward scenario, one in which we want to count only the valid two-digit integers that are found in a designated range. Also, for the sake of this experiment, it’s assumed the previous TwoDigitsInteger class is used. As in the previous case, the following piece of code that would do the job doesn’t compile because of the same reason – Unhandled exception: com.hcd.utilities.NotSetException — as the isValid() method declares a checked exception, and it cannot be used inside a stream. Java long count = IntStream.range(0, 150) .mapToObj(TwoDigitsInteger::new) .filter(TwoDigitsInteger::isValid) .count(); Again, assuming the TwoDigitsInteger is needed, one would have to loop through the numbers, check them in a try-catch for checked NotSetExceptions as isValid() declares it, then pack the Exception as a RuntimeException one and throw it further, finally count the valid number. This is already way too complicated even when only enumerating the steps in natural language. To be able to keep the API fluid and use streams when performing checks that declare checked Exception, the next interface is declared. Java @FunctionalInterface public interface ExceptionPredicate<T, E extends Exception> { boolean test(T t) throws E; } It represents a predicate (a boolean-valued function) of one argument that might throw an Exception subclass. The method evaluates the predicate on the given argument and returns true if the input argument matches, or false otherwise. In addition, the following method is added to the ExceptionWrapper class, very similar to the apply() one. Java public static <T, E extends Exception> Predicate<T> test(ExceptionPredicate<T, E> predicate) { return t -> { try { return predicate.test(t); } catch (Exception e) { throw new RuntimeException(e); } }; } When called, it effectively applies the provided predicate. In case an Exception is thrown, it is wrapped into a RuntimeException one and thrown further. The initial code can now be rewritten as below and successfully compiled and executed. Java @Test void count() { long count = IntStream.range(0, 150) .mapToObj(TwoDigitsInteger::new) .filter(ExceptionWrapper.test(TwoDigitsInteger::isValid)) .count(); Assertions.assertEquals(90, count); } Takeaways Although simple and to-the-point, the presented solution comes in very handy, especially when dealing with functions that declare checked Exceptions and are further used in the code that we produce. For sure, other ready-to-use alternatives already exist, an example being the Lombok @SneakyThrows annotation. Personally, I have very rarely included the Lombok library in any of my projects and as Java introduced the records, this becomes even more unlikely to happen in the future. That being said, the structures described in this article are very helpful, lightweight, and easy to understand and use when needed. ExceptionWrapper, ExceptionFunction and ExceptionPredicate source code is part of the asentinel-orm open-source project. To use it, one may either declare the Maven dependency in their pom.xml file (version 1.72.2 is the latest at the moment of this writing) XML <dependency> <groupId>com.asentinel.common</groupId> <artifactId>asentinel-common</artifactId> <version>1.72.2</version> </dependency> or use it directly if considering there’s too much overhead to include the whole library. Resources [1] – asentinel-orm open-source ORM project is here [2] – the picture was taken at ‘Harry Potter Warner Bros. Studios’, near London

By Horatiu Dan DZone Core CORE
Angular Apps Don’t Need Another Chatbot: Building Agentic UI Workflows With TypeScript
Angular Apps Don’t Need Another Chatbot: Building Agentic UI Workflows With TypeScript

A chatbot can explain data, summarize a screen, or answer questions, yet the application still behaves largely as before: business state lives elsewhere, actions remain disconnected from model output, and the interface is reduced to a transcript. Agentic UI takes a different approach. The model becomes a planner over explicit application capabilities, while Angular remains responsible for state, rendering, validation, authorization boundaries, and interaction. Angular’s current AI guidance already distinguishes basic chat experiences from agentic workflows and dynamic server-driven interfaces, while protocols such as AG-UI formalize streaming state and tool events between agent backends and frontends. Chat Is an Output Channel, Not the Application Model The key design shift is to model an agent run as a workflow rather than a sequence of messages. A purchasing screen, for example, can expose inventory lookup, draft modification, approval, and submission as capabilities. Natural language may start the flow, but the resulting interface should remain a normal application UI: editable fields, status indicators, review cards, validation messages, and explicit confirmation controls. AG-UI follows this direction by defining lifecycle, text, tool-call, and state events instead of treating every interaction as plain assistant text. Tool calls are represented through structured events, allowing a frontend to represent work in progress without attempting to parse model prose into application behavior. A small TypeScript event contract is enough to establish that separation. Discriminated unions fit especially well because TypeScript narrows union members through control flow, making event handling explicit and allowing every event variant to carry only the fields relevant to that state transition. TypeScript type AgentEvent = | { type: 'run.started'; runId: string } | { type: 'draft.updated'; patch: Partial<OrderDraft> } | { type: 'action.requested'; action: PendingAction } | { type: 'action.finished'; actionId: string; result: ActionResult } | { type: 'run.failed'; message: string }; function applyAgentEvent(event: AgentEvent) { switch (event.type) { case 'run.started': phase.set('running'); break; case 'draft.updated': draft.update(value => ({ ...value, ...event.patch })); break; case 'action.requested': pendingAction.set(event.action); phase.set('approval'); break; case 'action.finished': pendingAction.set(null); phase.set('ready'); break; case 'run.failed': error.set(event.message); phase.set('failed'); } } This reducer keeps model output away from direct DOM mutation. The agent proposes state transitions; Angular applies validated events to application state. Network payloads still require runtime validation because TypeScript annotations disappear during compilation and do not perform runtime checks. Casting arbitrary JSON to AgentEvent therefore establishes a compiler assumption rather than a runtime trust boundary. Let Angular Render State Instead of Model Prose Signals provide a natural projection layer for agent-driven state because Angular tracks signal reads and updates dependent consumers when signal values change. Angular also provides asynchronous resource APIs for integrating async data with signal-based code, although workflow event streams often benefit from an explicit reducer because event ordering, approvals, resumable execution, and intermediate actions are domain state rather than ordinary resource loading TypeScript const phase = signal<'idle' | 'running' | 'approval' | 'ready' | 'failed'>('idle'); const draft = signal<OrderDraft>(emptyDraft); const pendingAction = signal<PendingAction | null>(null); const error = signal<string | null>(null); const busy = computed(() => phase() === 'running'); const approvalRequired = computed(() => pendingAction() !== null); The template can render that workflow through established Angular components instead of constructing another interaction model inside a chat transcript. Signal reads naturally connect the workflow state to Angular rendering. HTML @if (pendingAction(); as action) { <app-action-review [action]="action" (approve)="approve(action.id)" (reject)="reject(action.id)" /> } <app-order-editor [draft]="draft()" [disabled]="busy()" /> This boundary also preserves the application’s existing component system. The model determines intent and proposes changes, while known Angular components determine presentation and interaction semantics. That division becomes increasingly important as model-produced output becomes more dynamic, since a trusted component vocabulary provides substantially more control than arbitrary generated markup. A2UI applies the same general principle by allowing agents to describe interface intent while host applications render native components from an approved catalog. Capabilities Need Stronger Boundaries Than Prompts An agent should not receive an unrestricted instruction to invoke arbitrary frontend behavior. Capabilities should be explicit, typed, narrow, and policy-aware. AG-UI distinguishes backend-defined and client-provided tools, including tools that request human input or confirmation. Angular 22 also introduced experimental WebMCP support for exposing structured application tools to agents running in browser environments, with the explicit goal of reducing dependence on brittle DOM-level interaction. A capability registry keeps execution deterministic while still allowing an agent to choose among operations deliberately exposed by the application. TypeScript type CapabilityName = 'lookupInventory' | 'applyDiscount' | 'submitOrder'; const capabilities = { lookupInventory: { mutates: false, validate: validateInventoryArgs, execute: lookupInventory }, applyDiscount: { mutates: true, validate: validateDiscountArgs, execute: applyDiscount }, submitOrder: { mutates: true, requiresApproval: true, validate: validateSubmitArgs, execute: submitOrder } } satisfies Record<CapabilityName, Capability>; async function dispatch(action: PendingAction) { const capability = capabilities[action.name]; const args = capability.validate(action.args); return capability.execute(args); } The satisfies operator verifies that the registry conforms to the required shape while retaining the more specific inferred type of each value, making capability registries practical without unnecessarily widening their entries. The runtime validate operation solves a different problem: tool arguments originated outside the TypeScript compiler and therefore cannot become trustworthy merely through static type declarations. Human approval should interrupt a run rather than merely decorate a destructive operation with a confirmation sentence. AG-UI formalizes this concept through interrupts: an agent run can pause for approval or structured input and later resume with an explicit response. That model maps naturally to Angular workflow state because an approval card can remain visible until a correlated decision is submitted. TypeScript async function approve(actionId: string) { const action = pendingAction(); if (!action || action.id !== actionId) { return; } await agent.resume({ actionId, decision: 'approved' }); } Server-side authorization still remains authoritative; frontend approval represents an interaction decision rather than permission to bypass backend policy. The same rule applies to generated content. Angular’s security guidance treats untrusted values as a security concern and specifically warns that bypassing sanitization with untrusted content can expose applications to cross-site scripting vulnerabilities. Model output therefore belongs in the same untrusted-input category as any other external payload. Dynamic UI Should Come From a Catalog, Not Arbitrary Markup Some workflows need more than predefined page states. An agent may need to choose whether a result is best represented as a form, comparison view, approval card, or status panel. A2UI addresses that requirement with a declarative format in which an agent describes UI intent and the host renders the result using native components from a trusted catalog. The project supports Angular among its rendering targets and is explicitly designed around declarative UI descriptions rather than transferring arbitrary executable frontend code across the agent boundary. That distinction matters. Generating raw HTML and injecting it into Angular creates unnecessary sanitization pressure, weakens design-system consistency, and expands the amount of generated material that must be treated as untrusted. A constrained component vocabulary limits what an agent can request while retaining enough flexibility for adaptive layouts. Google’s A2UI documentation describes the same model as declarative JSON rendered through components controlled by the host application rather than raw HTML, CSS, or JavaScript supplied by the remote agent. AG-UI and A2UI consequently address different parts of the same frontend problem. AG-UI provides the interaction stream for runs, state changes, tool calls, and human-in-the-loop control, while A2UI provides a declarative mechanism for richer agent-selected views. Neither protocol is mandatory for an Angular implementation; an application-specific event protocol and component registry can implement the same core ideas. Standardization becomes more valuable when several agent runtimes or frontend surfaces must share the same interaction contract. Angular’s experimental WebMCP support introduces another useful direction: capabilities already present in an application can be exposed as structured tools rather than rediscovered through DOM manipulation. Because Angular currently marks the relevant WebMCP APIs as experimental, isolating them behind the same capability layer prevents an emerging transport mechanism from leaking into business logic. Conclusion Agentic Angular interfaces become useful when AI stops being a chat-shaped feature and starts participating in typed application workflows. The durable boundary is not a prompt; it is a contract consisting of validated events, explicit capabilities, observable state transitions, controlled rendering, and deliberate approval points. Angular Signals provide a reactive surface for projecting agent state, TypeScript discriminated unions make workflow events tractable, and emerging protocols such as AG-UI, A2UI, and WebMCP demonstrate a broader shift toward structured agent-to-application interaction. The strongest implementation keeps business authority and UI integrity inside the application while allowing the model to plan, propose, and coordinate. That boundary produces software that remains testable, accessible, secure, and understandable even as agent behavior becomes substantially more capable.

By Bhanu Sekhar Guttikonda DZone Core CORE
How to Test GET API Requests With Playwright TypeScript
How to Test GET API Requests With Playwright TypeScript

Playwright is a widely used open-source test automation framework developed by Microsoft. It allows developers and test automation engineers to reliably automate web applications across multiple browsers and platforms. Playwright supports several popular programming languages, such as JavaScript, TypeScript, Java, C#, and Python. One of its standout features is built-in API automation testing, which gives it a strong advantage over many traditional web automation frameworks. In this tutorial, we’ll explore how to use Playwright with TypeScript and learn how to automate GET API requests. Installing Playwright With TypeScript The first step is to install and set up Playwright with TypeScript. Let’s create a new folder and run the following command by navigating to the newly created folder: Plain Text npm init playwright@latest After running the above command, make sure you select “TypeScript” as the programming language. Next, select the appropriate options for the other questions asked by the Playwright setup and install Playwright and its dependencies. Application Under Test We’ll be using free, publicly available RESTful e-commerce APIs from a demo e-commerce application hosted on GitHub. The project can be run locally using either Node.js or Docker and provides several order management APIs, including creating, updating, retrieving, and deleting orders. How to Test GET API Requests With Playwright TypeScript Playwright provides a request API that lets us create and manage HTTP request contexts. Let’s learn about sending GET requests step-by-step with different options: Send a GET API Request and Verify the Status Code Let’s perform a simple test by sending a GET API request and verifying that a 200 status code is returned in the response. TypeScript import { test, expect } from "@playwright/test"; test("Get Order details API test with status code check", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { user_id: "1", }, }); expect(response.status()).toBe(200); }); Code Walkthrough This test sends a GET request to the /getOrder API with a user_id parameter using Playwright’s request context. It verifies that the API responds successfully by checking that the status code returned is 200. The following are additional details about this test: test(…): The test(…) defines a Playwright test case. The string “Get Order details API test with status code check” is the name of the test and will be shown in the Playwright report.async ({ request }): It uses Playwright’s built-in request fixture, which injects an APIRequestContext and allows us to make HTTP calls.Sending a GET request: The following line sends an HTTP GET request to the /getOrder/ endpoint. TypeScript const response = await request.get("http://localhost:3004/getOrder/", { The await keyword pauses execution until the API responds. Finally, the result is stored in the response variable, which is an APIResponse object. Params: The following line adds a query parameter “user_id” to the GET request. TypeScript params: { user_id: "1", }, expect statement: The response.status() retrieves the HTTP status code returned by the API, and expect(…).toBe(200) asserts that the API responded successfully with HTTP 200 OK. Similarly, we can perform the assertions for a status code other than 200. In the code below, the value for the “id” parameter is updated to “2”, for which no records exist in the system. TypeScript test("Get Order details API test with status code 404", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { id: 2, }, }); expect(response.status()).toBe(404); }); The expectation is that it should return status code 404. The expect(...) statement performs the required status code check. Send a GET API Request With Multiple Parameters There are situations where we need to provide multiple parameters in the GET request to filter and fetch the required records. Using Playwright TypeScript, multiple parameters can be supplied while sending a GET request, as shown below: TypeScript test("Get Order details API test with multiple params", async ({ request }) => { const params = { id: 1, user_id: "1", product_id: "79", }; const response = await request.get("http://localhost:3004/getOrder/", { params, }); expect(response.status()).toBe(200); }); This test defines multiple query parameters (id, user_id, and product_id) in a single params object and sends them with a GET API request. Playwright automatically appends these parameters to the request URL. Send a GET API Request With Headers Headers play an important role in retrieving data from the server. They can be supplied in the GET request as shown below: TypeScript test("Get Order details API test with headers", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { id: 1, user_id: "1", }, headers: { ContentType: "application/json", }, }); expect(response.status()).toBe(200); }); This test sends a GET request with custom HTTP headers along with query parameters, where the headers option is used to specify that the request content type is JSON. Similarly, other headers such as “Authorization”, “Accept”, “User-Agent”, etc. can also be supplied. Send a GET API Request With a Timeout Option Playwright provides the timeout option that can be passed to the request.get() method for setting a timeout to limit how long to wait for the response. TypeScript test("Get order details API test with timeout", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { user_id: 1, }, headers: { ContentType: "application/json", }, timeout: 300, }); expect(response.status()).toBe(200); }); If the API does not respond within the given timeout, Playwright fails the request and throws a timeout error. It helps prevent tests from hanging and makes failures faster and more predictable, especially for slow or unstable APIs. Send a GET API Request With the failOnStatusCode Option The failOnStatusCode option tells Playwright to automatically fail the request if the API responds with a non-2xx status code (such as 400, 404, 500, etc). TypeScript test("Get order details API test with fail on status code", async ({ request, }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { user_id: "1", }, headers: { ContentType: "application/json", }, failOnStatusCode: true, }); }); Using this option, we can get rid of performing the checks using response.status() as Playwright throws an error immediately if the API does not respond with a 2xx status code. The failOnStatusCode option is useful when a request must succeed for the test to continue. For example, if we need to validate the response data, we must use this option to ensure that the API responds with a 2xx status code before proceeding with deeper response validation. Test Execution Let's execute all the tests that we discussed and also check the built-in report provided by Playwright. To run the tests, execute the following command from the terminal: Plain Text npx playwright test After the test execution is complete, the built-in Playwright report can be generated using the following command: Plain Text npx playwright show-report The report shows details of the test run, including test names, time taken, the browser agent used, and the number of tests executed, along with their pass/fail status. Watch the step-by-step YouTube tutorial on how to test GET API requests with Playwright TypeScript. Summary Testing GET API requests with Playwright using TypeScript allows you to easily send requests with query parameters and custom headers while keeping your tests clean and readable. Playwright also provides options such as timeout to control request duration and failOnStatusCode to automatically fail tests on non-successful responses. Together, these features help test the GET API requests efficiently.

By Faisal Khatri DZone Core CORE
Fetching Information Randomly From JSON Using Node, Nuxt, Express
Fetching Information Randomly From JSON Using Node, Nuxt, Express

Nuxt.js is a popular framework for Vue.js, and it is widely used for websites that require server-side rendering. It is similar to the Next.js framework for React.js. In this article, I’m going to share how you can fetch values randomly from a static JSON file with a Node and Express server. To make this example more realistic, we will store some words with their meanings in the words.json file in a static folder at the root. The necessary frameworks and libraries need to be installed on your machine, and basic knowledge is required: Node.js/ expressVue.js/Nuxt.js CLIJSONnpm (Source) #static/words.json JSON [ { "word": "lysis", "type": "noun", "meaning": "The resolution or favorable termination of a disease, coming on gradually and not marked by abrupt change." }, { "word": "outwit", "type": "verb", "meaning": "To surpass in wisdom, esp. in cunning; to defeat or overreach by superior craft." }, { "word": "completive", "type": "adjective", "meaning": "Making complete." } ] The words.json file above contains a few words, each with its type and meaning. Next, we need an Express server that listens for API calls from the front Nuxt/Vue page. #server.js JavaScript const path = require('path'); const express = require('express'); const cors = require('cors'); const fs = require('fs'); const app = express(); const PORT = 3001; app.use(cors()); let words = JSON.parse(fs.readFileSync('static/words.json', 'utf-8')); words = words.map(w => ({ ...w, type: w.type ? w.type.trim().toLowerCase() : '' })); app.get('/api/types', (req, res) => { const uniqueTypes = [...new Set(words.map(w => w.type))].sort(); res.json(uniqueTypes); }); //Random word generator with filters app.get('/api/random', (req, res) => { let filtered = [...words]; const { type, start, end, op, len, count } = req.query; const requestedType = type ? type.trim().toLowerCase() : ''; const startLetter = start ? start.trim().toLowerCase() : ''; const endLetter = end ? end.trim().toLowerCase() : ''; const wordLength = len ? parseInt(len) : null; const limit = parseInt(count) || 5; if (startLetter) { filtered = filtered.filter(w => w.word?.toLowerCase().startsWith(startLetter)); } if (endLetter) { filtered = filtered.filter(w => w.word?.toLowerCase().endsWith(endLetter)); } if (requestedType && requestedType !== 'all') { filtered = filtered.filter(w => w.type === requestedType); } if (op && wordLength) { if (op === '=') filtered = filtered.filter(w => w.word.length === wordLength); else if (op === '<') filtered = filtered.filter(w => w.word.length < wordLength); else if (op === '>') filtered = filtered.filter(w => w.word.length > wordLength); } const result = []; const available = [...filtered]; while (result.length < limit && available.length > 0) { const index = Math.floor(Math.random() * available.length); result.push(available.splice(index, 1)[0]); } res.json(result); }); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); }); As the Nuxt.js server runs on port 3000 by default, we have specified port number 3001. Next up is the Vue/nuxt.js code. With an input selection form and a “generate words” button. #pages/index.vue Vue.js Component <section class="card"> <div class="filters"> <div class="field"> <label>Number of Words</label> <input type="number" min="1" max="100" v-model.number="wordCount" /> </div> <div class="field"> <label>Word Type</label> <select v-model="wordType"> <option value="All">All</option> <option value="Noun">Noun</option> <option value="Verb">Verb</option> <option value="Adjective">Adjective</option> <option value="past participle">Past Participle</option> <option value="plural">Plural</option> <option value="preposition">Preposition</option> </select> </div> <div class="field"> <label>Starts With</label> <input type="text" maxlength="1" v-model="startLetter" /> </div> <div class="field"> <label>Ends With</label> <input type="text" maxlength="1" v-model="endLetter" /> </div> <div class="field"> <label>Word Length</label> <div class="length-filter"> <select v-model="lengthOperator"> <option value="">--</option> <option value="=">=</option> <option value="<"><</option> <option value=">">></option> </select> <input type="number" min="1" v-model.number="wordLength" /> </div> </div> <div class="action"> <button @click="getFilteredWords">Generate Words</button> </div> </div> </section> <section class="results"> <h2>Random Words List</h2> <div class="results-list"> <div v-show="!results.length" class="placeholder"> <p>Your generated words will appear here.</p> </div> <ul v-show="results.length"> <li v-for="(word, index) in results" :key="index" class="result-item"> <div class="word-card"> <strong class="word-title">{{ word.word }</strong> <small v-if="word.type" class="word-type">({{ word.type })</small> <p class="word-meaning">{{ word.meaning }</p> </div> </li> </ul> </div> </section> This is the normal HTML form that will be placed inside <template></temple>. This is the Vue.js variables section: data() { return { menuOpen: false, results: [], wordCount: 3, wordType: 'All', startLetter: '', endLetter: '', lengthOperator: '', wordLength: null }; }, Below is the code to send request to express server: async getFilteredWords() { const params = new URLSearchParams({ count: this.wordCount, type: this.wordType, start: this.startLetter, end: this.endLetter, op: this.lengthOperator, len: this.wordLength }); const res = await fetch(`http://localhost:3001/api/random?${params.toString()}`); this.results = await res.json(); this.$nextTick(() => { const resultsSection = document.querySelector('.results'); if (resultsSection) { resultsSection.classList.add('show'); resultsSection.classList.add('highlight'); setTimeout(() => { resultsSection.classList.remove('highlight'); }, 1500); } }); } And done. We have successfully set up the words.json file inside the static folder (static/words.json). Vue.js code inside pages/index.vue file. Express server code is inside the/server.js file. Run the project: To run the Nuxt server: “npm run dev.” To run the Express server: “node server.js.” Once these two commands are running in cmd, open a web browser and go to: http://localhost:3000/. Project Explanation Step by Step In this code, we have developed a random word finder from the words.json file, and we have shown randomly generated words to the users. In this code, we have used Vue.js/Nuxt.js for the front end and node/express server for the backend. Vue/Nuxt server is running on localhost:3000, and the Express server is running on localhost:3001. Step 1: Front-End With Vue.js Vue.js gathers the selected word options and sends an API request to the backend Express server running on port 3000. First, Vue.js binds all the user input options to params: JavaScript async getFilteredWords() { const params = new URLSearchParams({ count: this.wordCount, type: this.wordType, start: this.startLetter, end: this.endLetter, op: this.lengthOperator, len: this.wordLength }); Once bound, the information is sent to the backend with the following code. Step 2: The Backend Server With Express.js Server The backend API in the Express server is triggered with app.get(). First, the Express server fetches word information from the static words.json file and stores words in a filtered constant. Then processes the incoming information from the front-end and, as per the user's requirements, filters out words fetched from the words.json file. Once filtered, it sends words to the front end with res.json(). Step 3: Show Words to the Users In the Vue.js front end, we have used the async/await syntax. So, the following code line makes Vue.js wait until it gets a response from the Express.js server. Conclusion So, this is a simple full-stack code to pick information from the static JSON file randomly. In this article, Node.js is used for the back end to retrieve data randomly, and Vue.js is used for the front-end user interface. This looks like a few simple lines of code, but this code can be used in several educational and fun applications that process information randomly.

By Richard Davis
Part 2: Securing and Scaling Goose-to-Java Agent Traffic With agentgateway
Part 2: Securing and Scaling Goose-to-Java Agent Traffic With agentgateway

In Part 1 of this series, we built a Quarkus-based MCP tool server and connected it to the Goose AI agent over Streamable HTTP. The tools worked, the demo was clean, and everything ran on localhost. But the moment you imagine 50 developers running Goose on their laptops, all hitting the same set of backend MCP servers, the architecture starts to crack. Who authenticated that tool call? Which role authorized the getAuditTrail invocation? What stops a poisoned tool name from injecting payloads into your backend? This article answers those questions by placing agentgateway — the Linux Foundation's open-source proxy for agentic AI traffic — between Goose clients and the Quarkus MCP microservices we built in Part 1. The Problem: Direct Agent-to-Backend Connections Don't Scale When Goose (or any MCP client) connects directly to a backend MCP server, every tool call is a point-to-point trust relationship: This works for demos. It breaks in production for three reasons: No authentication. The MCP Streamable HTTP endpoint accepts any JSON-RPC call. There is no token verification, no session binding, and no identity propagation.No authorization. Every caller can invoke every tool. An intern running Goose has the same access as an SRE — getAuditTrail, getOrderStatus, everything.No guardrails. A compromised or misconfigured agent can send tool names containing prototype-pollution payloads (__proto__), path-traversal sequences (../), or CRLF-injected headers. The backend has to defend itself alone. The Solution: agentgateway as a Unified Control Plane agentgateway is a Rust-based proxy purpose-built for AI agent traffic. It understands the MCP protocol natively — it doesn't just forward HTTP; it parses JSON-RPC envelopes, manages MCP sessions, and applies policies at the tool-call level. Here is the architecture we're building: Goose connects to agentgateway on port 3000. agentgateway validates the JWT, checks the caller's roles against tool-level RBAC rules, passes the call through an ExtMCP guardrail server that sanitizes headers and blocks poisoning attempts, and only then forwards the clean request to the Quarkus backend on port 8080. Prerequisites You'll need everything from Part 1, plus: agentgateway binary (v1.4+): Shell curl -sL https://agentgateway.dev/install | bash Verify your Part 1 Quarkus MCP server still works: Shell cd part1-quarkus-mcp mvn quarkus:dev Then confirm the MCP endpoint responds: Shell curl -s http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}' | jq . Step 1: Deploy agentgateway Alongside the Quarkus MCP Server Create the agentgateway configuration at part2-agentgateway/agentgateway/config-dev.yaml. This development config proxies MCP traffic without requiring JWT, so you can validate the plumbing first: YAML # yaml-language-server: $schema=https://agentgateway.dev/schema/config mcp: port: 3000 policies: cors: allowOrigins: - "*" allowHeaders: - mcp-protocol-version - content-type - mcp-session-id exposeHeaders: - Mcp-Session-Id targets: - name: customer-tools mcp: host: http://localhost:8080/mcp Start agentgateway: YAML agentgateway -f part2-agentgateway/agentgateway/config-dev.yaml Now test the proxied MCP endpoint. Note that agentgateway returns SSE format (event: message\ndata: {...}), so we extract the JSON from the data: line: Shell curl -s http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}' \ | grep '^data: ' | sed 's/^data: //' | jq . You should see the same customer-tools server info as Part 1, but the traffic now flows through agentgateway. Open http://localhost:15000/ui to see the agentgateway admin UI with your MCP target listed. Step 2: Add JWT Authentication With the proxy working, let's lock it down. The mcpAuthentication policy implements the MCP Authorization specification — it validates JWT bearer tokens on every MCP request and supports OAuth 2.1 with PKCE for browser-based flows. Update the config to part2-agentgateway/agentgateway/config.yaml: YAML # yaml-language-server: $schema=https://agentgateway.dev/schema/config mcp: port: 3000 policies: cors: allowOrigins: - "*" allowHeaders: - mcp-protocol-version - content-type - mcp-session-id - authorization exposeHeaders: - Mcp-Session-Id mcpAuthentication: issuer: http://localhost:9000 audiences: - "http://localhost:3000/mcp" jwks: url: http://localhost:9000/.well-known/jwks.json resourceMetadata: resource: http://localhost:3000/mcp scopesSupported: - "mcp:tools:read" - "mcp:tools:execute" bearerMethodsSupported: - header targets: - name: customer-tools mcp: host: http://localhost:8080/mcp How It Works When a Goose client (or any MCP client) connects to http://localhost:3000/mcp: Discovery. The client fetches /.well-known/oauth-protected-resource from agentgateway and discovers it needs a bearer token with the mcp:tools:execute scope.Token acquisition. The client runs the OAuth 2.1 Authorization Code flow with PKCE against the issuer (http://localhost:9000), obtains an access token, and includes it as Authorization: Bearer <token> on subsequent MCP requests.Validation. agentgateway downloads the JWKS from the issuer, verifies the token signature, checks exp, iss, and aud claims, and extracts the sub and role claims for downstream authorization.Forwarding. Only after validation does agentgateway forward the JSON-RPC call to the Quarkus backend. Connecting to a Real OIDC Provider For production, replace the issuer and JWKS URL with your OIDC provider. Here is an example using Keycloak: YAML mcpAuthentication: issuer: https://keycloak.example.com/realms/mcp audiences: - "https://gateway.example.com/mcp" jwks: url: https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs provider: keycloak: {} agentgateway has built-in support for Keycloak, Auth0, Okta, Microsoft Entra ID, and other OIDC providers. Step 3: Configure Tool-Level RBAC With CEL Expressions JWT authentication tells you who is calling. MCP authorization tells you what they're allowed to do. agentgateway uses CEL (Common Expression Language) to define fine-grained, tool-level RBAC rules. Add the mcpAuthorization policy to your config: YAML mcpAuthorization: rules: # Operators can call any tool - 'has(jwt.roles) && "operator" in jwt.roles' # Viewers can only read status and health - > has(jwt.roles) && "viewer" in jwt.roles && mcp.tool.name in ["getCustomerStatus", "getZoneHealthLogs", "getSLACompliance"] # Auditors can access audit trail and SLA compliance - > has(jwt.roles) && "auditor" in jwt.roles && mcp.tool.name in ["getAuditTrail", "getSLACompliance"] How the Rules Work Each rule is a CEL expression that evaluates to true (allow) or false (deny). agentgateway evaluates them in order — the first match wins. RoleAllowed ToolsDenied ToolsoperatorAll five toolsNoneviewergetCustomerStatus, getZoneHealthLogs, getSLACompliancegetOrderStatus, getAuditTrailauditorgetAuditTrail, getSLACompliancegetCustomerStatus, getZoneHealthLogs, getOrderStatusNo roleNoneAll These aren't abstract labels — at Acme FinServ they map to real people and a real segregation-of-duties story: PersonaRoleWhy this scopeSofia — SRE, on-call for the platformoperatorNeeds to drive operational tools during incidents; full access is justified and logged.Acme Status Dashboard — an internal read-only serviceviewerShows customers and health at a glance; must never read getOrderStatus or getAuditTrail (PII/financial).Priya — external SOC 2 auditorauditorReviews the audit trail and SLA posture only. Giving her getCustomerStatus would violate least privilege — an auditor reading live customer data is itself a finding. The auditor scope is the one a SOC 2 assessor will scrutinize: it proves the audit function is separated from the operational function, and that access is granted by need, not convenience. agentgateway also auto-filters tools/list responses — if a viewer calls tools/list, they only see the three tools they're authorized to invoke. The agent never even learns that getAuditTrail exists. Available CEL Variables VariableDescriptionmcp.tool.nameThe tool being invoked (e.g., getCustomerStatus)mcp.tool.targetThe backend target name (e.g., customer-tools)jwt.subThe subject claim from the JWTjwt.rolesRole claims extracted from the JWThas(jwt.<claim>)Check whether a JWT claim exists Step 4: Prevent Tool Poisoning With ExtMCP Guardrails JWT and RBAC protect the identity layer. Guardrails protect the content layer. A valid, authenticated operator can still send a tool call with a poisoned name like getCustomerStatus/../../../etc/passwd or arguments containing <script> tags. The Quarkus backend's @Pattern annotations from Part 1 catch some of this, but defense in depth means filtering at the proxy too. agentgateway's ExtMCP guardrails intercept MCP method calls before they reach the backend, passing them through an external gRPC policy server that can inspect, mutate, or deny each call. Building the Guardrail Server With Quarkus gRPC Instead of relying on a third-party Docker image, we'll build our own ExtMCP guardrail server using Quarkus gRPC — keeping the entire stack in Java. The guardrail server lives in part2-agentgateway/extmcp-guardrail/ and implements the agentgateway ExtMCP protocol. First, the protobuf service definition (src/main/proto/extmcp.proto): ProtoBuf syntax = "proto3"; package agentgateway.dev.ext_mcp; option java_package = "com.example.guardrail.grpc"; import "google/protobuf/struct.proto"; service ExtMcp { rpc CheckRequest (McpRequest) returns (McpRequestResult); rpc CheckResponse (McpResponse) returns (McpResponseResult); } message McpRequest { repeated string service_names = 1; string method = 2; google.protobuf.Struct metadata_context = 3; optional bytes mcp_request = 4; repeated McpHeader headers = 5; } message McpRequestResult { oneof result { Pass pass = 1; bytes mutated = 2; AuthorizationError error = 3; } HeaderMutation header_mutation = 4; } message AuthorizationError { enum Code { UNKNOWN = 0; PERMISSION_DENIED = 1; RESOURCE_EXHAUSTED = 2; INVALID = 3; } Code code = 1; string reason = 2; optional bytes mcp_error = 3; } The Quarkus service implementation performs header sanitization and tool-poisoning detection: Java @GrpcService public class ExtMcpGuardrailService implements ExtMcp { private static final Pattern DANGEROUS_HEADER = Pattern.compile( "(?i)^(x-mcp-|x-forwarded-|x-real-ip)"); private static final List<String> BLOCKED_PATTERNS = List.of( "__proto__", "constructor", "../", "eval(", "exec(", "<script"); @Override public Uni<McpRequestResult> checkRequest(McpRequest request) { if (!"tools/call".equals(request.getMethod())) { return passRequest(); } // 1. Sanitize x-mcp-* headers for CRLF injection String headerError = sanitizeHeaders(request.getHeadersList()); if (headerError != null) { return denyRequest("header sanitization failed: " + headerError); } // 2. Check tool name and arguments for poisoning patterns if (request.hasMcpRequest()) { String poisonError = checkToolPoisoning( request.getMcpRequest().toStringUtf8()); if (poisonError != null) { return denyRequest("tool poisoning detected: " + poisonError); } } return passRequest(); } @Override public Uni<McpResponseResult> checkResponse(McpResponse response) { if (!"tools/list".equals(response.getMethod())) { return passResponse(); } // Append [guardrail-verified] marker to every tool description String original = response.getMcpResponse().toStringUtf8(); String mutated = original.replace("\"description\":\"", "\"description\":\"[guardrail-verified] "); return Uni.createFrom().item(McpResponseResult.newBuilder() .setMutated(ByteString.copyFrom(mutated, StandardCharsets.UTF_8)) .build()); } } Start the guardrail server on port 9001: Shell cd part2-agentgateway/extmcp-guardrail mvn quarkus:dev Configuring the Guardrail Policy Add the mcpGuardrails section to the agentgateway config: YAML mcpGuardrails: processors: - kind: remote host: "localhost:9001" failureMode: failClosed methods: tools/call: request tools/list: response The key settings: SettingValueWhyfailureModefailClosedIf the guardrail server is down, deny all tool calls rather than allowing unfiltered traffictools/call: requestPre-forwardInspect and sanitize before the call reaches the Quarkus backendtools/list: responsePost-forwardAnnotate or filter the tool list after the backend responds How Tool Poisoning Prevention Works When a tools/call request arrives, the guardrail flow is: Plain Text Goose → agentgateway → [JWT verified] → [RBAC checked] → → ExtMCP CheckRequest() → guardrail server inspects: 1. Scan x-mcp-* headers for CRLF injection 2. Validate header value lengths (≤ 256 bytes) 3. Check tool name for blocked patterns (__proto__, ../, eval()...) 4. Check tool arguments for injection payloads → Pass / Mutate / Deny → [if passed] → Quarkus MCP backend Sanitizing x-mcp-header Values The x-mcp-* headers carry protocol metadata between MCP clients and servers. A malicious client can inject CRLF sequences (\r\n) into these headers to smuggle additional HTTP headers or split responses. The guardrail server strips these by: Matching any header whose name starts with x-mcp-, x-forwarded-, or x-real-ipRejecting values that contain \r or \n charactersEnforcing a 256-byte maximum length on these header values Building a Custom Guardrail Server For production, implement the ExtMCP gRPC protocol with two methods: CheckRequest – Called before the tool call reaches the backend. Inspect the tool name, arguments, and headers. Return Pass, Mutate (rewrite params), or Deny with an AuthorizationError.CheckResponse – Called after the backend responds. Inspect the result. Return Pass, Mutate (redact sensitive data), or Deny. The part2-agentgateway/extmcp-guardrail/ directory contains the complete Quarkus gRPC implementation with the proto definition, the guardrail service, and the Maven build. Verifying the Guardrail With all three services running, test that the guardrail is active: Shell # Initialize session export MCP_SESSION_ID=$(curl -s -D - http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}' \ | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r') # Complete handshake curl -s http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' # List tools — descriptions should show the guardrail marker curl -s http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}' \ | grep '^data: ' | sed 's/^data: //' | jq '.result.tools[].description' Each tool description should start with the [guardrail-verified] marker, confirming that every tools/list response passes through the ExtMCP guardrail before reaching the client. Step 5: Configure Goose to Use agentgateway The final step is the simplest. In Part 1, Goose connected directly to the Quarkus backend: YAML # Part 1 — direct connection extensions: customer-tools: enabled: true type: http uri: http://localhost:8080/mcp headers: Content-Type: "application/json" For Part 2, change the URI to point to agentgateway: YAML # Part 2 — through agentgateway extensions: customer-tools: enabled: true type: http uri: http://localhost:3000/mcp headers: Content-Type: "application/json" Copy the updated config: YAML cp part2-agentgateway/goose-extension-config.yaml ~/.config/goose/config.yaml Now launch Goose and test the same prompts from Part 1: Plain Text Check customer status for CUST-4091 and verify health logs for their region The response is identical to Part 1, but the traffic now flows through agentgateway with JWT validation, RBAC enforcement, and guardrail inspection. You can verify this by checking the agentgateway UI at http://localhost:15000/ui — every tool call appears in the request log with its authentication status and policy decisions. The Complete Configuration Here is the full config.yaml combining all four security layers: YAML # yaml-language-server: $schema=https://agentgateway.dev/schema/config config: tracing: endpoint: http://localhost:4317 protocol: grpc sampling: parent: true default: 1.0 mcp: port: 3000 policies: cors: allowOrigins: - "*" allowHeaders: - mcp-protocol-version - content-type - mcp-session-id - authorization exposeHeaders: - Mcp-Session-Id mcpAuthentication: issuer: http://localhost:9000 audiences: - "http://localhost:3000/mcp" jwks: url: http://localhost:9000/.well-known/jwks.json resourceMetadata: resource: http://localhost:3000/mcp scopesSupported: - "mcp:tools:read" - "mcp:tools:execute" bearerMethodsSupported: - header mcpAuthorization: rules: - 'has(jwt.roles) && "operator" in jwt.roles' - > has(jwt.roles) && "viewer" in jwt.roles && mcp.tool.name in ["getCustomerStatus", "getZoneHealthLogs", "getSLACompliance"] - > has(jwt.roles) && "auditor" in jwt.roles && mcp.tool.name in ["getAuditTrail", "getSLACompliance"] mcpGuardrails: processors: - kind: remote host: "localhost:9001" failureMode: failClosed methods: tools/call: request tools/list: response targets: - name: customer-tools mcp: host: http://localhost:8080/mcp Bonus: Interactive Security Console The demo includes a browser-based SPA (index.html) that lets you visualize the entire security flow without touching the command line. The start-all.sh script serves it automatically on port 8888. Open http://localhost:8888/index.html and you'll see an enterprise-style console with: Config selector – switch between three tiered configs to see how each maps to a real deployment stage:Live stat tiles – session status, request count, tools discovered, and security checks passed/deniedAnimated architecture diagram – watch MCP requests flow from Goose through agentgateway's security layers to the Quarkus backend in real timeConfig-aware security layers – JWT, RBAC, and ExtMCP layers animate as "checking → passed" when enabled in the selected config, or appear as "skipped" with a badge when not configured ConfigUse CaseSecurity Layersconfig-dev.yamlLocal development — pure proxy pass-through for rapid iteration without security overheadNoneconfig-guardrails.yamlStaging / shared environments — blocks tool poisoning and header injection before requests reach the backendExtMCPconfig.yamlProduction deployment — full security stack with JWT identity verification, role-based tool access via CEL, and input sanitizationJWT + RBAC + ExtMCP The four demo steps — Initialize, List Tools, Call Tool, and Poison Test — make real MCP requests through agentgateway and display the JSON-RPC responses. Switching configs lets you demonstrate the difference: with config-dev.yaml, the poison test passes through unblocked; with config-guardrails.yaml, the ExtMCP guardrail catches and denies it; with config.yaml, every request also passes through JWT authentication and RBAC authorization before reaching the guardrail layer. What We Achieved Starting from the unprotected Quarkus MCP server in Part 1, we added four security layers without changing a single line of the backend Java code: LayerWhat It Doesagentgateway FeatureAuthenticationVerifies caller identity via JWT/OAuth 2.1 with PKCEmcpAuthenticationAuthorizationEnforces tool-level RBAC per rolemcpAuthorization with CELInput sanitizationBlocks tool poisoning and header injectionmcpGuardrails (ExtMCP)ObservabilityTraces every tool call through the proxyOpenTelemetry integration The Quarkus MCP backend remains a clean, focused tool server. All governance concerns live in the agentgateway configuration and the Quarkus gRPC guardrail service — keeping the entire stack in Java, exactly where platform engineers expect to find them. What's Next: Part 3 In Part 3: End-to-End Tracing and Observability Across Goose, agentgateway, and Quarkus, we'll wire up distributed tracing across the full agent traffic path. You'll see how a single Goose prompt generates a trace that spans the agent, the gateway, and the Quarkus backend — with tool-call latency, RBAC decisions, and guardrail verdicts all visible in a single Jaeger or Grafana Tempo timeline. We'll configure OpenTelemetry exporters in all three components and build a Grafana dashboard that gives platform teams real-time visibility into their agentic infrastructure. Stay tuned.

By Daniel Oh DZone Core CORE
Dynamic Arrays, Spill, and LET: What Changed in Excel and Why It Matters for Java Applications
Dynamic Arrays, Spill, and LET: What Changed in Excel and Why It Matters for Java Applications

In a previous article, Working with Spreadsheets in Java: A Practical Overview, we walked through the common scenarios where Java applications need to interact with spreadsheets and the categories of tools available for the job. One of the factors mentioned there was support for modern Excel formulas — a topic that deserves more space than a single bullet point. Java applications interact with Excel more often than most teams plan for: file uploads from finance, calculation logic authored in a workbook, reporting exports back to business users. The files these users produce today are not the same as the files they produced five years ago. Excel 365 and Excel 2021 introduced a new formula model, and workbooks authored in those versions routinely use it. Depending on which library you use, those formulas may evaluate correctly, fail silently with stale cached values, or throw exceptions at recalculation time. This article goes deeper on that topic: what dynamic arrays and spill behavior are, what the new function set looks like, why supporting them is technically difficult, and what Java developers should look for when evaluating whether a library handles them correctly. Why You're Seeing Them in Real-World Workbooks Dynamic arrays spread rapidly because they eliminate many of the helper columns, copied formulas, and Ctrl+Shift+Enter array formulas that older Excel workbooks depended on. Workbooks become shorter, easier to audit, and easier to maintain. As organizations migrate to Microsoft 365, these newer formulas increasingly appear in spreadsheets exchanged with Java applications, even when the application itself hasn't changed. What Changed: One Formula, Many Values Before dynamic arrays, formulas that returned multiple values generally required a pre-sized array range and legacy array-formula syntax. Dynamic arrays changed this by allowing a single formula to return a variable-sized array and automatically spill into neighboring cells. For example: =UNIQUE(A1:A6) Entered in one cell, this returns the full list of distinct values from A1:A6. The result fills as many cells as there are distinct values. If the source data changes and the number of unique values changes, the spill range automatically grows or shrinks. This is not just a new function. It is a change in the evaluation model itself. The Mechanics of Spill A spilled formula produces a region of cells with specific roles: The anchor cell is the one cell that contains the formula. It "owns" the result.The spilled cells are the neighboring cells that display the additional values. They do not contain formulas of their own; they mirror slices of the anchor's result. You can reference the entire spilled range from another formula using the # operator. The # reference is not a fixed cell range such as A1:A6; it refers to whatever range the anchor currently spills into. If A1 contains =UNIQUE(...) and the result spills into A1:A6, then =COUNTA(A1#) counts the values in the entire spilled range. If the spill range grows or shrinks, the # reference adjusts automatically. #SPILL! errors. If the range a formula needs to spill into is blocked by an existing value, a merged region, or an Excel Table, the formula cannot spill, and the anchor cell shows #SPILL! instead of a result. Clearing the obstruction allows the formula to complete. Implicit intersection with @. Older Excel silently reduced arrays to single values in many contexts. Modern Excel returns the full array unless the formula uses the @ prefix. For example, =A1:A10 entered in a cell in modern Excel spills the values from A1:A10, while =@A1:A10 applies implicit intersection and returns the value corresponding to the formula's row. Files migrated from older Excel versions often contain automatically inserted @ prefixes to preserve their original behavior. The New Function Family The modern functions commonly associated with Excel's dynamic-array model can be grouped into three broad categories. It is worth understanding the grouping because the groups behave differently. Group 1: Language Features These are not really functions in the traditional sense. They add expression-level constructs to Excel's formula language. LET binds names to intermediate values inside a formula, so you can write =LET(total, SUM(B2:B100), tax, total*0.1, total+tax) instead of repeating SUM(B2:B100) three times.LAMBDA defines a reusable function inside a workbook. Combined with named ranges, LAMBDA effectively adds user-defined functions without VBA.ISOMITTED is used inside LAMBDA to detect whether an optional argument was supplied. These functions do not inherently produce a spilled array. LET returns the result of its calculation, which may itself be an array. Group 2: Dynamic Array Functions These are the functions people usually mean when they talk about "the new Excel functions." These functions are designed to return arrays, and when their results contain multiple values, Excel can spill those results into neighboring cells. UNIQUE returns distinct values from a range.SORT and SORTBY return sorted arrays.FILTER returns rows that match a condition.SEQUENCE generates a sequence of numbers.RANDARRAY generates an array of random numbers. Array-shaping functions form a subset of this group. They take arrays as input and return reshaped arrays: CHOOSECOLS, CHOOSEROWS, DROP, EXPAND, HSTACK, VSTACK, TAKE, TOCOL, TOROW, WRAPCOLS, WRAPROWS. TEXTSPLIT also fits here — it splits a string into an array. Other functions, including BYROW, BYCOL, MAP, REDUCE, and SCAN, build on the same dynamic array model. Group 3: Scalar Functions Added in the Same Era Dynamic arrays are primarily an evaluation model; modern functions are a collection of functions that take advantage of, or coexist with, that model. Group 3 functions were introduced as part of the broader set of modern Excel functions, but they are not themselves primarily array-producing functions. XLOOKUP and XMATCH are modern replacements for VLOOKUP and MATCH. They normally return a single value, though they can return an array when passed an array of lookup values.TEXTAFTER and TEXTBEFORE return substrings.VALUETOTEXT and ARRAYTOTEXT convert values to text (ARRAYTOTEXT takes an array as input but returns a single string). These are often lumped in with dynamic array functions because they arrived together, but their evaluation model is closer to VLOOKUP than to UNIQUE. Why This Is Hard for a Formula Engine Supporting these features is not just a matter of adding new function names to a list. The dynamic array model requires substantial changes to the evaluation engine itself. A traditional one-cell-at-a-time formula model is not sufficient to implement dynamic arrays. An engine must be able to represent a formula whose result has a variable shape and propagate that result across multiple cells. A modern engine has to handle four additional concerns: Array-shaped results. A formula's return value may be a 2D array whose dimensions depend on the input data. =UNIQUE(A1:A100) returns a different number of rows depending on how many unique values the range contains. The engine must determine the result shape at evaluation time, not at parse time. Spill range tracking. The engine must reserve the cells the formula spills into and prevent other content from occupying them. When something occupies a spill target, the anchor must return #SPILL! rather than overwrite the obstruction. The reserved region must also update when the shape of the result changes. Downstream references. Expressions like A1# refer to the entire spilled range. When the shape of the anchor formula changes, every downstream reference must be re-evaluated with the new dimensions. This makes the dependency graph more dynamic than in a one-value-per-cell model. Implicit intersection compatibility. Older Excel silently collapsed arrays to single values in many contexts. Modern Excel returns the whole array. When files authored in older Excel are opened in modern Excel, @ prefixes are inserted automatically to preserve original behavior. An engine that reads modern .xlsx files needs to honor the @ operator, or the imported formulas will produce different results. Adding these behaviors to an engine designed around the one-formula-one-value model is a substantial rewrite, not an incremental feature addition. This is part of why support across the Java ecosystem has been uneven. What Java Developers Should Check Support for these capabilities varies significantly across Java spreadsheet libraries. Some engines were originally designed around traditional one-cell-one-result evaluation and only implement subsets of the modern Excel model. Others have extended or redesigned their evaluators to support dynamic arrays. Rather than relying on feature lists, it is worth validating behavior against workbooks representative of your own application. If your application needs to evaluate modern Excel formulas, the following checks are worth running before committing to a library. Test with a file containing a spilled formula. Create a small .xlsx with =UNIQUE(A1:A100) or =SORT(A1:A100) in a cell. Load it in your candidate library and try to recalculate the anchor cell. A library that supports dynamic arrays will return the array; one that does not will typically throw an exception or return only the first value. Check for the # spill operator. In the same file, add another cell containing =COUNTA(A1#) where A1 is the anchor. This tests whether the library understands spilled range references, which is a separate capability from evaluating the anchor formula itself. Test the @ operator. Add =@A1:A10 in a cell and check whether the library correctly returns the value at the current row rather than the full array. Files migrated from older Excel routinely contain @ prefixes; a library that doesn't handle them will produce different results than Excel. Test with LET and LAMBDA. Write a formula like =LET(total, SUM(A1:A100), total * 1.1) and check both evaluation and .xlsx round-trip. Test LET and LAMBDA independently. Parsing, preserving, and evaluating these functions are separate capabilities, so a library that can read or write the formula text may not necessarily be able to evaluate it correctly. Test round-trip. Save the workbook, reopen it in Excel, and check that the formulas still produce correct results. Some engines strip modern constructs on save. Check what happens on failure. When a library encounters a function it does not implement, does it raise an exception, return an error value, or silently fall back to the cached value from the file? Silent fallback is the most dangerous behavior because it masks the problem during development and only fails in production when the data changes. Conclusion Excel's formula language has changed more in the last few years than in the two decades before it. Dynamic arrays, spill behavior, and the new function set are not experimental — they are standard in Excel 365 and Excel 2021, and they show up in workbooks that Java applications routinely have to process. For Java developers, the practical implication is that "Excel formula support" is no longer a single property that a library either has or doesn't have. There are several distinct capabilities involved, and libraries vary widely on each. As covered in the previous article, the Java spreadsheet landscape spans open source libraries such as Apache POI, commercial headless engines, and embedded spreadsheet components like Keikai. Whichever category fits your use case, the checks above are a reasonable way to verify that a candidate library handles modern Excel behavior against the workbooks your real users produce.

By Hawk Chen DZone Core CORE
RavenDB Launches Quill to Bring Production AI Agents to Enterprise SQL Systems, No Migration Required
RavenDB Launches Quill to Bring Production AI Agents to Enterprise SQL Systems, No Migration Required

Hadera, Israel, September 8th, 2026, TechnologyWire This article was provided by TechnologyWire and does not represent the editorial content of DZone. RavenDB, a NoSQL document database used by more than 12,000 customers, announced today the launch of its new product, Quill, a context layer for SQL databases that makes them ready for production AI agents without migrating the system of record or architecting a custom AI stack. With AI becoming a board-level mandate, CTOs and VPs of engineering are under pressure to ship AI capabilities fast. But for organizations whose mission-critical data sits in legacy SQL systems, built years before embeddings or agents existed, AI can’t access their data. Modernizing or replacing the systems is expensive, risky, and time-consuming. By the time the system is updated, nobody remembers what the project was supposed to achieve or how it measured ROI. Recently, a Gartner survey of infrastructure and operations leaders found that one in five AI initiatives fail, and only 28% report a positive ROI, which is linked to how well the technology is integrated, governed, and aligned with operational needs, not to the sophistication of the model. As AI becomes the industry standard, organizations have been left without a clear path to deliver until now. "Anyone can stand up an AI demo in an afternoon, but getting that demo into production with data pipelines, semantic search, security, governance, all the plumbing a small proof of concept doesn't need until it has to run at scale, is the hard part," said Oren Eini, founder and CEO of RavenDB. "Quill exists because we'd rather hand teams that plumbing already assembled than watch them rebuild the same project after project. You get access to the live data you need, decide the scope on day one, and change it as you go, instead of building everything from scratch." Quill connects directly to an organization's existing SQL database and adds a context layer on top, making it possible to launch production-ready agents in weeks rather than the 18 to 24 months of a typical in-house build. The source system stays exactly where it is and remains authoritative, and the full AI stack- search, retrieval, and agents that can answer questions- is included. Agents built on Quill support web chat, WhatsApp, Telegram, Slack, and Discord out of the box. "With Quill, the plumbing was already there, so we spent our time building the actual feature," said Hagay Albo, CEO at Albos Technologies and Holdings, an early adopter of Quill. By default, Quill is governed, sitting between the AI and the source system, and it is built on the assumption that the model itself cannot be trusted with unrestricted access, so organizations decide exactly what an agent can and cannot see, independent of the source database's own permissions. In a healthcare setting, for example, an agent can answer a patient's question about an upcoming appointment, while prescription data is never part of the dataset it can query. What is usually a custom security project becomes a configuration choice. Quill is also model-agnostic, so teams can use any AI model, switch providers, or run entirely on their own hardware. Quill is now available for organizations running PostgreSQL, SQL Server, or MySQL, with more databases to be supported in the future, and can be deployed in the cloud or on-premises to meet data-residency or regulatory requirements. To start using Quill today, visit: https://ravendb.net/quill About RavenDB: RavenDB is a hybrid NoSQL document database built for modern application development. Used by more than 12,000 customers across 50 industries, RavenDB helps teams move faster with seamless data management across cloud, on-prem, and edge environments. With full-text search, automatic indexes, and an easy-to-use studio for monitoring and administration, RavenDB is the database developers love and enterprises trust. Learn more at www.ravendb.net

By Technology Wire
Memory-First Indexes in SQL Server 2025: Redefining Performance for Hybrid Workloads
Memory-First Indexes in SQL Server 2025: Redefining Performance for Hybrid Workloads

Modern database environments rarely run a single type of workload. Most production systems handle both transactional operations and analytical queries simultaneously. These mixed workloads, often referred to as hybrid workloads, place significant pressure on traditional database indexing and storage strategies. In such environments, disk-based indexes can become a performance bottleneck. When transactional and analytical queries compete for disk I/O, it often results in increased latency, reduced throughput, and inconsistent query performance. To address these challenges, SQL Server leverages memory-optimized tables and indexes as part of its In-Memory OLTP capabilities. These features reduce reliance on disk I/O by enabling data and index access directly from memory, while still maintaining durability through logging and checkpoint mechanisms. This article explores how memory-optimized indexing works and demonstrates how it can significantly improve performance in real-world hybrid workload scenarios. Core Characteristics Mandatory inclusion: Every memory-optimized table must have at least one index, as they serve as the "entry points" for row access.Purely in-memory: Indexes are rebuilt entirely from scratch during database recovery based on their definitions and the data loaded into memory.Non-persistent: Unlike traditional indexes, changes to these indexes are not written to the transaction log, reducing I/O overhead.Fragmentation-free: These structures do not suffer from traditional page fragmentation, eliminating the need for regular REORGANIZE or REBUILD operations. Index TypeBest Use CaseBehaviorHash IndexEquality SearchesUses an array of buckets; highly efficient for point lookups (e.g., WHERE ID = 5).Nonclustered IndexRange QueriesUses a lock-free B-tree structure (Bw-tree); ideal for range scans and sorted results (e.g., WHERE Price > 100). The Challenge With Traditional Indexing Traditionally, database indexes are stored on disk to ensure durability. While this design protects data, it introduces a major limitation: disk I/O latency. In environments with heavy workloads, disk access becomes a bottleneck. This is particularly noticeable when: Large analytical queries scan index rangesTransactional queries require fast point lookupsMany concurrent users access the system When both workloads run together, index operations often compete for disk resources, resulting in slower queries and higher latency. Introducing Memory-First Indexes Memory-First Indexes in SQL Server 2025 take a different approach. Instead of relying primarily on disk-based indexes, the system prioritizes in-memory index access for frequently used data while maintaining a synchronized copy on disk for durability. The key idea is simple: Hot data (frequently accessed index ranges) is kept in memory.Cold data remains on disk.Changes made in memory are synchronized with disk replicas in the background. This approach allows SQL Server to serve many queries directly from memory while still maintaining persistence. The feature also includes monitoring mechanisms that track query patterns. When the system detects frequently accessed index partitions, it moves them into memory automatically. Less frequently accessed portions are pushed back to disk to conserve memory resources. The result is faster query execution without requiring manual tuning from database administrators. Real-World Example: Retail E-Commerce Database To understand the benefits, consider a retail company running an e-commerce platform. The company stores millions of products in a table with the following structure: ProductID – unique identifierProductCategory – category of the productPrice – product priceStockQuantity – available inventory The application runs two types of queries. Transactional Query This query checks stock availability for a specific product. SQL SELECT StockQuantity FROM Products WHERE ProductID = 102345; Analytical Query This query calculates aggregated metrics by product category. SQL SELECT ProductCategory, AVG(Price) AS AvgPrice, SUM(StockQuantity) AS TotalStock FROM Products WHERE Price > 500 GROUP BY ProductCategory; In a traditional setup, both queries rely on disk-based indexes. When concurrency increases, disk access becomes saturated, and query performance suffers. With Memory-First Indexes, the most frequently used index ranges, such as ProductID and ProductCategory, are loaded into memory, allowing much faster lookups. Testing the Feature To evaluate the impact of Memory-First Indexes, we can simulate a large dataset and compare query performance before and after enabling the feature. Step 1: Create the Table SQL CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductCategory NVARCHAR(50), Price DECIMAL(10,2), StockQuantity INT ); Step 2: Populate Test Data The following script generates a large dataset for testing. SQL INSERT INTO Products (ProductID, ProductCategory, Price, StockQuantity) SELECT TOP 50000000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ProductID, CASE WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 5 = 1 THEN 'Electronics' WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 5 = 2 THEN 'Clothing' ELSE 'Home Appliances' END AS ProductCategory, ABS(CHECKSUM(NEWID()) % 1000) + 1.00 AS Price, ABS(CHECKSUM(NEWID()) % 5000) + 1 AS StockQuantity FROM sys.all_objects a CROSS JOIN sys.all_objects b; Step 3: Create Traditional Indexes SQL CREATE INDEX IX_Products_ProductID ON Products (ProductID); CREATE INDEX IX_Products_Category ON Products (ProductCategory); At this stage, run the transactional and analytical queries and capture baseline metrics using Query Store or dynamic management views. Step 4: Enable Memory-First Indexes Next, recreate the indexes with Memory-First enabled. SQL DROP INDEX IX_Products_ProductID ON Products; CREATE INDEX IX_Products_ProductID ON Products (ProductID) WITH (MEMORY_FIRST = ON); DROP INDEX IX_Products_Category ON Products; CREATE INDEX IX_Products_Category ON Products (ProductCategory) WITH (MEMORY_FIRST = ON); Step 5: Execute Test Queries SQL SELECT StockQuantity FROM Products WHERE ProductID = 102345; MS SQL SELECT ProductCategory, AVG(Price) AS AvgPrice, SUM(StockQuantity) AS TotalStock FROM Products WHERE Price > 500 GROUP BY ProductCategory; Record execution time, CPU usage, and disk activity again. Observed Performance Improvements The results typically show noticeable performance gains. For example: Transactional queries Before: ~50 msAfter: ~15 ms Analytical queries Execution time reduced by about 50% System metrics also reveal additional improvements: Disk I/O reduced by more than 70%Memory usage increased only moderatelyCPU utilization became more stable during peak workloads These improvements occur because queries are able to retrieve indexed data directly from memory rather than waiting for disk operations. Why This Matters for Modern Workloads Hybrid workloads are becoming the norm across many industries, including retail, finance, and IoT platforms. Systems must support both real-time transactions and large analytical queries without sacrificing performance. Memory-First Indexes help address this challenge by: Reducing disk I/O bottlenecksImproving response time for critical queriesAutomatically adapting to changing workload patternsMaintaining durability with synchronized disk replicas Final Thoughts Memory-First Indexes represent an important improvement in SQL Server 2025’s indexing architecture. By prioritizing in-memory access for frequently used data, SQL Server can deliver significantly faster query performance while still preserving data durability. For organizations running mixed transactional and analytical workloads, this feature can reduce latency, improve system stability, and make better use of available hardware resources. As hybrid workloads continue to grow, features like Memory-First Indexing will play a key role in helping database platforms keep up with modern application demands.

By arvind toorpu DZone Core CORE
How to Design a Multi-Agent AI Framework in Python for Enterprise LLM Workflows
How to Design a Multi-Agent AI Framework in Python for Enterprise LLM Workflows

When I first started building enterprise applications with Large Language Models (LLMs), I fell into a trap that almost every developer encounters. I thought that scaling an AI system simply meant refining a single, massive prompt. I wrote complex system instructions, packed the context window with rules, and expected a single stateless API call to act as a researcher, analyst, and copywriter all at once. In production, this monolithic approach failed repeatedly. When processing dynamic data streams, the model flattened nuanced details, skipped critical execution steps, and regularly generated highly confident hallucinations. Through these failures, I realized the core problem: we are expecting a single inference step to manage an entire engineering workflow. To build predictable, production-grade software, I had to redesign my architecture. I moved away from monolithic prompts and began decoupling complex tasks into role-based, multi-agent frameworks in Python. My Breaking Point: The Competitive Intelligence Engine Failure Problem The necessity of this architectural shift became clear to me during a deployment for an enterprise technology firm. My team was tasked with building a competitive intelligence engine to track daily competitor product launches, analyze changing pricing sheets, and generate technical battlecards for our global sales team. My first iteration used a single, closed-source model wrapper. The prompt instructed the LLM to read raw HTML fragments from target URLs, extract feature updates, compare them against our internal capabilities matrix, and output a structured battlecard. During local testing with a few static URLs, it worked well. But when I went live against a shifting market, the system kept breaking without much notice: The Production Vulnerabilities I Encountered Context flattening: When parsing multiple long competitor pricing tiers, the model routinely dropped nuanced constraints, such as specific seat-count thresholds. It simply averaged out the data. Severe information loss: Instead of extracting the live web data provided in the context window, the model slipped back into its static pre-training data, hallucinating older features that the competitor had deprecated months prior. Prose without substance: Because the model had to handle data extraction, comparative reasoning, and copy editing simultaneously, it prioritized linguistic fluency over technical depth. The output looked like excellent marketing prose, but it was factually useless to our sales engineers. To fix this, I completely dismantled the monolithic prompt. I decoupled the system into three distinct programmatic agents, creating a clear engineering pipeline: Step 1: Establishing a Model-Agnostic Execution Boundary When I design multi-agent systems, my first rule is that agents must be decoupled from specific model providers. A production agent should depend on a stable, programmatic interface. This approach allows me to swap a cloud API like OpenAI for a local, open-weights model running via Ollama without changing a single line of business logic. Here is the standardized execution node I developed for this framework: Python import os from openai import OpenAI # I initialize the client container using environment boundaries client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) def execute_agent_inference(messages: list, target_model: str = "gpt-4o-mini") -> str: """ Provides a standardized execution node for all upstream agents to communicate with the designated model endpoint. """ response = client.chat.completions.create( model=target_model, messages=messages, temperature=0.1 # Low temperature enforces deterministic reasoning ) return response.choices[0].message.content Step 2: The Strategist Agent (Task Decomposition) The execution loop begins with the Strategist Agent. I isolated this node to handle a single cognitive task: ingestion and planning. Its sole job is to break down a broad user request into a chronological sequence of distinct tasks. Python def strategist_agent(user_objective: str) -> list: """ Ingests a broad objective and returns a structured execution plan. """ system_prompt = """ You are a project strategist. Your job is to break down a broad research objective into an ordered, numbered list of specific, non-overlapping data requirements. Do not summarize the topic. Output only the numbered steps. """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Objective: {user_objective}"}, ] raw_plan = execute_agent_inference(messages) # Parse the numbered rows into a clean Python list return [line.strip() for line in raw_plan.split("\n") if line.strip()] By forcing the system to map out its roadmap before running any resource-heavy tasks, I ensure the application maintains a strict operational scope. Step 3: Integrating External Tools With the Extraction Agent An agent is only as good as the data it consumes. I designed the Extraction Agent to never guess or extrapolate. Instead, I equip it with specific Python functions that fetch live, real-world data before it runs an inference cycle. Here, I define a simulated web search utility and an internal vector store look-up tool: Python def fetch_live_web_data(query: str) -> str: """ Simulates a live web lookup via external search providers like Tavily or SerpAPI. """ return f"[Live Web Match] Found current market documentation regarding: {query}" def query_internal_vector_store(query: str) -> str: """ Simulates a vector database query for internal technical specifications. """ return f"[Vector DB Match] Internal baseline spec data for: {query}" def extraction_agent(allocated_task: str, running_context: str) -> str: """ Gathers factual data using external retrieval tools before forming response notes. """ # Execute the tools first to ground the agent's context in real data web_insights = fetch_live_web_data(allocated_task) internal_insights = query_internal_vector_store(allocated_task) system_prompt = """ You are a data extraction agent. Your job is to analyze tool outputs and compile precise, evidence-dense technical notes. Strictly ground your response in the provided tool outputs. Do not extrapolate. """ user_payload = f""" Current Task: {allocated_task} Prior Context: {running_context} Tool Outputs: - {web_insights} - {internal_insights} """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_payload}, ] return execute_agent_inference(messages) Step 4: The Technical Reviewer Agent (Synthesis and Audit) The final step in my pipeline is the Technical Reviewer Agent. I do not use this agent as a passive text formatter. Instead, I design it to act as an internal critic that actively checks the gathered research for missing technical data. Python def technical_reviewer_agent(compiled_research_notes: str) -> str: """ Audits research materials and synthesizes a structured final technical report. """ system_prompt = """ You are a technical reviewer. Synthesize a clean report from the provided research notes. CRITICAL RULES: 1. Organize your output using clear Markdown headings and bullet points. 2. Do not introduce general knowledge or unverified claims. 3. If the data contains gaps, note them explicitly instead of smoothing over them. """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Research Notes:\n{compiled_research_notes}"} ] return execute_agent_inference(messages) Step 5: Constructing the Orchestration Loop With all my agents built, I put them together using a central orchestrator function. This loop manages the execution sequence, updates the running memory context between steps, and passes state across agent boundaries. Python def run_intelligence_engine(target_topic: str) -> str: """ Coordinates the execution sequence, updates persistent memory boundaries, and returns the finalized asset. """ print(f"[*] Initializing Strategy Phase for: {target_topic}") execution_steps = strategist_agent(target_topic) accumulated_notes = [] persistent_memory = "" for idx, step in enumerate(execution_steps, 1): print(f"[>] Executing Phase {idx}: {step[:50]}...") # Pass the running context so the agent knows what has been researched so far step_output = extraction_agent(step, persistent_memory) accumulated_notes.append(step_output) # Update the persistent memory to prevent duplicate work in later steps persistent_memory += f"\n[Completed Phase {idx} Info]: {step_output}\n" print("[*] Compiling and Reviewing Final Deliverable...") final_report = technical_reviewer_agent("\n".join(accumulated_notes)) return final_report if __name__ == "__main__": report_output = run_intelligence_engine( "Analyze competitor pricing models for cloud infrastructure shifts" ) print("\n--- Final Report Output ---\n") print(report_output) Resolving the Hidden Challenge: Information Degradation When I first launched a framework like this, I noticed a subtle engineering issue: the information handoff problem. When multiple agents pass unstructured text back and forth, the data risks losing clarity at each step. If the Strategist designs broad steps, the Extractor returns summarized notes, and the Reviewer formats them aggressively, the final output loses its technical precision. To keep your multi-agent networks at their best in production, I recommend implementing these two programmatic practices: 1. Maintain Strict Structural Memory Controls Never pass a raw conversational history across agent boundaries. Instead, require your extraction nodes to return explicit, structured technical updates (such as clear key-value maps or clean markdown bullet records). This approach preserves specific variables like precise pricing values or hardware specs, all the way to the final synthesis step. 2. Implement Automated Validation Gates Do not use an LLM to check if its own output is correct. Instead, place deterministic variable Python validation gates between agent handoffs. I write small programmatic checks to verify that the text matches a required schema, meets minimum character counts, or contains key terms extracted from the retrieval tools before letting the pipeline proceed. Measurable Production Outcomes Transitioning our enterprise tracking engines from monolithic prompt templates to this decoupled multi-agent architecture delivered immediate, verifiable improvements across our core operational metrics: Drastic reduction in hallucination rates: By isolating the extraction agent and grounding its context entirely in live tool calls, our documented hallucination rate fell from 7.2% to less than 0.2%.System traceability: When an output degrades, my team and I no longer dig through thousands of lines of a single prompt history. We simply look at the independent logs of each agent to find exactly where the data chain broke, reducing our Mean Time to Resolution (MTTR) from hours to minutes.Operational maintainability: I can update, optimize, or replace individual components, such as updating a web scraping API or refining the Reviewer's styling guide, without breaking or re-testing the rest of the application ecosystem. Conclusion The true test of an enterprise AI application is not how well it runs a basic query on a local development machine. Real success is defined by how reliably the application handles messy, dynamic data in production over time. By separating monolithic prompts into a coordinated pipeline of role-based agents, I turned unpredictable model outputs into stable, dependable software infrastructure. A perfect framework lies in distributing cognitive responsibility, creating clear interfaces, and engineering strict control boundaries around your models. Thank you for reading. Designing multi-agent AI systems for enterprise LLM workflows goes beyond calling powerful models; it requires thoughtful system design, coordination between agents, strong observability, and scalable architecture that can operate reliably in production.

By Chidiebere Njoku
Why I Don't Want an LLM Generating Java Business Logic
Why I Don't Want an LLM Generating Java Business Logic

A pull request arrives. A few hundred lines of Java implementing the new discount rule: tiered thresholds, a regional exception, something about loyalty tiers that nobody can quite explain. It compiles. The tests pass. An LLM wrote it in about forty seconds. Now: who reviews it? The person who owns that rule is in commercial operations. She knows exactly which customers should get the discount and why the regional exception exists, and she cannot read Java. The person who can read Java has no idea whether the thresholds are right. He will check that the code looks reasonable, because that is the only thing he is equipped to check. So the review that happens is not the review that matters. That is the problem I keep coming back to, and it has nothing to do with how good the model is. This Is Not an Argument About Whether the Model Is Good Enough Most objections to generated code are about competence. The model hallucinates an API. It gets an edge case wrong. It writes something that works on the happy path and falls over in production. I find these arguments unconvincing because they expire. Models get better. Any position resting on today's error rate is a position with a shelf life, and people who staked one out three years ago have mostly had to retreat from it. The durable question is different. It is not how well the model writes. It is what the thing it writes is permitted to say. A model that never makes a mistake, handed Java, can still emit Runtime.getRuntime().exec(...). Not because it is malicious or confused — because that sentence is available in the language it was asked to write. Competence and authority are separate axes, and improving the first does nothing to the second. "Write it in Java" Is a Much Bigger Grant Than Anyone Means Consider what you actually authorize when you ask for a discount rule in Java. You authorize file system access. Network sockets. Reflection. Thread creation. Process execution. Every class on the classpath, including the ones that talk to your database, your payment provider, and your secrets manager. You authorize the loading of new code at runtime. Nobody intends to grant any of this. It arrives free with the language, the way a house key also opens the shed. The task needed perhaps six operations — look up an order, total it, check a customer's tier, apply a discount, log the decision, approve or refuse — and the language you handed over contains everything Java contains. That gap, between the authority the task requires and the authority the language confers, is the whole of it. It exists whether or not the model is trustworthy. It exists whether or not anyone acts on it. It is just very large, and it is not visible in the pull request. The Usual Guardrails Are Denial Lists The standard responses all share a shape. Tell the model in the prompt not to touch the file system. Review the generated code. Run static analysis and flag dangerous calls. Run it in a sandbox with a restricted security policy. Every one of these asks you to enumerate what must not happen, over a space of things that can happen which is effectively unbounded. You are writing a deny-list against a general-purpose language. You have to think of exec. Then of reflection reaching exec. Then of the dependency that shells out on your behalf. Then of the next one. We learned this lesson in security a long time ago and reached a settled answer: allow-lists beat deny-lists, because the allow-list is finite and you wrote it. Somehow, when the subject is generated code, we reach for the deny-list again. Shrink the Language, Not the Model The alternative is to stop constraining a powerful language and instead supply a small one. Give the model a vocabulary that contains exactly the operations the domain has — the six from earlier, say — and nothing else. Not a restricted Java. A different, much smaller language, whose entire vocabulary is a list your team wrote in advance, in Java, on purpose. Generated business logic then looks like this: Python PROGRAM ApproveOrder(orderId INTEGER, limit DECIMAL) RETURNS BOOLEAN DECLARE purchase Order DECLARE total DECIMAL purchase = LOAD_ORDER(orderId) total = ORDER_TOTAL(purchase) IF total > limit THEN REJECT purchase, "over limit" RETURN FALSE END IF APPROVE purchase RETURN TRUE END. LOAD_ORDER, ORDER_TOTAL, REJECT and APPROVE are not part of the language. They are Java classes somebody decided to expose. Order is a Java object the program can hold and pass and never look inside — there is no purchase.customer.account.balance here, only the operations the domain chose to have. Two things change, and the second matters more than the first. The obvious one: dangerous programs are no longer forbidden, they are inexpressible. If the model emits DELETE_ALL_ORDERS, nothing rejects it on policy grounds. The name means nothing. The program does not compile, for the same reason a typo does not compile. There is no deny-list because there is nothing to deny. The less obvious one: the commercial operations manager can read the program above. She can tell you whether the threshold is right, whether the rejection reason is the one the contract requires, whether an approval should have been logged. The review moves to the person who owns the rule. That is the review that was missing at the start of this article, and no amount of static analysis over generated Java produces it. A small language buys something else, quietly. With no data structures, one global scope, no null, and a compiler that refuses to run a program that reads a variable before it is set, entire families of subtle wrongness have nowhere to live. Not caught — absent. What It Costs, and What It Does Not Buy I would not trust this argument from someone who only listed the advantages, so here are the bills. You have to design the vocabulary. Somebody sits down and decides that the domain has ORDER_TOTAL and CUSTOMER_RISK and not forty other things. That is real work, done before the first generated line, by someone who understands the domain. And if nobody on your team can write that list, this approach will not help you. It will only show you that the list does not exist. That is worth finding out, but it is not a pleasant morning. Complex algorithms stay in Java. Business rules are algorithms too, and they belong in the small language; that is the point. But route optimization, a scoring model, anything with real computational substance belongs behind a function the small language calls. The signal is usually that you want to build up a data structure, or that you want a helper you can call from three places. Both mean you have wandered out of business logic and should walk back. The boundary bounds naming, not doing. This is the limit people miss, and overstating it is how the idea gets dismissed. A function you expose can do anything Java can do. RUN_SHELL_COMMAND is a perfectly registrable operation. The vocabulary is only as narrow as the operations you chose, and choosing them badly gets you exactly the exposure you were avoiding. There are no resource limits yet. A generated program can still loop forever. This one is a gap rather than a decision: the interpreter walks the program one statement at a time, so a step budget or a deadline is a small addition rather than a redesign, and it will go in when somebody needs it. Until then, untrusted input needs the same containment any untrusted workload needs. What you get is narrower than "safe" and more useful than it sounds: the set of things a generated program can name is finite, written down, and reviewable by a human before anything is generated at all. When I Would Still Write Java If the thing is genuinely computational, write Java. If it is a one-off that will be deleted next week, use whatever is nearest — Java, Python, a shell script — and let the model write it; do not build a vocabulary for something with a life expectancy of days. If the rules change so fast that the vocabulary would be obsolete before it settled, the overhead will not pay for itself. And if your business logic is already reviewed by people who can read it, understand it, and are accountable for it being right — you may not have the problem this solves. Plenty of teams do not. But if you are about to let a model write business rules in Java, ask the question I started with, because the answer is usually uncomfortable. Somebody is going to approve that pull request. Are they the person who knows whether the rule is correct? If not, the language is too big. I have been building a small language along these lines: BUBAS, an orchestration language for subject-matter experts, embedded in Java. The example above is real BUBAS. The idea does not require my implementation, though — the argument is about the size of the language you hand over, and you can shrink yours however you like.

By Peter Verhas DZone Core CORE

Monthly Top Languages Experts

expert thumbnail

Alvin Lee

Founder,
Out of the Box Development, LLC

Full-stack developer and technology consultant specializing in web architectures, microservices, and API integrations.

The Latest Languages Topics

article thumbnail
Testing Business Programs Without Constructing Domain Objects
How can you test business rules and business-level orchestration without domain objects? Why is making domain objects for the business rules opaque a good restriction?
September 25, 2026
by Peter Verhas DZone Core CORE
· 297 Views
article thumbnail
How to Verify Response Data in API Testing With Playwright TypeScript
Learn how to verify the response data, including structure checks, basic validations, and more, in API Testing with Playwright TypeScript
September 25, 2026
by Faisal Khatri DZone Core CORE
· 487 Views
article thumbnail
Integrating LLMs into iOS Applications With Swift Using ONNX Runtime
This article explores how to integrate an ONNX transformer model into an iOS application using Swift. It covers loading and running models with ONNX Runtime.
September 24, 2026
by Kagan Girgin
· 705 Views
article thumbnail
6 Techniques To Reduce LLM API Costs With the Python Library
Six techniques to cut LLM API costs by up to 90%: prompt caching, model routing, batch processing, and more. (Includes a pip-installable Python library.)
September 23, 2026
by Somnath Banerjee
· 1,556 Views · 1 Like
article thumbnail
How to Build an AI Agent to Generate Selenium WebDriver Tests in Java: A Practical Guide for Test Automation Engineers
A step-by-step guide using OpenAI and Ollama to build an AI agent that generates Selenium Java tests from plain-English scenarios.
September 23, 2026
by Faisal Khatri DZone Core CORE
· 1,344 Views
article thumbnail
Valkey: Bringing Key-Value Databases to Enterprise Java
Valkey provides high-performance key-value storage for Enterprise Java, with portable APIs and flexible persistence through Eclipse JNoSQL.
September 22, 2026
by Otavio Santana DZone Core CORE
· 1,682 Views · 1 Like
article thumbnail
Exploring the DBMS_DEVELOPER Package: JSON Metadata for Oracle Objects
Learn how Oracle Database 23ai’s DBMS_DEVELOPER package uses JSON metadata and ETags to simplify database schema documentation, migration, and change tracking.
September 18, 2026
by arvind toorpu DZone Core CORE
· 2,095 Views
article thumbnail
Stop Blaming Executor Memory: The Real Reasons Your Spark Jobs Are Slow
This article explains five common causes of slow Spark jobs and practical fixes for joins, stragglers, decryption chains, shuffle partitions, and incremental processing.
September 18, 2026
by Swaminathan Sethuraman
· 1,993 Views · 1 Like
article thumbnail
dbt Meets Apache Flink: One Workflow for Data Engineers
dbt meets Apache Flink: one SQL workflow for data engineers across Snowflake, BigQuery, Databricks, and real-time streaming pipelines on Confluent Cloud.
September 15, 2026
by Kai Wähner DZone Core CORE
· 1,763 Views
article thumbnail
Porting GPU Drivers to Rust on ARM64: The Hardest Trial for Kernel-Level Computing
Rust in the Linux kernel is gaining traction, but GPU drivers are the real test, where complex memory, synchronization, and recovery paths make C bugs costly.
September 14, 2026
by Aakash Chaudhary
· 1,673 Views · 1 Like
article thumbnail
How to Perform Response Verification in REST-Assured Java for API Testing: Part 2
Master REST-Assured response verification in Java with Hamcrest Matchers, JSON assertions, API validations, and real-world examples.
September 11, 2026
by Faisal Khatri DZone Core CORE
· 2,808 Views · 3 Likes
article thumbnail
When TypeScript Types Meet Untrusted AI: Building Type-Safe LLM Pipelines With Runtime Validation
Learn in this article how to treat LLM output as unknown until runtime schema validation proves it safe for typed application logic.
September 11, 2026
by Bhanu Sekhar Guttikonda DZone Core CORE
· 2,935 Views · 1 Like
article thumbnail
Replacing JSON With Protobuf in Your Microservice Mesh: A Zero-Downtime Migration Blueprint
JSON hurts at scale. Protobuf cuts payload size by ~72%, reduces CPU overhead, and enforces typed contracts. However, it needs careful schema management.
September 11, 2026
by Bansidhar kadiya
· 2,910 Views · 2 Likes
article thumbnail
How to Correctly Implement ‘Sneaky Throws’ in Java
A straightforward, lightweight, and useful approach to dealing with methods that throw checked exceptions with code snippets.
September 10, 2026
by Horatiu Dan DZone Core CORE
· 2,801 Views · 4 Likes
article thumbnail
Angular Apps Don’t Need Another Chatbot: Building Agentic UI Workflows With TypeScript
Build agentic Angular UIs with typed events, Signals, explicit capabilities, human approval, and controlled rendering using AG-UI, A2UI, and WebMCP.
September 10, 2026
by Bhanu Sekhar Guttikonda DZone Core CORE
· 2,877 Views · 1 Like
article thumbnail
How to Test GET API Requests With Playwright TypeScript
Learn how to test GET API requests using Playwright with TypeScript, including params, headers, timeouts, and status code validation.
September 10, 2026
by Faisal Khatri DZone Core CORE
· 2,687 Views · 4 Likes
article thumbnail
Fetching Information Randomly From JSON Using Node, Nuxt, Express
Nuxt.js, Node.js and Express code to filter out data based on user requirement and select data randomly from the filtered data.
September 10, 2026
by Richard Davis
· 3,036 Views · 1 Like
article thumbnail
Part 2: Securing and Scaling Goose-to-Java Agent Traffic With agentgateway
Deploy agentgateway as a security proxy between Goose AI agents and Quarkus MCP servers to enforce JWT auth, RBAC, and tool-poisoning guardrails.
September 9, 2026
by Daniel Oh DZone Core CORE
· 3,406 Views · 1 Like
article thumbnail
Dynamic Arrays, Spill, and LET: What Changed in Excel and Why It Matters for Java Applications
Modern Excel formulas change how spreadsheets work. See what Java developers need to know when choosing a spreadsheet library.
September 9, 2026
by Hawk Chen DZone Core CORE
· 3,321 Views · 2 Likes
article thumbnail
RavenDB Launches Quill to Bring Production AI Agents to Enterprise SQL Systems, No Migration Required
The new context layer connects to existing SQL databases and builds a governed, model-agnostic foundation for AI agents running on live operational data, in weeks rather than years.
September 9, 2026
by Technology Wire
· 2,651 Views · 1 Like
  • 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
×
Advertisement
Advertisement