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

Events

View Events Video Library

JavaScript

JavaScript (JS) is an object-oriented programming language that allows engineers to produce and implement complex features within web browsers. JavaScript is popular because of its versatility and is preferred as the primary choice unless a specific function is needed. In this Zone, we provide resources that cover popular JS frameworks, server applications, supported data types, and other useful topics for a front-end engineer.

icon
Latest Premium Content
Trend Report
Low-Code Development
Low-Code Development
Refcard #363
JavaScript Test Automation Frameworks
JavaScript Test Automation Frameworks
Refcard #288
Getting Started With Low-Code Development
Getting Started With Low-Code Development

DZone's Featured JavaScript Resources

The Code-Volume Delusion: Rethinking Engineering Velocity in the AI Era

The Code-Volume Delusion: Rethinking Engineering Velocity in the AI Era

By Rupesh Dabbir
Let's be honest about what happens when you give an entire engineering team AI coding assistants. You look at the sprint board, and tickets are moving to "In Review" faster than ever. Your developers are happy. They are writing boilerplate in seconds and generating entire component structures before their morning coffee gets cold. If you measure productivity by the sheer volume of code generated, your team has successfully turned into a factory. But then you look at your deployment frequency. It's flat. Depending on the week, it might actually be trending downward. How can a team be writing code twice as fast, but shipping to production at the exact same speed? Image 1: The AI Productivity Illusion The problem is that output increases, but throughput/deployments remains static or drops. The challenge is how to address this. The answer lies in basic systems engineering. If you optimize a step in a process that isn't the primary bottleneck, your overall throughput doesn't change. We just spent the last two years making typing faster. But typing was never the hardest part of software development. As engineering leaders, we are flying blind if we rely on legacy productivity metrics in an AI-assisted world. The bottleneck hasn't disappeared; it has simply shifted downstream. To actually measure and manage engineering velocity today, you need to abandon "lines of code" and start tracking the new friction points. Here are the three metrics you need to start watching immediately. 1. PR Cycle Time (and the "Rubber-Stamp" Ratio) AI tools are incredible at generating massive blocks of code. They are decidedly not incredible at explaining the architectural reasoning behind why they generated it. When a developer uses an LLM to build a feature, they often submit a massive Pull Request. For the author, it took ten minutes. For the senior engineer assigned to review it, it's a nightmare. Reviewing 500 lines of AI-generated code in a GitHub diff requires significantly more cognitive load than reviewing human-written code, because AI code often lacks a recognizable, human train of thought. The immediate result? Your PRs sit in the queue for days. What to track: Time to First Review / Time to Merge: If this metric is spiking while ticket completion is dropping, your bottleneck is purely code review. You need to enforce strict PR size limits. AI or not, a PR should rarely exceed 300-400 lines of logic. The Rubber-Stamp Ratio: Look for massive PRs that are approved in under five minutes with a simple "LGTM." This means your senior engineers are overwhelmed and are just pushing AI code through without reading it. That is a ticking time bomb for production outages. 2. CI/CD Pipeline Stability and P95 Build Times More code means more tests. AI is perfectly happy to generate fifty unit tests for a single utility function. On the surface, high test coverage looks great to upper management. But if those tests are poorly constructed, overly coupled to implementation details, or reliant on flaky assertions (especially in UI testing), your CI/CD pipeline is going to grind to a halt. We’ve all seen it: a developer merges a feature, and the build fails three times in a row because of a flaky, auto-generated E2E test that didn't properly await a DOM element. What to track: Build Failure Rate (specifically on the main branch): If your developers are moving fast but your pipeline is constantly turning red, your velocity is zero. P95 Build Duration: Is your pipeline taking 45 minutes to run because it's executing thousands of low-value, AI-generated tests? You might need to implement a "Build Gardener" rotation—assigning an engineer each sprint to aggressively prune useless tests, update assertions, and optimize pipeline caching. 3. The Code Churn Rate and Architectural Decay Think of an AI coding assistant as a highly enthusiastic, incredibly fast junior developer. It solves the immediate problem right in front of it. It does not look at the holistic architecture of your application and decide to refactor a base class for long-term scalability. If developers lean too heavily on AI for problem-solving, you end up with massive amounts of duplicated logic and hyper-localized fixes. The code works today, but the architecture rots tomorrow. What to track: Code Churn (Percentage of code rewritten within 3-4 weeks of being merged): High churn in an AI environment usually means developers are using trial-and-error via prompts rather than thinking through the system design. They merge a feature, realize it breaks an edge case, and have the AI write a messy patch the very next week. Tech Debt Ratio vs. Feature Delivery: You have to force developers to slow down and do architectural planning before they open their IDE. If you don't track the time spent paying down debt, the sheer volume of AI-generated code will eventually bankrupt your architecture. The Takeaway AI coding assistants are a massive net positive for the industry. But they are a hyper-efficient engine, and an engine is dangerously useless if you don't upgrade your brakes and steering at the same time. Stop looking at how fast your developers are closing tickets. Start looking at the health of your review culture, the speed of your pipelines, and the stability of your architecture. That is where the real engineering management happens today. More
Stop Hand-Rolling Chat UIs: Streaming LLM Tokens Into React Native Without the Jank

Stop Hand-Rolling Chat UIs: Streaming LLM Tokens Into React Native Without the Jank

By Tammo Ronke
A chat screen looks like a weekend project: a list of bubbles and a text input pinned to the bottom. In React Native, it is one of the hardest things to ship well, because it sits on top of the two most hostile surfaces in mobile development: the software keyboard and a scrolling list that changes size while you're looking at it. We're putting LLMs into everything now, and there is still no good drop-in chat view for React Native. You glue together an aging library with strong opinions, or you hand-roll it. I hand-rolled it. Then I made the LLM stream its replies token by token, and the whole thing fell apart in a way that took a week to understand. This is the story of that break, and the fix, which arrived with suspicious good timing as a library release three months ago. The App I work on an app built around an LLM chat: characters that remember you and reply as an open-ended story unfolds. The messages between the reader and the characters are rendered in a chat-like view: an inverted list, the newest message at the bottom, and a composer pinned above the keyboard. Standard chat anatomy. The twist that makes it hard: the character replies are generated by an LLM, and they stream. Tokens arrive in bursts, a few every hundred milliseconds, with a full reply landing over two or three seconds. Each batch makes the last bubble taller. The list isn't just appending a finished message. It's growing on every frame, while the user might be typing, scrolling, or dismissing the keyboard. That single fact is what turns "I'll just use a FlatList" into weeks of work. Why There's Nothing Good to Reach For The first thing I did was look for a library. The honest state of the art: react-native-gifted-chat is the default answer and it's showing its age. It's opinionated about your data shape, its rendering, and its layout, and fighting those opinions costs more than writing your own.Most "chat UI" packages are really just a styled `FlatList` plus a text input. They solve the easy half and hand you the two genuinely hard problems: keyboard choreography and a live-resizing list.The keyboard utilities that _do_ exist (`KeyboardAvoidingView` and friends) were built for forms, not for an inverted list whose last row is growing while the keyboard animates. So I wrote my own keyboard-and-scroll layer. It was close to 500 lines of KeyboardAvoidingView overrides, manual scrollToOffset calls, listeners on keyboard show/hide events, and offset math to keep the composer glued to the keyboard. It worked, demos looked clean, and I shipped it. The Break: Streaming Meets the Keyboard The bug reports were all variations on "the chat is jumpy." No crashes, just jank. I couldn't reproduce it at first because each of the two features behaved perfectly on its own. The keyboard animation was smooth. The streaming was smooth. The problem only showed up at their intersection. That's the kind of bug that costs a week, because nothing is actually broken. Two correct things are simply disagreeing. Here's what was actually happening. While a character reply streams in: Every batch of tokens makes the last bubble taller.On an inverted list, growing the bottom row shifts the content offset.React Native re-runs the layout to absorb the new height.If the keyboard is open, or worse, mid-animation, my keyboard layer is _also_ adjusting offsets at the same time. Two systems are writing to the scroll position on the same frames. The result: the content jumps, the composer twitches, and if the user has scrolled up to re-read an earlier message, the stream yanks them around. Layout thrash. A steady 60fps collapsed into the low teens precisely when the app is supposed to feel most alive, and on a mid-range Android phone, it was worse. TypeScript // The naive streaming append: looks innocent, thrashes layout. // Every chunk triggers a re-measure of the growing bubble, // which fights whatever the keyboard handler is doing this frame. for await (const chunk of stream) { setMessages((prev) => { const next = [...prev]; next[0] = { ...next[0], text: next[0].text + chunk }; // index 0 = newest, inverted list return next; }); } The streaming itself has its own sharp edges, and they compound the layout problem. Two worth calling out before the fix: React Native's fetch can't stream a response body. There's no response.body.getReader() in stock RN. You reach for an SSE polyfill like react-native-sse or if you're on Expo like me, the streaming-capable fetch from expo/fetch. Pick deliberately. This is the single most common thing people get wrong on day one. TypeScript import { fetch } from "expo/fetch"; const res = await fetch(url, { method: "POST", body, signal: controller.signal, }); const reader = res.body.getReader(); const decoder = new TextDecoder(); // ...read loop, parse SSE frames, dispatch tokens Partial markdown will bite you. Tokens arrive mid-syntax. At some frame, your buffer is literally The dragon turned and **stared with the bold marker opened and not yet closed. A naive markdown renderer will either render the asterisks as literal text or flip half the conversation bold. You need a renderer that tolerates unterminated syntax, or you sanitize the buffer before each render. Cancellation has to be real. The user closes the chat, switches characters, or fires off a new message mid-reply. You need an AbortController whose signal actually reaches the fetch. Skip it and you're billed for tokens nobody will read, streamed into a view that already unmounted. The Fix I was about to rewrite my keyboard layer for the fourth time when react-native-keyboard-controller shipped KeyboardChatScrollView in v1.21.0, on March 16, 2026. It is, as far as I can tell, the first component built specifically for the chat-plus-keyboard problem rather than the form-plus-keyboard one, and it happens to solve the streaming case directly. The piece that matters for an LLM app is built on a ClippingScrollView that provides cross-platform contentInset behavior by extending the scrollable geometry rather than recomputing the layout. That one design choice is why the thrash disappears. The keyboard no longer fights the list because absorbing keyboard height is no longer a layout operation. The props read like a tour of every chat app you've used: keyboardLiftBehavior picks how the content reacts to the keyboard. "always" keeps the latest messages visible no matter where you've scrolled (Telegram, WhatsApp). "whenAtEnd" lifts only when you're already at the bottom, and leaves you alone if you've scrolled up to read history (ChatGPT). "persistent" lifts when the keyboard opens and, unlike the rest, stays put when it closes instead of snapping back down (Claude). "never" lets the keyboard cover the content and moves nothing (Perplexity).blankSpace reserves room for an incoming response while absorbing keyboard height. This is the direct antidote to streaming jank. Instead of the list growing reactively frame by frame and fighting the keyboard, you reserve the space up front and let the tokens fill it.extraContentPadding handles a composer that grows as the user types a long message, without jumping the content.freeze locks the layout during emoji and attachment-picker transitions, the other place chat UIs jump. TypeScript import { KeyboardChatScrollView } from "react-native-keyboard-controller"; <KeyboardChatScrollView keyboardLiftBehavior="persistent" // the Claude pattern: lifts on open, stays put on close blankSpace={pendingReply ? estimatedReplyHeight : 0} > {messages.map(renderBubble)} </KeyboardChatScrollView>; On paper whenAtEnd is the tidy answer for a reading-heavy app: don't move the content out from under someone studying an old exchange. I shipped persistent anyway. So many of my users live in assistant apps that Claude's settle-and-stay behavior is just what their hands expect, and familiarity beat theory. Nobody had to relearn how the chat feels. My streaming loop didn't change. What changed is that the loop is now the only thing touching layout while a reply comes in. The keyboard handler stepped out of the fight. The composer stopped twitching. The user who scrolls up to re-read an old exchange stays put while the character keeps talking below the fold. What I'd Keep, and What I'd Throw Away If I were starting Y/N's chat today, I'd delete my hand-rolled keyboard layer without ceremony and start from KeyboardChatScrollView. The custom code I'd keep is the part that was always mine to own: the streaming reader, the partial-markdown guard, and the cancellation plumbing. Those aren't keyboard problems, and no layout library will solve them for you. The general lesson applies well beyond chat. The expensive bug is almost never one broken feature. It's two correct features interacting on the same frame. My keyboard handler was right. My streaming was right. The week disappeared into the seam between them. When something janks and every part tests clean in isolation, stop testing the parts and go look at what they're both writing to. And the smaller, practical one: the chat box is never the easy part of the app. Budget for it like it's a feature, because it is one. For the first time in a while, you don't have to build all of it yourself. If you've solved the Android side of this, or made partial-markdown rendering feel good while streaming, I'd be glad to compare notes in the comments. More
Text Analysis Without a Backend: Replacing an LLM Call With Intl.Segmenter and 60 Lines of JavaScript
Text Analysis Without a Backend: Replacing an LLM Call With Intl.Segmenter and 60 Lines of JavaScript
By Kevin Brown
A Framework-Agnostic Approach to SSR for Microfrontends
A Framework-Agnostic Approach to SSR for Microfrontends
By Vitaly Zheltko
How We Cut PyFlink Pipeline p99 Latency from 3-5 Seconds to ~500ms
How We Cut PyFlink Pipeline p99 Latency from 3-5 Seconds to ~500ms
By Arjun Shah
Refresh Token Rotation in Node.js: Stopping Token Theft Without Logging Users Out
Refresh Token Rotation in Node.js: Stopping Token Theft Without Logging Users Out

JWT-based authentication is simple to start with and surprisingly hard to get right. The naive setup of a long-lived access token stored in the browser is a security liability. The textbook fixes short-lived access tokens plus a refresh token — introduce their own problem: What Happens When a Refresh Token Is Stolen? This article walks through implementing refresh token rotation with reuse detection, a pattern that limits the damage of a stolen token while keeping legitimate users logged in. All examples are in Node.js with Express. Why a Single Long-Lived Token Is Dangerous If you issue one access token that lives for days, you have no way to revoke it before it expires. If it leaks through an XSS bug, a logging mistake, or a compromised device, an attacker has full access until expiry, and you cannot do anything about it. Short-lived access tokens (say, 15 minutes) limit this window. But you cannot ask users to log in every 15 minutes, so you pair the access tokens with a longer-lived refresh token whose only job is to mint new access tokens. The New Problem: Stolen Refresh Tokens A refresh token is now the crown jewel. If an attacker steals it, they can mint access tokens indefinitely. Simply making it long-lived recreates the original problem at a higher level. Rotation addresses this: every time a refresh token is used, it is invalidated, and a brand-new refresh token is issued. A stolen token is only useful until the legitimate user next refreshes, at which point the stolen token becomes invalid. But rotation alone is not enough. Consider the race: Attacker steals refresh token R1.Legitimate user refreshes with R1, gets R2. R1 is now invalid.Attacker tries R1. It is rejected, but the system does not yet know a theft occurred. The missing piece is reuse detection: if an already-rotated token is presented again, that is a strong signal of theft, and the entire token family should be revoked. Implementing Token Families The key concept is the token family. When a user logs in, you create a family with a shared family_id. Every rotation issues a new token in the same family. If any consumed token in the family is ever presented again, you revoke the whole family, forcing the attacker (and the victim) to re-authenticate. Here is the schema: SQL CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), family_id UUID NOT NULL, user_id INTEGER NOT NULL, token_hash VARCHAR(255) NOT NULL, consumed BOOLEAN NOT NULL DEFAULT false, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_family ON refresh_tokens (family_id); Note that we store a hash of the token, never the token itself. If your database leaks, the stored hashes are useless to an attacker, the same reasoning behind hashing passwords. Issuing Tokens at Login TypeScript const crypto = require('crypto'); const jwt = require('jsonwebtoken'); function hashToken(token) { return crypto.createHash('sha256').update(token).digest('hex'); } async function issueTokens(userId, familyId = crypto.randomUUID()) { const accessToken = jwt.sign( { sub: userId }, process.env.ACCESS_SECRET, { expiresIn: '15m' } ); const refreshToken = crypto.randomBytes(40).toString('hex'); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days await pool.query( `INSERT INTO refresh_tokens (family_id, user_id, token_hash, expires_at) VALUES ($1, $2, $3, $4)`, [familyId, userId, hashToken(refreshToken), expiresAt] ); return { accessToken, refreshToken, familyId }; } The Rotation Endpoint With Reuse Detection This is where the security logic lives: TypeScript async function rotateRefreshToken(presentedToken) { const hash = hashToken(presentedToken); const result = await pool.query( `SELECT * FROM refresh_tokens WHERE token_hash = $1`, [hash] ); if (result.rowCount === 0) { throw new Error('INVALID_TOKEN'); } const token = result.rows[0]; // REUSE DETECTION: a consumed token is being presented again. // This means the token was either stolen or replayed. Burn the family. if (token.consumed) { await pool.query( `DELETE FROM refresh_tokens WHERE family_id = $1`, [token.family_id] ); throw new Error('TOKEN_REUSE_DETECTED'); } if (new Date(token.expires_at) < new Date()) { throw new Error('EXPIRED_TOKEN'); } // Mark this token consumed, then issue a fresh one in the same family. await pool.query( `UPDATE refresh_tokens SET consumed = true WHERE id = $1`, [token.id] ); return issueTokens(token.user_id, token.family_id); } The crucial branch is the token consumed check. Under normal operation, a token is used exactly once and then never seen again. If a consumed token reappears, the only explanations are theft or a replay attack, so the system revokes every token in the family. The attacker is locked out, and the legitimate user is forced to log in again, which is the correct, safe outcome. A Common Mistake: Forgetting the Grace Window There is a subtle real-world wrinkle. Mobile clients on flaky networks sometimes fire two refresh requests for the same token because the first response was lost in transit. With strict reuse detection, the second request looks like an attack and nukes the family, logging out a user who did nothing wrong. The pragmatic fix is a short grace window: if a consumed token is reused within a few seconds of being consumed, return the already-issued replacement token instead of revoking the family. This tolerates network retries without weakening protection against real theft, which happens on a much longer timescale. TypeScript const GRACE_MS = 10_000; if (token.consumed) { const age = Date.now() - new Date(token.consumed_at).getTime(); if (age < GRACE_MS) { // Likely a network retry — return the existing replacement. return getReplacementToken(token.family_id); } // Otherwise, treat as theft. await revokeFamily(token.family_id); throw new Error('TOKEN_REUSE_DETECTED'); } (This requires adding a consumed_at timestamp and a pointer to the replacement token, omitted here for brevity.) Takeaways Keep access tokens short-lived (15 minutes is reasonable) and never rely on them being revocable. Rotate refresh tokens on every use so a stolen token has a short useful life.Detect reuse of consumed tokens and revoke the entire token family — this is what actually catches theft.Store only hashes of refresh tokens, never the raw values.Add a small grace window so flaky-network retries do not get misread as attacks. Rotation with reuse detection is more code than a plain JWT setup, but it turns authentication from "hope nothing leaks" into a system that actively detects and contains compromise.

By Bilal Azam
React 19 Killed Half My Performance Optimization Code, and I'm Grateful
React 19 Killed Half My Performance Optimization Code, and I'm Grateful

I maintain a React admin dashboard codebase that had — at last count before upgrading to React 19 — 34 instances of useMemo, 28 instances of useCallback, and 19 components wrapped in memo(). I spent a nontrivial amount of time over two years adding those optimizations, debugging cases where I'd gotten the dependency arrays wrong, and explaining to junior developers why the table re-rendered on every keystroke. React 19 with the compiler deleted most of that work. Here's what actually changed and what still matters. The Compiler Handles What You Used to Do Manually The React Compiler analyzes component render behavior and adds memoization where it's beneficial automatically. You don't specify it. You don't maintain dependency arrays. You don't wrap components in memo(). Before: JSX const Dashboard = memo(function Dashboard({ userId, filters }) { const processedData = useMemo( () => processData(rawData, filters), [rawData, filters] ); const handleFilterChange = useCallback( (newFilter) => updateFilters(newFilter), [updateFilters] ); return <DataTable data={processedData} onFilter={handleFilterChange} />; }); After React 19 with compiler: JSX function Dashboard({ userId, filters }) { const processedData = processData(rawData, filters); const handleFilterChange = (newFilter) => updateFilters(newFilter); return <DataTable data={processedData} onFilter={handleFilterChange} />; } Same performance. Half the code. Zero dependency array bugs. I removed 31 of my 34 useMemo calls after upgrading. The three I kept are genuinely complex computations where I want explicit control. Everything else the compiler handles better than I was doing manually. The One Thing That Still Kills Dashboard Performance The compiler doesn't solve virtualization. If your data table renders 5,000 DOM nodes because your dataset has 5,000 rows — React 19 won't fix that. The browser is still creating and painting 5,000 nodes. Any table with more than 100 rows needs virtualization: JSX import { useVirtualizer } from '@tanstack/react-virtual'; export default function DataTable({ rows }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 52, overscan: 5 }); return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }> <div style={{ height: virtualizer.getTotalSize() }> {virtualizer.getVirtualItems().map(row => ( <div key={row.index} style={{ position: 'absolute', top: 0, transform: `translateY(${row.start}px)`, height: row.size } > <TableRow data={rows[row.index]} /> </div> ))} </div> </div> ); } 5,000 rows. 20 DOM nodes in the viewport at any time. Scroll is smooth. The React Compiler cannot help you here — this is a DOM problem, not a React problem. Optimistic Updates Changed How My Dashboard Feels The biggest perceived performance improvement in React 19 for admin dashboards isn't the compiler. It's useOptimistic. Admin dashboards involve constant small mutations — toggling user status, updating values, changing settings. Before React 19, every mutation waited for the server response before updating the UI. Fast servers meant 200ms delays. Slow servers meant users clicking buttons twice because nothing happened visually. JSX 'use client'; import { useOptimistic, useTransition } from 'react'; function UserStatusBadge({ user, onUpdateStatus }) { const [optimisticStatus, setOptimisticStatus] = useOptimistic( user.status, (_, newStatus) => newStatus ); const [isPending, startTransition] = useTransition(); const toggle = () => { const newStatus = optimisticStatus === 'active' ? 'inactive' : 'active'; startTransition(async () => { setOptimisticStatus(newStatus); // instant UI update await onUpdateStatus(user.id, newStatus); // server in background }); }; return ( <button onClick={toggle} disabled={isPending} className={`badge ${optimisticStatus === 'active' ? 'bg-success' : 'bg-secondary'}`} > {optimisticStatus} </button> ); } Click. Status changes instantly. The server call happens in the background. If the server fails — React reverts the optimistic update automatically. The dashboard feels instant because it is instant from the user's perspective. I added this to every status toggle, every inline edit, every bulk action in my dashboard. The difference in perceived performance is more noticeable to users than any memoization optimization I'd done previously. What React 19 Didn't Fix Route-level code splitting still matters and still requires explicit configuration. If your dashboard loads all route components upfront, the initial bundle is large regardless of React version: JavaScript // Still need this in React 19 const UsersPage = lazy(() => import('./pages/Users')); const AnalyticsPage = lazy(() => import('./pages/Analytics')); const SettingsPage = lazy(() => import('./pages/Settings')); Image optimization still requires next/image or equivalent. Unoptimized images are still the most common performance problem I see in dashboard codebases, and React 19 does nothing about them. Database query performance still determines how fast your data loads. The fastest React rendering in the world doesn't compensate for a 3-second API response. The Summary React 19 removes the memoization overhead that made large React codebases tedious. Use the compiler, stop writing useMemo for everything, and trust that it handles the cases you were handling manually. What still requires your attention: virtualize large tables, add optimistic updates for frequent mutations, split routes with lazy loading, and optimize your images. The performance work that remains is more interesting than the work React 19 eliminated. That's a good trade.

By Rohit G
Add Observability to Your React Native Application in 5 Minutes
Add Observability to Your React Native Application in 5 Minutes

In modern application development, feature flags are the guardrails that keep experiments controlled and rollbacks safe when conditions shift. If feature flags act as the guardrails, observability provides the visibility: the headlights (traces), mirrors (logs), and dashboard instruments (metrics) that reveal what’s happening in the environment and how well a feature is performing. Together, feature flags and observability unlock powerful insights by correlating code changes with real-time system behavior. This combination reduces time-to-diagnosis and builds greater confidence when rolling out new features. In this post, we’ll walk through just how to add observability to a React Native application using LaunchDarkly’s observability SDK. To demonstrate the process, we’ll build on the PlusOne app, a simple counter app that includes increment (+1), reset, and error-triggering buttons. This lightweight demo provides a clean foundation to showcase how logs, traces, and errors can seamlessly flow into LaunchDarkly for monitoring and debugging. Prerequisites LaunchDarkly account. Sign up for a free one here.Visual Studio or another code editor of choice. All code from this tutorial can be found on GitHub. Setting Up Your Environment Before running a React Native app, make sure your development environment is set up correctly. You can find the full setup instructions for both Android and iOS here. In this tutorial, we'll be running iOS, but keep in mind Expo Orbit, the platform we'll be using to run our iOS simulator, requires both Xcode and Android Studio to be installed. After going through the instructions, you should have the following installed: Node JS (preferably via nvm)Watchman for file monitoringJDK via zulu package managerAndroid Studio. Don’t forget to set your Android_Home environment variablesXcode for the iOS simulatorCocoapods for iOS dependency managementExpo Orbit for running Expo apps on Android or iOS If you're using Android, don't forget to add your environment variables to bash or zsh profile. JavaScript export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/emulator export PATH=$PATH:$ANDROID_HOME/platform-tools Starting Up the PlusOne App To get started, let’s clone the repo for the PlusOne app and run npm install to ensure the proper dependencies are present in our node_modules file. Clone the repo. JavaScript git clone https://github.com/arober39/PlusOne Install dependencies using npm. JavaScript cd PlusOne npm install We’ll also need to run both the prebuild command to generate the iOS file and the expo run command to run the iOS simulator. Prebuild for iOS. JavaScript npx expo prebuild Run expo app. JavaScript npm expo run:ios Now we can view the iOS app in the iPhone simulator using npm. JavaScript # iOS npm run ios # Android npm run android The app should look something like this: Feel free to interact with the app to ensure all is working as expected. As you can see in the code, we have three buttons: one that adds one to the displayed number, one to bring the count back to zero, and an intentional Error button to test error monitoring within the LaunchDarkly UI. JavaScript // app/index.tsx import { useState } from "react"; import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; export default function Index() { const [count, setCount] = useState(0); const handleReset = () => setCount(0); const handleIncrement = () => setCount((prev) => prev + 1); const triggerRecordedError = () => { try { throw new Error("Simulated controlled error from Plus One app") } catch (e) { alert("You intentionally threw an error") } }; return ( <View style={styles.container}> <View style={styles.header}> <Text style={styles.headerText}>Plus One</Text> </View> <View style={styles.counterWrapper}> <Text style={styles.counterText}>{count}</Text> </View> <View style={styles.actionsRow}> <ButtonBox label="Reset" onPress={handleReset} /> <ButtonBox label="+1" onPress={handleIncrement} /> <ButtonBox label="Error" onPress={triggerRecordedError} /> </View> </View> ); } type ButtonBoxProps = { label: string; onPress: () => void; }; function ButtonBox({ label, onPress }: ButtonBoxProps) { return ( <TouchableOpacity onPress={onPress} style={styles.button} activeOpacity={0.8}> <Text style={styles.buttonText}>{label}</Text> </TouchableOpacity> ); } /* The rest of the application code */ Now that we have verified a working app, we can add observability support by downloading the observability React Native SDK. Install LaunchDarkly SDK dependencies. JavaScript npm install @launchdarkly/react-native-client-sdk npm install @launchdarkly/observability-react-native Next, you’ll need to initialize the React Native LD client in the app/_layout file. Replace the in the layout file by pasting the following code. JavaScript // app/_layout.tsx import { Observability } from '@launchdarkly/observability-react-native'; import { AutoEnvAttributes, LDOptions, LDProvider, ReactNativeLDClient } from '@launchdarkly/react-native-client-sdk'; import { Stack } from 'expo-router'; import { useEffect, useState } from 'react'; const options: LDOptions = { applicationInfo: { id: 'Plus-One', name: 'Sample Application', version: '1.0.0', versionName: 'v1', }, debug: true, plugins: [ new Observability({ serviceName: 'my-react-native-app', serviceVersion: '1.0.0', }) ], }; const userContext = { kind: 'user', key: 'test-hello' }; export default function RootLayout() { const [client, setClient] = useState<ReactNativeLDClient | null>(null); useEffect(() => { // Initialize client const featureClient = new ReactNativeLDClient( 'mob-abc123', AutoEnvAttributes.Enabled, options, ); featureClient.identify(userContext).catch((e: any) => console.log(e)); setClient(featureClient); // Cleanup function that runs when component unmounts return () => { featureClient.close(); }; }, []); if (!client) { return null; } return ( <LDProvider client={client}> <Stack /> </LDProvider> ); } First, we’re importing the Observability SDK as well as a few LD libraries to add options and attributes to the LD client. Initialized the SDK and plugin options.Defined the user context.Lastly, you initialized the client. Now that you have defined your LD React Native client, you can implement different observability methods within your application logic. We can do this by importing the LDObserve library in the app/_layout.tsx file. JavaScript import { LDObserve } from '@launchdarkly/observability-react-native'; Then, add the recordError() method within the triggerRecordedError function inside the app/_layout.tsx file. This will allow for error messages to be sent back to the LD UI. JavaScript const triggerRecordedError = () => { try { throw new Error("Simulated controlled error from Plus One app") } catch (e) { LDObserve.recordError(e as Error, {feature: "test-button"}) alert("You intentionally threw an error") } }; Before being able to receive data in the LD UI, you’ll need to add your mobile key to the React Native LD client, which can be found by logging in to the LD UI. Once logged in, tap the settings button at the bottom left. Navigate to the Projects page and click Create to create a new project. Define the new Project and click Create Project. Then, define the environment where you would like your data to be sent. Now, grab the mobile key by pressing the three dots for the environment and selecting the mobile key, which will copy the key to your keyboard. Then, add it to the app/_layout file. JavaScript const featureClient = new ReactNativeLDClient( ‘mob-abc123’, AutoEnvAttributes.Enabled, options, ); Finally, you can generate data by interacting with your app in the iOS app simulator. Feel free to restart the app to ensure data is displaying in real time. JavaScript npm expo run:ios Once you navigate back to the LD UI, you should be able to see the logs, traces, and errors under the Monitor section. Logs Traces Errors Conclusion In just a few minutes, we’ve taken the PlusOne React Native app from a simple counter to a fully observable application connected to LaunchDarkly. By setting up the SDK, initializing observability plugins, and recording errors, we now have a live feedback loop where application behavior is visible in the LaunchDarkly UI. This makes it far easier to diagnose issues, validate feature flag rollouts, and ensure smooth user experiences. Next Steps Looking ahead, there are many ways to expand on what we’ve built by including features like recording custom metrics and session replay, which provide even deeper insights into app behavior. By integrating observability at the foundation of your React Native projects, you equip your team with the clarity needed to debug faster, ship features more confidently, and deliver reliable experiences to your users. You can also read this article to learn more about observability and guarded releases.

By Alexis Roberson
Dead Letter Queue Patterns in Apache Flink: Handling Poison Messages Without Stopping Your Stream
Dead Letter Queue Patterns in Apache Flink: Handling Poison Messages Without Stopping Your Stream

Streaming systems usually fail in one of two ways: Loudly, when infrastructure breaksQuietly, when one bad record keeps replaying until the pipeline is effectively dead The second failure mode is more dangerous because it often starts with something small: malformed JSON, an unexpected schema change, a missing required field, or a downstream timeout that was never handled correctly. In Apache Flink, one unhandled exception can trigger a restart. If the same poison message is still sitting in Kafka after recovery, the job reads it again, fails again, restarts again, and enters a loop. At that point, the pipeline is technically "recovering," but operationally it is down. This is exactly why production Flink jobs need a Dead Letter Queue (DLQ) strategy from day one. A proper DLQ pattern does three things: Isolates bad records so they do not stop good onesCaptures enough failure context to debug the issue laterPreserves replayability so quarantined records can be reprocessed after the root cause is fixed Anything less is not really a DLQ. It is either silent data loss or delayed outage. In this article, I will walk through the most practical DLQ patterns for Apache Flink 1.18: Side outputs as the core DLQ primitiveRetry with exponential backoff for transient failuresTiered DLQ routing by error classKafka and S3 sink patternsMetrics and alertingReplay with a dedicated reprocessing jobA PyFlink version of the side output pattern The goal is simple: a bad message should never silently disappear, and it should never silently stop the stream. Why Poison Messages Break Otherwise Healthy Pipelines A poison message is any record that consistently fails processing. Typical examples include: Malformed JSONIncompatible schema versionsMissing required fieldsInvalid business valuesRecords that trigger unexpected code pathsMessages that repeatedly fail downstream enrichment calls Without DLQ handling, the failure path usually looks like this: The record enters the pipelineDeserialization or validation throws an exceptionThe operator failsFlink restarts from the last checkpointThe same record is consumed againThe same exception happens again That loop can continue indefinitely. The result is predictable: Throughput drops to zeroDownstream consumers starveCheckpoint recovery does not helpOn-call engineers get paged for a problem caused by one record This is why DLQ handling is not just an error-handling convenience. It is a core reliability pattern. What a DLQ Should Look Like in Flink In a streaming architecture, a DLQ is a durable destination for records that could not be processed successfully. For Flink, that means the DLQ record should usually include: Raw payloadError typeError messageStack trace or summarized failure contextFailure timestampSource metadata such as topic, partition, or offset when available That information matters because a DLQ is only useful if someone can answer two questions later: Why did this record fail?How do I replay it safely once the issue is fixed? If you only log the exception, you lose replayability. If you only store the payload, you lose debugging context. If you drop the record entirely, you lose both. So the design target is not "catch exceptions." The design target is durable, observable, replayable failure handling. Pattern 1: Use Side Outputs as the Core DLQ Primitive The most natural DLQ mechanism in Flink is the side output. A side output allows one operator to emit records to multiple streams: The main stream for successful recordsOne or more side streams for failures, late data, or quarantined records That makes it the right primitive for DLQ routing. Define the DLQ Envelope and Output Tag Java import org.apache.flink.util.OutputTag; import org.apache.flink.streaming.api.functions.ProcessFunction; import org.apache.flink.util.Collector; public static final OutputTag<DeadLetterRecord> DLQ_TAG = new OutputTag<DeadLetterRecord>("dead-letter-queue") {}; public record DeadLetterRecord( String rawPayload, String errorType, String errorMessage, String stackTrace, long failedAtEpochMs, String sourceTopicPartition, long sourceOffset ) {} The important point here is that the DLQ record is not just the failed payload. It is an envelope that preserves enough context for triage and replay. Route Failures Inside a ProcessFunction Java public class EntityEventProcessor extends ProcessFunction<String, EntityEvent> { @Override public void processElement( String rawMessage, Context ctx, Collector<EntityEvent> out) { try { EntityEvent event = parseAndValidate(rawMessage); out.collect(event); } catch (JsonParseException e) { ctx.output(DLQ_TAG, new DeadLetterRecord( rawMessage, "JSON_PARSE_FAILURE", e.getMessage(), getStackTrace(e), System.currentTimeMillis(), ctx.element().toString(), -1L )); } catch (SchemaValidationException e) { ctx.output(DLQ_TAG, new DeadLetterRecord( rawMessage, "SCHEMA_VALIDATION_FAILURE", e.getMessage(), getStackTrace(e), System.currentTimeMillis(), ctx.element().toString(), -1L )); } catch (Exception e) { ctx.output(DLQ_TAG, new DeadLetterRecord( rawMessage, "UNKNOWN_FAILURE", e.getMessage(), getStackTrace(e), System.currentTimeMillis(), ctx.element().toString(), -1L )); } } private EntityEvent parseAndValidate(String raw) throws JsonParseException, SchemaValidationException { EntityEvent event = objectMapper.readValue(raw, EntityEvent.class); if (event.entityId() == null || event.entityId().isBlank()) { throw new SchemaValidationException("entityId is required"); } if (event.timestamp() <= 0) { throw new SchemaValidationException("timestamp must be positive"); } return event; } } This is the minimum viable DLQ pattern, and it already solves the most important operational problem: bad records no longer stop good ones. Wire the Main Stream and DLQ Stream Java StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<String> kafkaSource = env .fromSource(buildKafkaSource(), WatermarkStrategy.noWatermarks(), "entity-events-source"); SingleOutputStreamOperator<EntityEvent> processed = kafkaSource.process(new EntityEventProcessor()); DataStream<EntityEvent> goodEvents = processed; DataStream<DeadLetterRecord> deadLetters = processed.getSideOutput(DLQ_TAG); goodEvents.sinkTo(buildDownstreamKafkaSink()); deadLetters.sinkTo(buildDlqKafkaSink()); env.execute("Entity Resolution Pipeline"); If you do nothing else, do this. Side outputs should be the default DLQ foundation in Flink. Pattern 2: Retry Transient Failures Before Escalating to DLQ Not every failure belongs in the DLQ immediately. Some failures are transient: A downstream service is temporarily unavailableA database call times outAn external API is rate-limitedA network dependency is briefly unstable If you send all of those directly to the DLQ, you create noise and bury the truly bad records. The better pattern is: Retry transient failures a limited number of timesUse exponential backoffEscalate to DLQ only after retries are exhausted Retry With KeyedProcessFunction and Timers Java public class RetryingEnrichmentProcessor extends KeyedProcessFunction<String, EntityEvent, EnrichedEvent> { private static final int MAX_RETRIES = 3; private static final long BASE_BACKOFF_MS = 500L; private transient ValueState<Integer> retryCountState; private transient ValueState<EntityEvent> pendingEventState; @Override public void open(Configuration parameters) { retryCountState = getRuntimeContext().getState( new ValueStateDescriptor<>("retry-count", Integer.class)); pendingEventState = getRuntimeContext().getState( new ValueStateDescriptor<>("pending-event", EntityEvent.class)); } @Override public void processElement( EntityEvent event, Context ctx, Collector<EnrichedEvent> out) throws Exception { try { EnrichedEvent enriched = callEnrichmentService(event); retryCountState.clear(); pendingEventState.clear(); out.collect(enriched); } catch (TransientServiceException e) { int retries = retryCountState.value() == null ? 0 : retryCountState.value(); if (retries >= MAX_RETRIES) { retryCountState.clear(); pendingEventState.clear(); ctx.output(DLQ_TAG, new DeadLetterRecord( event.toString(), "MAX_RETRIES_EXCEEDED", "Failed after " + MAX_RETRIES + " retries: " + e.getMessage(), getStackTrace(e), System.currentTimeMillis(), ctx.getCurrentKey(), -1L )); } else { retryCountState.update(retries + 1); pendingEventState.update(event); long backoffMs = BASE_BACKOFF_MS * (long) Math.pow(2, retries); ctx.timerService().registerProcessingTimeTimer( System.currentTimeMillis() + backoffMs ); } } catch (PoisonMessageException e) { ctx.output(DLQ_TAG, new DeadLetterRecord( event.toString(), "POISON_MESSAGE", e.getMessage(), getStackTrace(e), System.currentTimeMillis(), ctx.getCurrentKey(), -1L )); } } @Override public void onTimer( long timestamp, OnTimerContext ctx, Collector<EnrichedEvent> out) throws Exception { EntityEvent pending = pendingEventState.value(); if (pending == null) return; try { EnrichedEvent enriched = callEnrichmentService(pending); retryCountState.clear(); pendingEventState.clear(); out.collect(enriched); } catch (TransientServiceException e) { int retries = retryCountState.value(); if (retries >= MAX_RETRIES) { retryCountState.clear(); pendingEventState.clear(); ctx.output(DLQ_TAG, new DeadLetterRecord( pending.toString(), "MAX_RETRIES_EXCEEDED", "Timer retry exhausted: " + e.getMessage(), getStackTrace(e), System.currentTimeMillis(), ctx.getCurrentKey(), -1L )); } else { retryCountState.update(retries + 1); long backoffMs = BASE_BACKOFF_MS * (long) Math.pow(2, retries); ctx.timerService().registerProcessingTimeTimer( timestamp + backoffMs ); } } } } Why This Works Especially Well in Flink This pattern is stronger in Flink than in many other stream processors because timers and state are checkpointed. That means: Retry counters survive restartsPending events survive restartsScheduled retries resume after recovery In other words, the retry workflow itself is fault-tolerant. That is exactly what you want when handling transient failures in a long-running stream. Pattern 3: Split the DLQ by Failure Type Once a pipeline matures, a single DLQ topic usually becomes too coarse. Schema failures, business validation failures, exhausted retries, and unknown exceptions all end up mixed together. That makes triage slower and replay harder. A better pattern is to classify failures and route them to separate DLQ streams. Define Failure Tiers Java public enum DlqTier { TRANSIENT_EXHAUSTED, SCHEMA_INVALID, BUSINESS_RULE, UNKNOWN } Route by Exception Class Java public class TieredDlqRouter extends ProcessFunction<String, EntityEvent> { @Override public void processElement( String raw, Context ctx, Collector<EntityEvent> out) { try { EntityEvent event = parse(raw); validate(event); out.collect(event); } catch (JsonParseException | MappingException e) { route(ctx, raw, DlqTier.SCHEMA_INVALID, e); } catch (BusinessValidationException e) { route(ctx, raw, DlqTier.BUSINESS_RULE, e); } catch (Exception e) { route(ctx, raw, DlqTier.UNKNOWN, e); } } private void route(Context ctx, String raw, DlqTier tier, Exception e) { OutputTag<DeadLetterRecord> tag = getTierTag(tier); ctx.output(tag, new DeadLetterRecord( raw, tier.name(), e.getMessage(), getStackTrace(e), System.currentTimeMillis(), "", -1L )); } } Define One Output Tag Per Tier Java public static final OutputTag<DeadLetterRecord> DLQ_SCHEMA = new OutputTag<>("dlq-schema-invalid") {}; public static final OutputTag<DeadLetterRecord> DLQ_BUSINESS = new OutputTag<>("dlq-business-rule") {}; public static final OutputTag<DeadLetterRecord> DLQ_UNKNOWN = new OutputTag<>("dlq-unknown") {}; Sink Each Tier Independently Java SingleOutputStreamOperator<EntityEvent> processed = kafkaSource.process(new TieredDlqRouter()); processed.getSideOutput(DLQ_SCHEMA) .sinkTo(buildKafkaSink("dlq.schema-invalid")); processed.getSideOutput(DLQ_BUSINESS) .sinkTo(buildKafkaSink("dlq.business-rule")); processed.getSideOutput(DLQ_UNKNOWN) .sinkTo(buildKafkaSink("dlq.unknown")); This makes the DLQ operationally useful instead of just technically correct. For example: Schema failures can be routed to the producer teamBusiness rule failures can feed data quality workflowsUnknown failures can trigger higher-severity alerting Pattern 4: Choose DLQ Sinks Based on How You Plan To Recover Once records are routed to a DLQ stream, they need a durable destination. In practice, the two most common choices are Kafka and object storage. Kafka DLQ Sink Kafka is the right choice when you want: Near-real-time inspectionStreaming replayOperational integration with existing consumers Java private static KafkaSink<DeadLetterRecord> buildDlqKafkaSink( String topicName) { return KafkaSink.<DeadLetterRecord>builder() .setBootstrapServers("kafka-broker:9092") .setRecordSerializer( KafkaRecordSerializationSchema.builder() .setTopic(topicName) .setValueSerializationSchema( new JsonSerializationSchema<>(DeadLetterRecord.class)) .setKeySerializationSchema( record -> record.errorType().getBytes()) .build() ) .setDeliveryGuarantee(DeliveryGuarantee.AT_LEAST_ONCE) .build(); } S3 DLQ Sink Object storage is the better choice when you want: Long retentionLow-cost quarantineBatch replay with Spark or AthenaPartitioned storage by date or error type Java private static FileSink<DeadLetterRecord> buildS3DlqSink() { return FileSink .forRowFormat( new Path("s3://your-bucket/dlq/entity-resolution/"), new JsonRowEncoder<>(DeadLetterRecord.class) ) .withRollingPolicy( DefaultRollingPolicy.builder() .withRolloverInterval(Duration.ofMinutes(15)) .withInactivityInterval(Duration.ofMinutes(5)) .withMaxPartSize(MemorySize.ofMebiBytes(128)) .build() ) .withBucketAssigner( new DateTimeBucketAssigner<>( "error-type='unknown'/year=yyyy/month=MM/day=dd/hour=HH") ) .build(); } A practical production pattern is to use: Kafka for short-term operational handlingS3 for long-term quarantine and replay That gives you both fast response and durable history. Pattern 5: Monitor DLQ Rate, Not Just Job Uptime A DLQ that nobody watches is just a backlog with better branding. Job uptime alone is not enough. A Flink job can stay green while quietly routing 10% of traffic to the DLQ. That is still a production incident. Add Metrics Inside the Operator Java public class MonitoredEntityEventProcessor extends ProcessFunction<String, EntityEvent> { private transient Counter dlqCounter; private transient Counter successCounter; private transient Histogram processingLatency; @Override public void open(Configuration parameters) { MetricGroup metrics = getRuntimeContext() .getMetricGroup() .addGroup("entity_resolution"); dlqCounter = metrics.counter("dlq_routed_total"); successCounter = metrics.counter("processed_success_total"); processingLatency = metrics.histogram( "processing_latency_ms", new DescriptiveStatisticsHistogram(1000) ); } @Override public void processElement( String raw, Context ctx, Collector<EntityEvent> out) { long start = System.currentTimeMillis(); try { EntityEvent event = parseAndValidate(raw); successCounter.inc(); out.collect(event); } catch (Exception e) { dlqCounter.inc(); ctx.output(DLQ_TAG, buildDeadLetter(raw, e)); } finally { processingLatency.update(System.currentTimeMillis() - start); } } } Alert on DLQ Rate A useful alert is DLQ throughput relative to successful throughput: YAML - alert: FlinkDlqRateHigh expr: | rate(flink_entity_resolution_dlq_routed_total[5m]) / rate(flink_entity_resolution_processed_success_total[5m]) > 0.01 for: 2m labels: severity: warning annotations: summary: "DLQ rate exceeds 1% of total throughput" description: "Check dlq.unknown Kafka topic for upstream schema changes" As a rule of thumb: above 1% often indicates schema drift or producer issuesabove 5% usually indicates a broader systemic problem The exact thresholds depend on the pipeline, but the principle does not: monitor DLQ rate as a first-class health signal. Pattern 6: Replay With a Dedicated Reprocessing Job A DLQ is only complete when replay is possible. The cleanest design is a separate Flink job that reads from the DLQ topic and routes records back through the main processing logic. Example Replay Job Java public class DlqReprocessingJob { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<DeadLetterRecord> dlqStream = env .fromSource( buildKafkaSource("dlq.schema-invalid"), WatermarkStrategy.noWatermarks(), "dlq-source" ); DataStream<String> replayStream = dlqStream .filter(r -> r.failedAtEpochMs() >= START_EPOCH && r.failedAtEpochMs() <= END_EPOCH) .map(DeadLetterRecord::rawPayload); SingleOutputStreamOperator<EntityEvent> reprocessed = replayStream.process(new EntityEventProcessor()); reprocessed.sinkTo(buildDownstreamKafkaSink()); reprocessed.getSideOutput(DLQ_TAG) .sinkTo(buildKafkaSink("dlq.permanent-quarantine")); env.execute("DLQ Reprocessing Job"); } } Why Replay Should Be a Separate Job Keeping replay separate from the main pipeline gives you: Independent scalingIndependent schedulingCleaner checkpoint behaviorSafer operational control It also lets you drain backlogs on your own terms: Off-peak hoursReduced parallelismOr maximum parallelism when you need to catch up quickly That separation keeps the main pipeline stable while still making recovery practical. PyFlink Version: Same Pattern, Same Principle If your team uses PyFlink, the same side output pattern applies. Python from pyflink.datastream import StreamExecutionEnvironment from pyflink.datastream.functions import ProcessFunction from pyflink.common.typeinfo import Types from pyflink.datastream.output_tag import OutputTag DLQ_TAG = OutputTag( "dead-letter-queue", Types.ROW_NAMED( ["raw_payload", "error_type", "error_message", "failed_at_ms"], [Types.STRING(), Types.STRING(), Types.STRING(), Types.LONG()] ) ) class EntityEventProcessor(ProcessFunction): def process_element(self, value, ctx): try: event = parse_and_validate(value) yield event except Exception as e: from pyflink.common import Row yield DLQ_TAG, Row( raw_payload=str(value), error_type=type(e).__name__, error_message=str(e), failed_at_ms=int(time.time() * 1000) ) env = StreamExecutionEnvironment.get_execution_environment() source_stream = env.from_source(...) processed = source_stream.process( EntityEventProcessor(), output_type=Types.STRING() ) good_events = processed dead_letters = processed.get_side_output(DLQ_TAG) good_events.sink_to(build_downstream_sink()) dead_letters.sink_to(build_dlq_sink()) env.execute("Entity Resolution Pipeline") The syntax changes, but the design principle stays the same: good records continue, bad records are isolated and persisted. Production Checklist Before shipping a Flink pipeline, verify the following: RequirementWhy It MattersRisky operators wrapped in try/catchPrevents restart loops from unhandled exceptionsDLQ output tags use explicit typingAvoids runtime serialization failuresDLQ sink is durableFailed records must survive restartsDLQ metrics are exportedSilent DLQ growth is otherwise invisibleReplay path exists and is testedA DLQ without replay is just storageDLQ retention is long enoughTeams need time to diagnose and replayPermanent quarantine existsPrevents infinite replay loopsAlerting is based on DLQ rateJob health alone is not enough This checklist is worth automating in code review or deployment readiness checks. DLQ handling is too important to leave to convention. Key Takeaways If you are building Flink pipelines in production, the safest default is: Use side outputs for DLQ routingRetry transient failures before escalationClassify failures into separate DLQ streamsSink DLQ records durablyExport DLQ metricsReplay through a dedicated job The core rule is simple: A bad message should never silently disappear, and it should never silently stop the stream. That is what turns DLQ handling from a defensive coding trick into a real reliability pattern. Environment Notes The examples in this article target: Apache Flink 1.18Java 17PyFlink 1.18 A few implementation notes: The retry timer pattern requires a keyed stream before KeyedProcessFunctionRocksDB is usually the safer state backend for larger retry stateHashMap state backend can work well for smaller, latency-sensitive workloadsAT_LEAST_ONCE is usually sufficient for DLQ sinks Final Thoughts Poison messages are not rare in streaming systems. They are inevitable. The real question is whether one bad record can take down an otherwise healthy pipeline. With the right DLQ design in Flink, the answer becomes no. The stream keeps moving. Good records continue. Bad records are quarantined. Alerts fire. Replay remains possible. And the pipeline stays operational while the root cause is fixed. That is the difference between a stream that works in staging and one that survives production.

By Rohit Muthyala
AI-Augmented React Development: How I Rebuilt My Workflow Without Losing Control of the Code
AI-Augmented React Development: How I Rebuilt My Workflow Without Losing Control of the Code

Every React developer reaches a point where the sheer volume of boilerplate starts to slow them down. Prop drilling, repetitive hook patterns, component scaffolding, unit test setup — the cognitive overhead adds up fast, especially at enterprise scale. When GitHub Copilot entered my workflow, I expected a productivity boost. What I didn't expect was how much I'd have to think about using it correctly. After integrating AI-assisted development into a React 18 codebase — spanning custom hooks, context-based state management, and accessibility-driven UI — I came away with a clear picture of where AI genuinely accelerates the work, where it quietly introduces risk, and what guardrails every team needs before they ship AI-assisted code to production. This isn't a tutorial on setting up Copilot. It's an honest account of what changed in my day-to-day React workflow, and how I rebuilt my development process around the strengths of AI without surrendering architectural judgment. Where AI Actually Accelerates React Development 1. Component Scaffolding The most immediate win was generating boilerplate-heavy component shells. React functional components follow a predictable structure: imports, props interface, state declarations, effect hooks, render return. Copilot autocompletes this structure accurately and fast, especially when your file already has consistent patterns. For example, starting a new form component with a comment like: Plain Text // Controlled form component with validation and submit handler … triggers a usable scaffold within seconds. In a codebase with 50+ form components, this adds up to meaningful time savings. 2. TypeScript Prop Typing One of the most tedious parts of React 18 development is defining interface types for component props — especially for components consuming API response shapes. Copilot handles this well when the API shape is already defined elsewhere in the file or project. It infers prop types from usage context and generates clean interfaces without much guidance. 3. Unit Test Generation Copilot shines at generating @testing-library/react test cases for presentational components. Given a component file, it can suggest: Render testsUser interaction tests (click, input change)Accessibility checks using getByRole This reduced the time I spent on repetitive test scaffolding by roughly 40% for simple components. 4. Repetitive Hook Patterns Standard hooks like useEffect with cleanup, useCallback with dependency arrays, and useMemo for expensive computations follow well-known patterns. Copilot autocompletes these reliably — and the suggestions are often correct on the first try when the surrounding context is clear. Where AI Fails React Developers (and Why It Matters) This is the part most AI-workflow articles skip. In my experience, Copilot introduced subtle issues in three specific areas: 1. State Management Architecture Copilot is pattern-matching, not reasoning. When I was designing a context-based global state solution for a multi-step form flow, Copilot consistently suggested patterns that worked for isolated examples but didn't scale: it created redundant useContext calls across components that should have been wrapped in a provider, and it failed to account for re-render performance implications. The lesson: Never accept AI suggestions for state architecture without reviewing the component tree. AI optimizes locally; architecture requires global thinking. 2. Custom Hook Dependency Arrays Incorrect dependency arrays in useEffect and useCallback are a well-known React footgun. Copilot's suggestions here were hit-or-miss. It occasionally omitted dependencies that needed to be included and included stale values that triggered unnecessary re-renders. I started treating all AI-generated dependency arrays as drafts that required manual review against the ESLint react-hooks/exhaustive-deps rule. This step is non-negotiable. 3. Accessibility in JSX This one is subtle. Copilot generates functional JSX — but accessible JSX requires deliberate attention to ARIA roles, focus management, and semantic HTML. AI-generated components often defaulted to div-heavy markup without the aria-* attributes or keyboard event handlers that production apps require. For any component touching user interaction — modals, dropdowns, form controls — I reviewed AI-generated output against WCAG 2.1 AA standards before committing. My Rebuilt Workflow: A Practical Stack After months of iteration, here's the workflow that works: Phase 1: Design First, Prompt Second Before I open a new file, I sketch the component's responsibilities on paper or in a comment block: JavaScript /** * UserProfileCard * - Displays user avatar, name, role * - Supports edit mode toggle * - Emits onSave callback with updated values * - Must be keyboard accessible */ This comment becomes the Copilot context. The more specific the intent, the better the scaffold. Phase 2: Accept Scaffolding, Write Logic I accept Copilot suggestions for: Component shellProp interfaceState variable declarationsJSX structure for simple layouts I write manually: useEffect logic and cleanupEvent handler implementationsContext provider designError boundariesAny business logic touching API data Phase 3: Review AI-Generated Tests Copilot generates test scaffolding well. I review every generated test for: Correct use of userEvent vs fireEventAccurate assertions (not just "it rendered")Missing edge cases (empty state, error state, loading state) Phase 4: Accessibility Audit Pass Every component gets a final pass against: Semantic HTML element usagearia-label / aria-describedby for interactive elementsKeyboard navigation (tab order, focus trap for modals)Color contrast (handled at design system level, not component level) A Real Before-and-After Example Before (pre-AI workflow): A controlled input component with validation took roughly 25–30 minutes to scaffold, type, test, and review. After (AI-augmented workflow): The same component takes 10–12 minutes — with Copilot handling the initial scaffold and test shell, and me handling the validation logic, hook dependencies, and accessibility pass. Here's a simplified example of the kind of component where AI delivers the most value: TypeScript interface SearchInputProps { value: string; onChange: (value: string) => void; onSubmit: () => void; placeholder?: string; isLoading?: boolean; } const SearchInput: React.FC<SearchInputProps> = ({ value, onChange, onSubmit, placeholder = "Search...", isLoading = false, }) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") onSubmit(); }; return ( <div role="search"> <input type="search" value={value} onChange={(e) => onChange(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder} aria-label="Search" disabled={isLoading} /> <button onClick={onSubmit} disabled={isLoading} aria-label="Submit search"> {isLoading ? "Searching..." : "Search"} </button> </div> ); }; The scaffold, prop interface, and JSX structure above were AI-generated in under 30 seconds. The aria-label attributes, role="search", and handleKeyDown implementation were my additions — things Copilot consistently missed in initial suggestions. Where AI Hits a Wall: Large-Scale Enterprise React Projects Small, isolated components are where AI shines. But real enterprise codebases are rarely small or isolated. Once you're working inside a large monorepo with hundreds of components, shared design systems, domain-specific business logic, and cross-team API contracts, AI-assisted development runs into a fundamental limitation: it only sees what's in its context window. Here's where that breaks down in practice: 1. Cross-File Dependency Awareness In a large React application, a single component may depend on a shared context provider defined four directories away, a utility hook maintained by a different team, and a TypeScript type exported from a core domain package. Copilot's autocomplete works within the file you're editing — it doesn't have a deep understanding of the full dependency graph. The result: AI-generated code that compiles locally but breaks at integration because it assumes a prop shape, import path, or context value that doesn't match what actually exists in the broader system. I've seen this surface most often with shared form validation schemas and API response types that live outside the component's immediate file tree. 2. Institutional Knowledge and Business Logic Enterprise React codebases carry years of intentional decisions that aren't documented anywhere in the code — they live in the heads of the team. Why is this particular component wrapped in a custom error boundary? Why does this dropdown use a local state copy instead of reading directly from context? Why is this API called twice? Copilot has no way of knowing. When it generates code in these areas, it produces something that looks reasonable but violates the implicit contract the team has built over time. Catching these violations requires a senior developer who understands the why behind the existing patterns — AI cannot substitute for that. 3. Design System Consistency at Scale Large teams typically maintain a shared component library — think an internal fork of Material UI or a custom design system. AI tools don't know which internal components to reach for. Copilot frequently suggests raw HTML elements or third-party components when the project has established internal equivalents: <Button> from your design system instead of <button>, <TextInput> from your library instead of a raw <input>. At scale, this creates design debt fast. Every AI-generated component that uses a raw HTML element instead of the design system equivalent is a component that diverges from your visual and behavioral standards — and accumulates technical debt that's expensive to audit later. 4. Performance Optimization in Complex Component Trees React 18 introduced useDeferredValue, useTransition, and concurrent rendering features specifically to handle performance in large, deeply nested component trees. These are nuanced APIs — their correct usage depends on understanding the rendering priority of specific subtrees, which operations are expensive, and what the user experience should be during transitions. Copilot-generated code in this area is almost always naive. It doesn't know that a particular list component renders 500+ items and needs virtualization. It doesn't know that a specific state update should be wrapped in startTransition to keep the UI responsive. Optimizing a large React application for performance remains deeply human work. 5. Multi-Team Merge Conflicts and Shared State In enterprise projects with multiple teams contributing to the same React codebase, shared state management becomes politically and technically complex. Redux slices, Zustand stores, or React Query caches span team boundaries. AI tools can suggest changes to these shared structures without awareness of how other teams depend on them — leading to breakages that only surface in integration environments. The practical takeaway: the larger and more interconnected the codebase, the more you need to treat AI as a localized assistant, not a system-aware collaborator. Use it to accelerate work on leaf-node components and isolated utilities. Treat any AI suggestion that touches shared state, cross-team APIs, or core infrastructure with the same scrutiny you'd give an external contributor who just joined the project. If you're introducing AI-assisted development into a React team, here are the non-negotiables: 1. Never merge AI-generated code without lint and type checks passing. Run eslint, tsc --noEmit, and your test suite before treating any AI-generated file as complete. 2. Establish a "no AI for architecture" rule. Component tree design, context structure, routing decisions, and data fetching strategy should be human-driven. AI is a code accelerator, not an architect. 3. Code review AI-generated PRs with extra scrutiny. Reviewers should specifically look for: missing hook dependencies, over-broad useEffect triggers, missing accessibility attributes, and logic that "looks right" but doesn't account for edge cases. 4. Document what AI touched. Some teams are beginning to tag AI-assisted code in commit messages or comments. This creates accountability and helps reviewers calibrate their scrutiny. 5. Keep your feedback loop active. When Copilot generates something wrong, reject it explicitly rather than accepting and editing. This helps calibrate your own pattern recognition for what AI does and doesn't handle well. What's Coming Next: Agentic React Workflows The current state of AI in React development is assistive — it completes what you start. The next wave is agentic: AI agents that can take a design spec or Figma export, scaffold an entire component hierarchy, wire up state, and generate test coverage — with a human reviewing the output rather than writing it line by line. Early tools like Cursor's Composer mode and experimental GitHub Copilot Workspace are beginning to move in this direction. For React developers, the implication is a shift in the skill that matters most: from writing components quickly to reviewing and evaluating AI-generated component systems critically. The developers who will thrive in this environment are those who deeply understand React's rendering model, state management tradeoffs, and accessibility requirements — not because they're writing every line, but because they're the final judgment layer on what ships. Conclusion AI-augmented development isn't about replacing React expertise — it's about redirecting it. The hours saved on scaffolding and boilerplate are hours you can reinvest in architecture, performance, accessibility, and code quality. The key insight from rebuilding my workflow around GitHub Copilot is this: AI is a force multiplier for what you already know well. If you understand React deeply, it makes you faster. If you're still learning React's mental model, it can quietly introduce patterns that seem right but aren't. Used with clear guardrails and deliberate review habits, AI turns a good React developer into a significantly more productive one — without sacrificing the code quality that enterprise applications demand.

By Sathwik Nagulapati
Fix the Target, Precompute Once: A Backend-Free Word-Ladder Solver With a BFS Distance Field
Fix the Target, Precompute Once: A Backend-Free Word-Ladder Solver With a BFS Distance Field

When you build an interactive puzzle, the latency budget is unforgiving. Every keystroke needs an answer that feels instant. A daily word-ladder game has to do three of those instant jobs at once: confirm that the word a player typed is legal, tell them the best possible score for the day, and, on request, reveal the shortest solution. I ran into all three while building Poople, a daily game where you change a 4-letter word into POOP one letter at a time, and the fix turned out to be a tidy lesson in trading repeated computation for one-time precomputation. The obvious approach is to run a graph search whenever you need an answer. That works, and it is also the wrong default here. This article walks through why, then shows how fixing the destination word lets you replace every future search with a single offline pass plus an O(1) lookup. The whole solver then runs in the browser, with no backend and no per-request search. Figure 1. The expensive graph work happens once at build time. The runtime only does lookups. The Problem in Graph Terms A word ladder connects two words by changing one letter at a time while keeping a valid word at each step. The idea is old. Lewis Carroll published it as Doublets in 1877. Model it as a graph, and it becomes a textbook shortest-path problem: Each valid 4-letter word is a node.Two nodes share an edge when their words differ by exactly one letter.The shortest path between two words is the fewest steps to ladder between them. Figure 2. Distances to the fixed target POOP form a field. Every word with a finite distance has a neighbor one step closer. In Poople, the destination is always the same word, POOP, and that fixed target is the hinge the whole design turns on. The shortest distance from a word to POOP is what the game calls par, the best achievable score for that day's starting word. In an unweighted graph like this one, breadth-first search gives those shortest distances directly. The graph is small. The shipped dictionary holds about 2,300 valid 4-letter words, and the hardest starting words sit around eleven steps from POOP. Small, but not so small that you want to search for it again on every interaction. Modeling the Edges Without Storing Them You do not need an adjacency list. Because an edge is just a one-letter difference, you can generate a node's neighbors on demand by trying every single-letter change and keeping the ones that land on a real word. Membership is a Set lookup. TypeScript const ALPHABET = "abcdefghijklmnopqrstuvwxyz"; /** Every dictionary word exactly one letter away from `word`. */ function neighbors(word: string, dictionary: Set<string>): string[] { const out: string[] = []; for (let i = 0; i < word.length; i++) { for (const c of ALPHABET) { if (c === word[i]) continue; const candidate = word.slice(0, i) + c + word.slice(i + 1); if (dictionary.has(candidate)) out.push(candidate); } } return out; } For a 4-letter word, this checks 4 positions times 25 other letters, so 100 candidate strings, each an O(1) Set lookup. The graph stays implicit, which keeps the shipped data to a flat word list rather than a serialized edge structure. Figure 5. Neighbors are generated by mutating each position, then filtered by membership in the word set. Invalid strings are dropped. The Naive Approach, and Why It Does Not Fit With neighbors in hand, the textbook move is a per-query breadth-first search that returns the path. TypeScript function shortestPath(start: string, end: string, dict: Set<string>): string[] | null { if (!dict.has(start)) return null; const queue: string[][] = [[start]]; const visited = new Set<string>([start]); while (queue.length) { const path = queue.shift()!; const node = path[path.length - 1]; if (node === end) return path; for (const next of neighbors(node, dict)) { if (!visited.has(next)) { visited.add(next); queue.push([...path, next]); } } } return null; This is correct and easy to read. It also has two properties I did not want in a game loop. It stores a full path for every entry in the queue, so memory grows with the frontier. More importantly, it repeats the entire search for every word a player explores. On a game whose target never changes, that is the same work over and over. The Inversion: One Search From the Target Here is the key observation. Every query ends at the same node, POOP. So search backward from POOP exactly once. One breadth-first pass sources at the target labels every reachable word with its distance to POOP. That labeling is a distance field, the same idea as a flow field in grid pathfinding, and it answers every future query in advance. Figure 4. Searching per query repeats work. One precomputed field turns every later query into a lookup. The build step runs offline, in a script during the build, never in the player's browser. TypeScript /** Run once at build time. Distance from every reachable word to the target. */ function buildDistanceField(words: Set<string>, target = "poop"): Map<string, number> { const dist = new Map<string, number>([[target, 0]]); let frontier = [target]; while (frontier.length) { const next: string[] = []; for (const word of frontier) { const d = dist.get(word)! + 1; for (const neighbor of neighbors(word, words)) { if (!dist.has(neighbor)) { dist.set(neighbor, d); next.push(neighbor); } } } frontier = next; } return dist; Because the graph is undirected, distance from POOP to a word equals distance from that word to POOP, so one source covers the entire dictionary. The output serializes to one word,distance line per word, which is the data the game ships. TypeScript // build-distances.ts const field = buildDistanceField(allWords); const lines = [...field].map(([word, d]) => `${word},${d}`).join("\n"); writeFileSync("word-dist.ts", "export const WORD_DIST_RAW = `\n" + lines + "\n`;"); For about 2,300 words, the full pass finishes in a few milliseconds on a laptop, and the resulting table is roughly 17 KB of raw text. That table is the only artifact the runtime needs. Runtime: Lookups Instead of Searches At load, the shipped table parses once into two structures: a Map from word to distance, and a Set of valid words. After that, the three jobs from the introduction are all constant-time or close to it. TypeScript const distEntries: Array<[string, number]> = WORD_DIST_RAW .trim() .split("\n") .map((line) => { const [word, dist] = line.split(","); return [word.trim().toLowerCase(), parseInt(dist, 10)]; }); /** word -> shortest distance to POOP. This is "par". */ export const wordDist: Map<string, number> = new Map(distEntries); /** Every legal move, derived from the same table. */ export const allWords: Set<string> = new Set(distEntries.map(([w]) => w)); export function getDist(word: string): number { return wordDist.get(word.toLowerCase()) ?? -1; // -1 means unknown word } export function isWord(word: string): boolean { return allWords.has(word.toLowerCase()); } Validating a move is isWord, an O(1) Set lookup. Reading par is getDist, an O(1) Map lookup. The third job, showing a full shortest solution, is where the distance field pays off a second time. You do not need another search. From the start word, repeatedly step to any neighbor whose distance is one less than the current distance, until you reach POOP. TypeScript function solveShortestPath(start: string, target = "poop"): string[] { let current = start.toLowerCase(); const path = [current]; let dist = getDist(current); if (dist < 0) return path; // unknown word, no route while (current !== target && dist > 0) { const step = neighbors(current, allWords).find((n) => getDist(n) === dist - 1); if (!step) break; path.push(step); current = step; dist -= 1; } return path; } This greedy descent is always correct on a distance field, and that is worth stating precisely. Every node at distance d greater than zero has at least one neighbor at distance d - 1, because that is exactly how BFS assigned the labels. So a step down always exists, and the walk reaches zero in d steps. There is no queue and no visited set. The work is proportional to par, which caps near eleven, so a solution is effectively free to produce. Figure 3. One shortest solution, produced by stepping down the distance field one level at a time. A Daily Puzzle With No Database There is one more piece. The game is the same for everyone in the world on a given day, and it still has no backend. The puzzle is a pure function of the clock. Take whole days since a fixed epoch and use that integer both as the puzzle number and as the index into a list of starting words. TypeScript const DAY_MS = 86_400_000; const EPOCH_UTC = Date.UTC(2025, 7, 14, 8, 0, 0, 0); // 2025-08-14 08:00 UTC function daysSinceEpoch(nowMs = Date.now()): number { if (nowMs <= EPOCH_UTC) return 0; return Math.floor((nowMs - EPOCH_UTC) / DAY_MS); } function getStartWord(startWords: string[], dayIndex = daysSinceEpoch()): string { const len = startWords.length; return startWords[((dayIndex % len) + len) % len]; No database read, no per-user state, no synchronization. Two players who open the page at the same moment compute the same puzzle independently. The 08:00 UTC rollover is just the time component baked into the epoch. Because the result depends only on the date, the page is fully cacheable at the edge, which is what lets the whole game sit behind a CDN. Tradeoffs and Lessons Precompute when the target is fixed. The entire win comes from one constraint: every query ends at the same node. That lets a single backward search amortize across all future queries. If the target varied per day, you would rebuild the field per day, which is still cheap here but changes the calculus. A distance field beats path-in-queue BFS for repeated queries. The naive solver allocates a growing array per queued path and re-explores every time. The field uses one shared Map, and reconstruction is a greedy walk with O(par) memory. Keep the shipped data flat and parse it once. A word,distance table is trivial to generate, diff in version control, and parse into a Map and a Set at module load. There is no custom binary format to maintain. Mind the graph-construction cost if you scale up. Generating neighbors with per-position membership tests is O(N x L x 26) across the dictionary. At four letters and a 26-letter alphabet, that is nothing. For longer words or larger alphabets, the classic optimization is to bucket words by wildcard patterns such as *OOP, P*OP, PO*P, and POO*, so words sharing a bucket are neighbors. That builds adjacency in O(N x L) and is worth the switch only when the simple version starts to hurt. Guard the edges. Unknown words return a sentinel distance of -1 rather than throwing, and the descent has a natural termination because the distance strictly decreases. A small step cap is a cheap safety net against any future data inconsistency. Many shortest paths can exist. Several routes can tie for par. The greedy descent returns one valid par path, which is all the game needs to show, and scoring by step count treats every par route as equal. Where This Pattern Applies The technique generalizes to any setting where many shortest-path queries share a fixed endpoint over a static graph: Routing toward a single sink, such as a depot or an exit.Autocomplete ranking by edit distance to a fixed term.Game hint systems and grid flow fields, where a unit always heads toward one goal.Any repeated shortest-path query to a constant target where the graph rarely changes. The limits follow from the assumptions. The field assumes a fixed target and a static graph. Change the target or the word set, and you rebuild the field, which is a build-time cost rather than a request-time one. For variable targets, a bidirectional search or a small set of precomputed fields, one per target, keeps most of the benefit. The lesson that stuck with me is simple. When a search always ends in the same place, stop searching forward from the start. Search backward from the end once, write down the answer for every node, and let the runtime read instead of compute. You can see the result running live at Poople, where every par score and every shortest solution is a lookup into a table that was built before you ever opened the page.

By horus he
The Real-Time Revolution: Why Blockchain Needs Data Stream Processing
The Real-Time Revolution: Why Blockchain Needs Data Stream Processing

Blockchain is an extremely data-driven technology because its primary function is to store, verify, and coordinate independent records in a secure, distributed data network. Without this information, no transaction, smart contract execution, or network activity would be valid, and it could jeopardize the integrity of much larger functions of trust. The data coming into the blockchain affects the accuracy of the whole system. Blockchain is nothing without the data it connects to, so, as far as transparency, immutability, and safe decisions are concerned, data is the backbone of blockchain. Blockchain and data streaming are bringing unprecedented levels of security, transparency, and real-time mechanisms to move data across the digital world. Blockchain forms an unbreakable chain of trust through keeping decentralized records, and streaming data streamlines the process by allowing for insights when information is constantly flowing. These form the backbone of next-generation applications, unleashing innovation, scalability, and better decision-making across industries. Both blockchain and data streaming are independently large and powerful technologies as they exist in the present time. However, when combined, data streaming can amplify the potential impact of a blockchain solution. Real-Time Data Integration Data streaming platforms, such as Apache Kafka and Apache Flink, continuously process and deliver real-time data. When we integrate with blockchain, transactions can be updated instantly on the ledger, smart contracts can react to live data feeds, and delays can be reduced compared to batch processing. For example, we can visualize it as the IoT sensors streaming temperature data can trigger a blockchain-based smart contract in real time. Improved Scalability One major limitation of blockchain systems like Ethereum has been scalability. By leveraging data streaming, we can pre-process and filter large volumes of data before sending it to the blockchain. Can reduce unnecessary transactions that are stored on-chain, and, on top of that, offload heavy computation on-chain and push it to a stream processing engine that is available on data streaming platforms.This results in faster and more efficient blockchain performance. Enhanced Data Integrity and Trust Blockchain ensures immutability and transparency; on the other hand, data streaming ensures continuous data flow. As data stream processing enables continuous validation, filtering, and analysis of data elements before they are processed on the ledger, it enhances data integrity and trust in blockchain. Real-time processing helps identify anomalies in the data, prevent tampering, and ensure that only accurate, high-quality data enters the blockchain. Combining this provides a trusted, secure, and eventually transparent ecosystem in which information can be verified instantly and with confidence. We can consider a use case of supply chain tracking where real-time shipment data is streamed and permanently recorded. Better Event-Driven Architectures Blockchain systems can become more dynamic when combined with an event-driven streaming platform such as Confluent, Amazon Kinesis, or the open-source Apache Kafka. Smart contracts can act as automated responders to streamed events and can be enabled for automation across distributed systems, which finally reduces manual intervention. For example, a payment is automatically released when a delivery event is streamed and confirmed. Efficient Data Storage Strategy Not all data needs to be stored on-chain, which is expensive and slow, but by leveraging streaming platforms, we can store and process high-volume data off-chain. Streaming platforms can be integrated with streaming databases to store data already processed by stream engines. We can allow the Blockchain to store only critical summaries, hashes, or proofs, maintaining efficiency while ensuring verification. Real-Time Analytics and Monitoring Data stream processing facilitates real-time analytics and monitoring in blockchain by analyzing transaction data as it streams over the network. This enables organizations to detect suspicious activity, monitor system performance, and obtain real-time information on blockchain activity by analyzing transaction patterns. Transparency, responsiveness, and operational efficiency across blockchain ecosystems can be upgraded if we convert the raw data into actionable intelligence by integrating a real-time stream processing platform. Wrapping Up Combining these two technologies — data stream processing and blockchain — creates an ecosystem that blends real-time intelligence with secure, immutable record-keeping. Blockchain ensures transparency, trust, and data integrity, while stream processing powers instant analysis, continuous monitoring, and real-time decision-making based on that data. When combined, they improve power efficiency, enhance security, and enable scalable, data-driven applications. These technologies play an instrumental role in the construction of smarter, more intelligent systems that must respond to increase confidence among organizations relying on that real-time information.

By Gautam Goswami DZone Core CORE
Migrate a Hardcoded LangGraph Agent to LaunchDarkly AI Configs in 20 Minutes
Migrate a Hardcoded LangGraph Agent to LaunchDarkly AI Configs in 20 Minutes

In this tutorial, you’ll run a small LangGraph agent locally, then migrate its hardcoded prompts, model choice, and tools into LaunchDarkly AI Configs. After the migration, every prompt tweak, model swap, or tool change ships as a LaunchDarkly update instead of a code deploy. The migration takes about 20 minutes. When you finish, the codebase will: Pull its system prompt, model name, and parameters from a LaunchDarkly AI Config on every request.Load its Tavily search tool definition from the same Config instead of a hardcoded module-level list.Emit duration, token, success, and error metrics to LaunchDarkly on each user turn.Have one offline-eval dataset staged for pre-rollout regression testing in the LaunchDarkly Playground.Fail gracefully by falling back to the original hardcoded values if LaunchDarkly is unreachable.Run A/B tests on models, prompts, parameters, and tool sets by creating variations and targeting them at user segments. Tutorial Summary The agent you’ll run is the official langchain-ai/react-agent template: a single-node React agent that uses Claude Sonnet and a Tavily search tool. The migration will pull three files into LaunchDarkly: The prompt in prompts.py,The model name in context.py, andthe tool list in tools.py. The aiconfig-migrate agent skill completes the work in five stages (audit, wrap, move tools, instrument, and attach evaluators). It pauses at the end of each stage for you to review. The provider call and the routing logic stay where they are. react-agent is one LLM that decides, one ToolNode that runs the tools the LLM asks for, and one conditional edge that loops between them. When you add a second agent with a handoff, you move the topology into a LaunchDarkly Agent Graph. This is a reviewer’s workflow, not a coding exercise. You ask your agent to run the aiconfig-migrate skill, then read the diffs and verify the skill got the audit, fallback, and tool schemas right. Every code sample below is an example of what your agent should produce, not something you should copy and paste. If you’d rather compare your migration to a finished one, the aiconfig-migrate branch of launchdarkly-labs/react-agent is the reference end state for this tutorial: the five stages applied against the upstream template, with AI Config-driven model, prompt, and tool wiring already in place. Prerequisites You’ll need: Python 3.11 or higher with uvA LaunchDarkly account with an AI project and access to your LaunchDarkly SDK keyAn Anthropic API key for Claude SonnetA Tavily API key for the search toolClaude Code (or another Claude Agent SDK client) with the LaunchDarkly agent skills installed and the LaunchDarkly MCP server configured. If you haven’t used skills before, the agent skills quickstart completes the setup in under 10 minutes. Clone the hardcoded starting point: Shell git clone https://github.com/langchain-ai/react-agent cd react-agent uv sync cp .env.example .env Specify an ANTHROPIC_API_KEY and TAVILY_API_KEY in .env. Then identify what’s hardcoded. The aiconfig-migrate skill’s first step is a read-only audit. Knowing the shape from the beginning makes the audit output easier to read. Here’s a table of the hardcoded values in react-agent: TitleFile:lineCurrent valueSystem promptsrc/react_agent/prompts.py:3"You are a helpful AI assistant.\n\nSystem time: {system_time}"Default modelsrc/react_agent/context.py:25"anthropic/claude-sonnet-4-5-20250929"max_search_resultssrc/react_agent/context.py:3310Toolsrc/react_agent/tools.py:17Tavily search function.bind_tools(TOOLS)src/react_agent/graph.py:37Binds the module-level listToolNode(TOOLS)src/react_agent/graph.py:73Runs the same list Skill Stage 1: Audit the Hardcoded Values Open Claude Code inside the cloned repo and run: Plain Text Migrate this app to LaunchDarkly AI Configs using the aiconfig-migrate skill. The skill starts by performing a read-only audit. It scans for hardcoded model and prompt values, identifies your package manager and provider, and produces a structured summary. For react-agent, the summary will look similar to this example: Python Language: Python 3.11+ Package manager: uv LLM provider: LangChain (init_chat_model) -> Anthropic Existing LD SDK: none Target mode: agent (LangGraph custom StateGraph) Hardcoded targets: - src/react_agent/prompts.py:3 SYSTEM_PROMPT (templated with {system_time}) - src/react_agent/context.py:25 model = "anthropic/claude-sonnet-4-5-20250929" - src/react_agent/context.py:33 max_search_results = 10 - src/react_agent/tools.py:29 TOOLS = [search] - src/react_agent/graph.py:37 .bind_tools(TOOLS) - src/react_agent/graph.py:73 ToolNode(TOOLS) Proposed plan: - Single AI Config key `react-agent` in agent mode - Stage 3 (tools) required, one tool (search) with schema extracted from the function signature via StructuredTool.from_function - Stage 4 (tracking) inline via LangChain callback handler - Stage 5 (evals) attached programmatically via create_judge - Existing Context dataclass becomes the fallback shape The skill stops here. Reply “continue” (or whatever affirmative response is appropriate for your shape) to begin Stage 2. Audit Output Can Vary If your audit output doesn’t match this, don’t continue without making improvements. The skill is designed to adapt. Read what it produces, reconcile that output against the table in Step 1, and tell the skill where it’s wrong. Iterate until the audit output addresses all the hardcoded values in the table. Skill Stage 2: Wrap the Call in the AI SDK This is the first stage where the skill writes code. It installs the SDK, creates the AI Config in LaunchDarkly, rewrites the hardcoded prompt to Mustache syntax, and adds a new ld_client.py module. To read the finished file, visit ld_client.py. Three things to check in the diff: The fallback mirrors the audit exactly. Every value you captured in Step 1 appears in FALLBACK with the same model name, provider, instruction text, and knob values. A drifted fallback silently changes behavior when LaunchDarkly is unreachable. max_search_results belongs in ModelConfig(custom={...}), not parameters={...}. parameters is forwarded to the provider SDK, and Anthropic, OpenAI, and Gemini all reject unknown kwargs.Model construction goes through create_langchain_model(ai_config), not a hand-rolled init_chat_model or load_chat_model wrapper. Hand-rolled builders only pass the model name, so variation parameters such as temperature, max_tokens, and top_p silently drop. If the template’s utils.load_chat_model is still present, have the skill delete it.{{ system_time } interpolation goes through the SDK, not a manual .replace(). The fourth argument to agent_config(...) is {"system_time": system_time}. If you see .replace("{{ system_time }", ...) at the call site, the skill missed the built-in interpolation. Verify both paths run before continuing. The skill won’t move to Stage 3 until both work. Here’s how to do that: In one terminal, start the dev server with your SDK key: Shell LD_SDK_KEY=sdk-... uv run --with "langgraph-cli[inmem]" langgraph dev --no-browser In a second terminal, invoke the graph once via the local API: Shell curl -s http://127.0.0.1:2024/runs/wait \ -H "Content-Type: application/json" \ -d '{ "assistant_id": "agent", "input": {"messages": [{"role": "user", "content": "What is the weather in San Francisco?"}]} }' | jq '.messages[-1].content' A natural-language answer should appear. To make the LaunchDarkly-served path visually distinct from the fallback path, open the react-agent AI Config in LaunchDarkly, edit the default variation’s instructions, and append a sentence like: Always respond in over-the-top 1980s slang. Use words like “totally,” “rad,” “gnarly,” and “tubular.” Drop a “righteous!” somewhere. Save the variation, then re-run the curl command. Within a few seconds, you should see the answer come back with added 80s slang. That’s proof the LaunchDarkly-served prompt is winning over the hardcoded fallback. Next, stop the server, unset LD_SDK_KEY, restart it, and run the same curl call again. The slang should disappear, and the answer should read in the original neutral voice. That’s proof that the fallback, which still follows the pre-migration prompt exactly, runs when LaunchDarkly is unreachable. If you’d rather click through a chat UI, LangGraph Studio (free LangSmith login) and the hosted Agent Chat UI (point it at http://127.0.0.1:2024 with the graph id agent) both work against the same local server. Skill Stage 3: Move the Tool into the Config Stage 3 attaches the tool schema to the LaunchDarkly variation and rewires graph.py and tools.py to read the tool list from the AI Config using the skill’s tool factory pattern. Each tool is built by a factory that takes the per-run ai_config and returns a closure. The closure captures max_search_results, or any other model.custom knob, one time at the start of the turn, so the tool body never re-evaluates the AI Config. For the finished shape, visit tools.py and graph.py. The pattern, drawn verbatim from the reference repo: Python # Source of truth: launchdarkly-labs/react-agent@aiconfig-migrate src/react_agent/tools.py:15-42 def make_search(ai_config: AIAgentConfig) -> Callable[..., Any]: """Build a search tool that closes over this run's max_search_results. Capturing the value at run setup keeps it stable across the turn, so a mid-run flag flip won't change it between two tool calls. The tool body never re-evaluates the AI Config, which would emit an extra $ld:ai:agent_config event per tool call. """ max_results = ai_config.model.get_custom("max_search_results") or 10 async def search(query: str) -> dict: """Search for general web results. This function performs a search using the Tavily search engine, which is designed to provide comprehensive, accurate, and trusted results. It's particularly useful for answering questions about current events. """ return await TavilySearch(max_results=max_results).ainvoke({"query": query}) return search # Registry of tool factories keyed by the LD AI Tool name. Each factory takes # the per-run AI Config and returns the actual callable. graph.py materializes # this into {name: callable} on the first call_model tick. TOOL_FACTORIES: Dict[str, Callable[[AIAgentConfig], Callable[..., Any]]] = { "search": make_search, } graph.py materializes the factories inside call_model’s first-tick branch: built = {name: factory(ai_config) for name, factory in TOOL_FACTORIES.items()}, then update["tools"] = build_structured_tools(ai_config, built). Subsequent ticks read state.tools and pass it to create_langchain_model(ai_config).bind_tools(tools). For an exact sample, visit graph.py:50-63. Verify three things: The registry exports TOOL_FACTORIES and not a plain TOOL_REGISTRY of callables,Each factory returns a closure that reads model.custom values at construction time, not from inside the tool body, andbind_tools reads the materialized tool list off state instead of referencing the registry directly. build_structured_tools from ldai_langchain.langchain_helper wraps each built callable as a LangChain StructuredTool with the LD-served schema. Why the Factory Pattern Matters Reading ai_config.model.get_custom(...) from inside a tool body fires get_agent_config() on every tool invocation, inflating $ld:ai:agent_config event counts proportional to tool-call volume and letting a mid-turn flag change swap max_search_results between the first and second tool call. The factory captures the value one time at the start of the turn, preserves turn-level atomicity, and keeps agent_config evaluations at one per turn. Skill Stage 4: Wire the Tracker This is the stage where the graph topology changes. The migration adds a finalize node so every metric event for a user turn shares one runId, the unit LaunchDarkly bills and groups by in the Monitoring tab. A React agent turns loops through call_model several times to pick a tool, execute, and summarize. The at-most-once events, such as duration, tokens, success, and error, fire one time across that whole loop, not one time per tick. The three things to understand: Run-scoped state. On the first call_model tick of a turn, the migration resolves the AI Config, mints one tracker with ai_config.create_tracker(), materializes the tool factories into concrete callables, starts a perf_counter_ns timer, and stashes all of it on state. Every subsequent tick reuses what’s on state. The same tracker uses the same runId and results appear in one row per turn in Monitoring.Per-step events stay in call_model. tracker.track_tool_calls(...) is explicitly not at-most-once. It runs every tick that the LLM dispatches tools. Token usage accumulates into Annotated[int, add] state fields across ticks.Run-level events move to a new finalize node. track_duration, track_tokens, track_success, and track_error all fire there, one time per turn, reading totals off state. Read state.py for the run-scoped fields (ai_config, tracker, tools, start_perf_ns, three token counters, errored) and graph.py for the lazy-init prelude in call_model, the finalize node, and other details. Two SDK Details You Should Know ai_config.create_tracker() is a factory method as of launchdarkly-server-sdk-ai 0.18.0. If your skill emits ai_config.tracker instead of ai_config.create_tracker, regenerate. This migration workflow uses get_ai_usage_from_response rather than get_ai_metrics_from_response so the graph can accumulate tokens across ticks into state fields rather than tracking them synchronously per-call. Test this yourself by sending one request through the graph, then opening the AI Config in LaunchDarkly and reviewing the Monitoring tab. Within one or two minutes, you should see one row per user question with non-zero duration and token counts. If the tab fills up with multiple rows per question, the skill minted a tracker inside call_model instead of threading one through state. The Monitoring tab shows duration, token, and generation metrics for a migrated AI Config. Two Simplifications Compared to the Skill This repo collapses the setup steps of resolving the config, minting the tracker, and building the tools into the first tick of call_model instead of a dedicated setup_run node. It also skips track_metrics_of_async around ainvoke, which would fire duration and success per call rather than per turn. This helps produce a legible code diff, but production code should follow the skills setup_run and finalize factoring. If your app has a thumbs-up/down UI, the skill will also wire tracker.track_feedback(...). Feedback usually arrives in a later request from a different process, so pass tracker.resumption_token out to your frontend at call time and rebuild the tracker with LDAIClient.create_tracker(token, context) in the feedback handler. react-agent doesn’t have a feedback UI, so we’ve intentionally skipped this step. Keep Going The migration is done. The payoff is what you can do next without another code deploy: Reference implementation. Diff your own run against launchdarkly-labs/react-agent on the aiconfig-migrate branch to validate fallback shape, tool wiring, and tracker placement.Regression-test before rollout. Agent-mode Configs don’t support UI-attached automatic judges, so run an offline evaluation against a fixed dataset. The skill generates a starter datasets/react-agent-tests.csv from your audit; take it to the Offline Evaluation of RAG-Grounded Answers tutorial. The Accuracy judge at threshold 0.85, on a different model family than the agent, is the right starting point.Zero-code changes in production. Swap models per cohort, A/B test prompts or tool sets on 50/50 traffic, disable a tool for a segment, or watch duration, token spend, and eval scores land in the Monitoring tab in real time. All from the LaunchDarkly UI.Scale to a second agent. The moment you add a supervisor plus specialists or any routing handoff, move the topology itself into LaunchDarkly via ai_client.agent_graph("key", ld_context). The Beyond n8n tutorial walks the full pattern, and launchdarkly-labs/devrel-agents-tutorial (agent-skills branch) is the production-grade reference with three agents, per-user targeting, and dynamic routing.

By Scarlett Attensil
Alternative Structured Concurrency
Alternative Structured Concurrency

Java structured concurrency has been under development for a span of 5 years, weaving through 8 (!) distinct JEPs (JEP 428, JEP 437, JEP 453, JEP 462, JEP 480, JEP 499, JEP 505, JEP 525). To me, this feels rather excessive for what could be considered a fairly concise feature. My goal here is to experiment with an alternative approach that leverages Java's tried-and-tested, robust functionality available since JDK 1.5. It's possible this pathway could achieve better outcomes than what is proposed in JEP 505, which, from my perspective, introduces a suite of redundant interfaces and classes that replicate pre-existing ones. No doubt, developers need some governance, even in a relatively safe development environment like Java, with its automatic garbage collection, memory management, and strict typing. No matter how safe the provided path is, developers will still make mistakes, such as dereferencing nulls, using out-of-bound indexes, swallowing exceptions, and who knows what else. And, undoubtedly, concurrency is the hardest thing to get right — it's an endless source of bugs. But first, let me introduce some helper code that we will use throughout the article. Java // Example Proto package net.tascalate.concurrentx; // imports here public class FuturesDemo { static final ScopedValue<String> DEMO_SV = ScopedValue.newInstance(); // This emulates long-running calls // we need to execute asynchronously -- // all we do is returning value after the delay // or throw a supplied exception to emulate error private static <T> Callable<T> produceValue(T value, long delay) { return () -> { var start = System.currentTimeMillis(); try { System.out.println(">> Waiting value: " + value + " (SCOPED VALIUE IS " + DEMO_SV.orElse("<UNBOUND>") + ")"); Thread.sleep(delay); System.out.println(">> Producing value: " + value); if (value instanceof Exception) { throw (Exception)value; } else { return value; } } finally { var finish = System.currentTimeMillis(); System.out.println(">> Exiting " + value + ", " + Thread.currentThread() + ", done in " + (finish - start) + "ms, vs " + delay + "ms specified"); } }; } public static void main(String[] argv) { // implementation will be here } } According to Oracle, the majority of Java developers tend to approach concurrency execution in the following way (excerpt courtesy JEP 505, modified to use a helper code from above): Java // Example A - "unstructured concurrency" public static void main(String[] argv) throws InterruptedException, ExecutionException { var executor = Executors.newVirtualThreadPerTaskExecutor(); var start = System.currentTimeMillis(); try { Future<String> a = executor.submit( produceValue("A", 1000)); Future<LocalDateTime> b = executor.submit( produceValue(LocalDateTime.now(), 1500)); Future<BigInteger> c = executor.submit( produceValue(BigInteger.valueOf(42), 500)); var result = List.of(a.get(), b.get(), c.get()); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); executor.shutdownNow(); } } Here, a range of critical problems lurk, several of which are detailed in the "Motivation" section of the JEP: In contrast to the above example, Oracle proposes the use of its structured concurrency API as a solution that, hypothetically, addresses these concerns: Java // Example B -- structured concurrency @SuppressWarnings("preview") public static void main(String[] argv) throws InterruptedException, ExecutionException { var start = System.currentTimeMillis(); try (var scope = StructuredTaskScope.open( StructuredTaskScope.Joiner.allSuccessfulOrThrow())) { var a = scope.fork(produceValue("A", 1000)); var b = scope.fork(produceValue(LocalDateTime.now(), 1500)); var c = scope.fork(produceValue(BigInteger.valueOf(42), 500)); scope.join(); var result = List.of(a.get(), b.get(), c.get()); System.out.println("*** ALL result: " + result); } catch (StructuredTaskScope.FailedException ex) { System.out.println("*** ALL exception: " + ex.getCause()); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } } Let’s shift our focus back to the original code. After putting in diligent QA efforts, writing useful tests with good code coverage, and completing a thorough code review, what’s the developer’s next move? Most likely, they'll refine the initial code block to resemble the updated version below: Java // Example C - fixed "unstructured concurrency" from Example A public static void main(String[] argv) throws InterruptedException, ExecutionException { Future<String> a = null; Future<LocalDateTime> b = null; Future<BigInteger> c = null; var executor = Executors.newVirtualThreadPerTaskExecutor(); var start = System.currentTimeMillis(); try { a = executor.submit(produceValue("A", 1000)); b = executor.submit(produceValue(LocalDateTime.now(), 1500)); c = executor.submit(produceValue(BigInteger.valueOf(42), 500)); var result = List.of(a.get(), b.get(), c.get()); System.out.println("ALL result: " + result); } finally { var finish = System.currentTimeMillis(); Stream.of(a, b, c) .filter(Objects::nonNull) .forEach(f -> f.cancel(true)); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); executor.shutdownNow(); } } At a glance, this approach seems fairly effective — any remaining Features are canceled in the instance of an intermediate error, and all execution threads are properly terminated. However, there's still a fair amount of boilerplate code, which remains cumbersome to implement consistently. No problem, let's extract common functionality into some reusable class. Please see the TaskScope class in the Gist. By doing so, the code undergoes a noticeable transformation: Java // Example D - fixed "unstructured concurrency" from Example A // with a reusable TaskScope class public static void main(String[] argv) throws InterruptedException, ExecutionException { var start = System.currentTimeMillis(); try (var scope = new TaskScope( Executors.newVirtualThreadPerTaskExecutor())) { var a = scope.fork(produceValue("A", 1000)); var b = scope.fork(produceValue(LocalDateTime.now(), 1500)); var c = scope.fork(produceValue(BigInteger.valueOf(42), 500)); var result = List.of(a.get(), b.get(), c.get()); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } } Upon inspecting the Gist sources — which you absolutely should for understanding — you’ll notice something important: this implementation relies on Java version 1.8, released over 12 years ago. Furthermore, if it does not use java/util/stream/Stream, it can even run seamlessly on JDK 1.5! But hold on — why incorporate java/util/stream/Stream here? Quite frankly, it's the core of the proposal. Take example D above: it efficiently handles just one scenario, namely, waiting for all tasks to finish while throwing an error if any fail along the way. Support for different scenarios requires something a bit more sophisticated. The TaskScope implementation shared in the Gist translates a queue of completed Futures (irrespective of whether completion came via a result, error, or cancellation) directly into a Stream. Curious why this may be useful? Let's rewrite this boring example once again: Java // Example E - same as Example D but with Stream pipeline public static void main(String[] argv) { var start = System.currentTimeMillis(); try (var scope = new TaskScope( Executors.newVirtualThreadPerTaskExecutor())) { scope.fork(produceValue("A", 1000)); scope.fork(produceValue(LocalDateTime.now(), 1500)); scope.fork(produceValue(BigInteger.valueOf(42), 500)); var result = scope.completions() .map(Future::resultNow) .toList(); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } } This way, we just convert all the completed features into the list of results and keep our fingers crossed that there were no errors. Let’s turn all successfully completed futures into a result list, disregarding potential errors entirely. No exceptions will ever be thrown within this scope: Java var result = scope.completions() .filter(f -> f.state() == Future.State.SUCCESS) .map(Future::resultNow) .toList(); Or simply find the first result available: Java var result = scope.completions() .filter(f -> f.state() == Future.State.SUCCESS) .map(Future::resultNow) .findAny() .orElse("<NONE>"); Or, alternatively, select no more than the first N results: Java var N = 5; var result = scope.completions() .filter(f -> f.state() == Future.State.SUCCESS) .map(Future::resultNow) .limit(N) .toList(); In these two recent examples, any remaining futures will automatically be terminated once the try-with-resources block in the main method exits. Clearly, we can also handle errors while gathering results and terminate prematurely — if the code logic doesn't permit intermediate errors: Java var result = scope.completions() .peek(f -> { if (f.state() == Future.State.FAILED) throw new CompletionException(f.exceptionNow()); }) .map(Future::resultNow) .limit(2) .toList(); If you're already acquainted with JEP 505, you’ll understand what is being replaced here: StructuredTaskScope.Joiner. Now, you can mimic any type of "join" behavior without the need to subclass/implement StructuredTaskScope.Joiner. The Stream pipeline API over the completions queue serves as an expressive tool to achieve this out of the box. Plus, with the introduction of Gatherers, there’s room for truly ad hoc scenarios, such as managing result windows — think fixed-size batches of completed results processed as soon as they are ready. It’s also worth noting that in JEP 505, a certain StructuredTaskScope.Joiner implementations produce streams as their output. However, it’s the Joiner that determines when all forks have finished processing and opens the resulting stream post-join. In the alternative methodology described here, the decision of where and how joins occur resides within user-defined scope-flow logic. It’s a lazy, on-demand process — guided by conditions that may take more into account than just Future results. For instance, elements like internal object state or in-scope variables can directly influence decisions about which results to collect and which errors, if any, can be disregarded in the operation. Now to the real challenge. A notable limitation with the code given is its inability to propagate context, namely, the current ScopedValue-s bindings. This characteristic is sometimes cited as a primary strength of JEP 505 StructuredTaskScope. To be fair, one might argue it's an unfair advantage, one that exists solely because JDK-internal mechanisms make it achievable. Current bindings are captured and propagated by using jdk/internal/misc/ThreadFlock — a utility inaccessible to code outside of the JDK. Perhaps, in a more ideal universe, there is a JDK 25, equipped with the following official API for java/util/concurrent/ThreadFactory, introducing possibilities for bridging this gap: Java public interface ThreadFactory { abstract Thread newThread(Runnable code); default ThreadFactory captureContext() { ThreadFactory delegate = this; Object currentScopedValueBindings = SomeInternalClass.captureValueBindingsForTheCurrentThread(); return new ThreadFactory() { public Thread newThread(Runnable code) { Thread result = delegate.newThread(code); SomeInternalClass.applyValueBindings(result); return result; } }; } } But that's not the case for us. Thankfully, the classes from the java/util/concurrent package offer immense customizability and are remarkably adaptable tools (a big nod to Dr. Douglas S. Lea for this). So you can find another class, TaskScopeContextual, in the same Gist. This class adopts StructuredTaskScope to the ExecutorService API, solely aimed at promoting ScopedValue bindings for forked tasks. The following example highlights all the advantages of employing this alternative structured scope design: Java // Example F - true structured concurrency with context passing public static void main(String[] argv) { var start = System.currentTimeMillis(); ScopedValue.where(DEMO_SV, "VALUE_DEFINED_IN_MAIN").call(() -> { try (var scope = new TaskScopeContextual()) { scope.fork(produceValue("A", 1000)); scope.fork(produceValue("B", 2000)); scope.fork(produceValue("C", 2000)); scope.fork(produceValue("D", 2000)); var timeout = scope.fork(produceValue(null, 2500)); scope.fork(produceValue("E", 2000)); scope.fork(produceValue("F", 3000)); scope.fork(produceValue("G", 3000)); var result = scope.completions() .takeWhile(f -> f != timeout) .filter(f -> f.state() == Future.State.SUCCESS) .limit(6) .map(Future::resultNow) .sorted() .toList(); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } return null; }); } Take note of the elegant handling of timeouts with Streams. Unlike the current approach in JEP 505, there's no necessity to incorporate it into the API. In summary, here’s a recap: There's no requirement for StructuredTaskScope.Subtask — the existing java/util/concurrent/Future API already does the job adequately. Consequently, the inclusion of StructuredTaskScope.Subtask.State is redundant — even with the current JEP 505, Future.State is more than sufficient. StructuredTaskScope.Joiners demand subclassing for all but the simplest cases. A java/util/stream/Stream pipeline over the completed futures would serve as a much more convenient solution. The StructuredTaskScope.FailedException feels unnecessary — even in the current API, java/util/concurrent/CompletionException fulfills the same purpose just fine. Built-in StructuredTaskScope timeouts possess timing characteristics that are challenging to predict (e.g., try adding lengthy blocking calls before the initial fork). It's far simpler and more controlled to handle timeouts explicitly. I'm really interested to hear readers' opinions. Do you share my ideas or do you support JDK team's statement that Futures "are counterproductive in structured concurrency" (see the "Alternatives" section of JEP 505)? Would you say that the well-known and adaptable Stream API is superior to Joiners or strict set of Joiners is simpler?

By Valery Silaev
Building Enterprise-Grade Real-Time IoT Dashboards with Vue 3, MQTT, and Kafka
Building Enterprise-Grade Real-Time IoT Dashboards with Vue 3, MQTT, and Kafka

The convergence of IoT, real-time data streaming, and modern frontend frameworks is redefining how engineers build enterprise monitoring systems. Over the course of designing and leading the Device IoT Platform — an enterprise-grade solution for real-time monitoring, configuration, and diagnostics of thousands of distributed network devices — I encountered and solved a core architectural challenge: how do you build a frontend dashboard that can handle hundreds of concurrent device telemetry streams without sacrificing performance, maintainability, or user experience? This article shares the architectural patterns, technology decisions, and hard-won lessons from that journey — covering the full stack from MQTT ingestion to Vue 3 reactivity to Kafka-backed event processing. The Core Problem: Real-Time at Scale Most developers are familiar with polling — periodically fetching data from an API endpoint. For IoT, polling is fundamentally broken: Latency: A 5-second polling interval means 5 seconds of stale state.Inefficiency: You're requesting data even when nothing has changed.Scale: 1,000 devices × 1 request/5s = 200 requests/second just to read status — before any user interaction. The solution is event-driven architecture: devices push telemetry when something changes, and the platform reacts. This requires a rethinking of both backend ingestion and frontend state management. Architecture Overview Plain Text [IoT Devices] | MQTT Broker (Mosquitto / AWS IoT Core) | [Node.js Telemetry Microservice] | [Kafka Topic: device.telemetry.raw] | (stream processor) [Kafka Topic: device.telemetry.enriched] | [WebSocket Server (Node.js)] | [Vue 3 Dashboard Frontend] Each layer has a distinct responsibility: MQTT Broker handles lightweight publish/subscribe with devices using minimal overhead.Node.js microservices bridge MQTT → Kafka, performing initial validation and normalization.Kafka provides durable, replayable event streams — critical for audit trails and late-joining consumers.WebSocket server fans out enriched telemetry to connected dashboard clients in real time.Vue 3 handles reactive rendering, ensuring only the affected UI components re-render when new data arrives. Backend: MQTT → Kafka Bridge in Node.js The heart of the ingestion pipeline is a lightweight Node.js service using the mqtt and kafkajs libraries. Plain Text import mqtt from 'mqtt'; import { Kafka } from 'kafkajs'; const mqttClient = mqtt.connect(process.env.MQTT_BROKER_URL!, { clientId: `telemetry-bridge-${process.pid}`, username: process.env.MQTT_USERNAME, password: process.env.MQTT_PASSWORD, clean: true, }); const kafka = new Kafka({ clientId: 'iot-bridge', brokers: [process.env.KAFKA_BROKER!] }); const producer = kafka.producer(); mqttClient.on('connect', async () => { await producer.connect(); mqttClient.subscribe('devices/+/telemetry', { qos: 1 }); console.log('MQTT → Kafka bridge active'); }); mqttClient.on('message', async (topic, payload) => { const deviceId = topic.split('/')[1]; const data = JSON.parse(payload.toString()); await producer.send({ topic: 'device.telemetry.raw', messages: [ { key: deviceId, value: JSON.stringify({ deviceId, timestamp: Date.now(), ...data }), }, ], }); }); Key design decisions here: QoS Level 1 — ensures at-least-once delivery for telemetry messages. For command acknowledgments, we use QoS 2.Device ID as Kafka partition key — guarantees ordering per device while allowing parallel processing across partitions.Separation of raw vs. enriched topics — the device.telemetry.raw topic contains the bare payload; a downstream stream processor enriches it with device metadata, geolocation, and alert thresholds before publishing to device.telemetry.enriched. WebSocket Fan-Out Server The WebSocket layer subscribes to Kafka's enriched topic and pushes updates to connected browser clients. We use Kafka consumer groups to allow horizontal scaling of the WebSocket tier. Plain Text import { WebSocketServer } from 'ws'; import { Kafka } from 'kafkajs'; const wss = new WebSocketServer({ port: 8080 }); const kafka = new Kafka({ clientId: 'ws-fanout', brokers: [process.env.KAFKA_BROKER!] }); const consumer = kafka.consumer({ groupId: 'websocket-fanout-group' }); // Track subscriptions: deviceId → Set<WebSocket> const deviceSubscriptions = new Map<string, Set<WebSocket>>(); wss.on('connection', (ws) => { ws.on('message', (msg) => { const { action, deviceId } = JSON.parse(msg.toString()); if (action === 'subscribe') { if (!deviceSubscriptions.has(deviceId)) { deviceSubscriptions.set(deviceId, new Set()); } deviceSubscriptions.get(deviceId)!.add(ws); } }); ws.on('close', () => { deviceSubscriptions.forEach((clients) => clients.delete(ws)); }); }); async function startKafkaConsumer() { await consumer.connect(); await consumer.subscribe({ topic: 'device.telemetry.enriched' }); await consumer.run({ eachMessage: async ({ message }) => { const payload = JSON.parse(message.value!.toString()); const clients = deviceSubscriptions.get(payload.deviceId); clients?.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(payload)); } }); }, }); } startKafkaConsumer(); This design enables selective subscription — a dashboard user viewing 50 devices only receives telemetry for those 50 devices, not the full firehose. This is critical for performance at scale. Frontend: Vue 3 Reactive Architecture The frontend is built with Vue 3 Composition API + Pinia for state management. The goal is to update only the UI components bound to a specific device when its telemetry arrives — not re-render the entire dashboard. WebSocket Composable Plain Text // composables/useDeviceTelemetry.ts import { ref, onMounted, onUnmounted } from 'vue'; import { useDeviceStore } from '@/stores/deviceStore'; export function useDeviceTelemetry(deviceIds: string[]) { const store = useDeviceStore(); let ws: WebSocket | null = null; const connect = () => { ws = new WebSocket(import.meta.env.VITE_WS_URL); ws.onopen = () => { deviceIds.forEach((id) => { ws!.send(JSON.stringify({ action: 'subscribe', deviceId: id })); }); }; ws.onmessage = (event) => { const telemetry = JSON.parse(event.data); store.updateDeviceTelemetry(telemetry.deviceId, telemetry); }; ws.onclose = () => { // Exponential backoff reconnection setTimeout(connect, Math.min(1000 * 2 ** reconnectAttempts++, 30000)); }; }; onMounted(connect); onUnmounted(() => ws?.close()); } Pinia Store with Fine-Grained Reactivity Plain Text // stores/deviceStore.ts import { defineStore } from 'pinia'; import { reactive } from 'vue'; interface DeviceTelemetry { deviceId: string; status: 'online' | 'offline' | 'degraded'; signalStrength: number; latency: number; lastSeen: number; alerts: string[]; } export const useDeviceStore = defineStore('devices', () => { const telemetryMap = reactive<Record<string, DeviceTelemetry>>({}); function updateDeviceTelemetry(deviceId: string, data: Partial<DeviceTelemetry>) { if (!telemetryMap[deviceId]) { telemetryMap[deviceId] = {} as DeviceTelemetry; } Object.assign(telemetryMap[deviceId], data); } return { telemetryMap, updateDeviceTelemetry }; }); Using reactive() with a map structure means Vue's dependency tracking is at the property level — a component subscribed to telemetryMap['device-001'].signalStrength won't re-render when device-002's data changes. This is the key to dashboard scalability. Device Card Component Plain Text <!-- components/DeviceCard.vue --> <template> <div class="device-card" :class="statusClass"> <div class="device-header"> <span class="device-id">{{ deviceId }</span> <StatusBadge :status="telemetry?.status" /> </div> <div class="metrics"> <MetricBar label="Signal" :value="telemetry?.signalStrength" unit="dBm" /> <MetricBar label="Latency" :value="telemetry?.latency" unit="ms" /> </div> <AlertList :alerts="telemetry?.alerts ?? []" /> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { useDeviceStore } from '@/stores/deviceStore'; const props = defineProps<{ deviceId: string }>(); const store = useDeviceStore(); // Only this device's slice of state — targeted re-renders only const telemetry = computed(() => store.telemetryMap[props.deviceId]); const statusClass = computed(() => ({ 'status-online': telemetry.value?.status === 'online', 'status-offline': telemetry.value?.status === 'offline', 'status-degraded': telemetry.value?.status === 'degraded', })); </script> Performance Optimizations 1. Virtual Scrolling for Large Device Lists When monitoring 500+ devices, rendering all device cards simultaneously tanks performance. We use vue-virtual-scrollerto only render visible cards: Plain Text <RecycleScroller class="device-list" :items="filteredDevices" :item-size="120" key-field="deviceId" v-slot="{ item }" > <DeviceCard :device-id="item.deviceId" /> </RecycleScroller> 2. Debounced Batch Updates Devices can emit bursts of telemetry. Updating the Pinia store on every single message causes excessive re-renders. We batch incoming messages within a 100ms window: Plain Text let pendingUpdates: Record<string, Partial<DeviceTelemetry>> = {}; let batchTimeout: ReturnType<typeof setTimeout> | null = null; function queueUpdate(deviceId: string, data: Partial<DeviceTelemetry>) { pendingUpdates[deviceId] = { ...(pendingUpdates[deviceId] ?? {}), ...data }; if (!batchTimeout) { batchTimeout = setTimeout(() => { Object.entries(pendingUpdates).forEach(([id, update]) => { store.updateDeviceTelemetry(id, update); }); pendingUpdates = {}; batchTimeout = null; }, 100); } } 3. Lazy Loading and Code Splitting Device diagnostic panels (charts, event logs, configuration editors) are loaded on demand: Plain Text const DeviceDiagnostics = defineAsyncComponent( () => import('@/components/DeviceDiagnostics.vue') ); Combined with route-level code splitting, the initial bundle stays under 200KB gzipped. Security Architecture: OAuth 2.0 + RBAC Device management platforms require fine-grained access control. Not every user should be able to issue firmware update commands to production devices. JWT Claims-Based RBAC We encode role information directly in the JWT access token: Plain Text { "sub": "user-123", "roles": ["device:read", "device:configure"], "scope": "region:us-east", "exp": 1699999999 } The frontend reads these claims to conditionally render action buttons, and the backend validates them on every API call: Plain Text // middleware/rbac.ts export function requirePermission(permission: string) { return (req: Request, res: Response, next: NextFunction) => { const token = req.headers.authorization?.split(' ')[1]; const decoded = verifyJWT(token!); if (!decoded.roles.includes(permission)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; } // Route definition router.post('/devices/:id/firmware', requirePermission('device:firmware'), handleFirmwareUpdate); Deployment: CI/CD on AWS The entire platform is containerized and deployed via a GitLab CI/CD pipeline to AWS ECS with Fargate. Plain Text # .gitlab-ci.yml (excerpt) stages: - test - build - deploy build-and-push: stage: build script: - docker build -t $ECR_REGISTRY/iot-frontend:$CI_COMMIT_SHA . - docker push $ECR_REGISTRY/iot-frontend:$CI_COMMIT_SHA deploy-production: stage: deploy script: - aws ecs update-service --cluster iot-platform --service frontend --force-new-deployment environment: production only: - main Blue-green deployments ensure zero downtime for this 24/7 critical infrastructure platform. Results and Key Metrics After migrating from a polling-based architecture to this event-driven stack: Dashboard latency: reduced from 5–10 seconds (polling) to under 200ms (WebSocket push).Backend API load: reduced by ~78% — telemetry pushes replaced constant polling.Frontend bundle size: kept under 220KB gzipped through lazy loading and tree-shaking.Throughput: validated at 10,000 concurrent telemetry events/second through Kafka partitioning. Conclusion Building a real-time IoT dashboard at enterprise scale requires rethinking the entire data flow — from device communication protocols through streaming pipelines to fine-grained frontend reactivity. The combination of MQTT for lightweight device communication, Kafka for durable event streaming, WebSockets for real-time push to browsers, and Vue 3's targeted reactivity model creates a system that scales gracefully without sacrificing developer ergonomics. The patterns described here — selective WebSocket subscriptions, batched Pinia updates, virtual scrolling, and JWT-based RBAC — have been validated in production on a platform serving critical network infrastructure. They are broadly applicable to any domain requiring real-time monitoring at scale: energy management, fleet tracking, industrial automation, and beyond. Github: Real-Time-IoT-Dashboards-Vue-3-MQTT-Kafka

By Venkata Sandeep Dhullipalla

Monthly Top JavaScript Experts

expert thumbnail

John Vester

Senior Staff Engineer,
Marqeta

IT professional with 30+ years expertise in app design and architecture, feature development, and project and team management. Currently focusing on establishing resilient cloud-based services running across multiple regions and zones. Additional expertise architecting (Spring Boot) Java and .NET APIs against leading client frameworks, CRM design, and Salesforce integration.
expert thumbnail

Justin Albano

Software Engineer,
IBM

I am devoted to continuously learning and improving as a software developer and sharing my experience with others in order to improve their expertise. I am also dedicated to personal and professional growth through diligent studying, discipline, and meaningful professional relationships. When not writing, I can be found playing hockey, practicing Brazilian Jiu-jitsu, watching the NJ Devils, reading, writing, or drawing. ~II Timothy 1:7~ Twitter: @justinmalbano

The Latest JavaScript Topics

article thumbnail
Node.js Microservices Architecture: A Complete Guide
This guide walks you through the core architecture components and design patterns needed to build scalable microservices with Node.js and explains when to use each.
September 2, 2026
by Megha Verma
· 1,507 Views · 1 Like
article thumbnail
The Code-Volume Delusion: Rethinking Engineering Velocity in the AI Era
AI is shifting the engineering bottleneck downstream, requiring leaders to prioritize PR cycle times, CI/CD stability, and architectural health.
August 25, 2026
by Rupesh Dabbir
· 2,402 Views
article thumbnail
Text Analysis Without a Backend: Replacing an LLM Call With Intl.Segmenter and 60 Lines of JavaScript
Build deterministic browser-based text tools instead of LLM APIs to reduce cost, latency, privacy risks, and nondeterministic results.
August 24, 2026
by Kevin Brown
· 1,902 Views
article thumbnail
Stop Hand-Rolling Chat UIs: Streaming LLM Tokens Into React Native Without the Jank
In a React Native chat, the keyboard and a streaming, resizing list fight over the scroll position and cause jank. KeyboardChatScrollView fixes it.
August 20, 2026
by Tammo Ronke
· 1,724 Views
article thumbnail
A Framework-Agnostic Approach to SSR for Microfrontends
Framework-agnostic SSR for independently deployed microfrontends — without a shared build, central orchestrator, or framework lock-in.
August 11, 2026
by Vitaly Zheltko
· 1,586 Views · 1 Like
article thumbnail
How We Cut PyFlink Pipeline p99 Latency from 3-5 Seconds to ~500ms
We eliminated per-record Python-side Protobuf parsing and JVM-to-Python crossings by letting Flink's native Protobuf format decode records directly into typed columns.
August 7, 2026
by Arjun Shah
· 2,038 Views · 1 Like
article thumbnail
Refresh Token Rotation in Node.js: Stopping Token Theft Without Logging Users Out
Implementing refresh token rotation with reuse detection, a pattern that limits the damage of a stolen token while keeping legitimate users logged in.
July 22, 2026
by Bilal Azam
· 2,714 Views
article thumbnail
React 19 Killed Half My Performance Optimization Code, and I'm Grateful
React 19's compiler eliminated most of my useMemo and useCallback code. Table virtualization, optimistic updates, and route splitting still need manual attention.
July 22, 2026
by Rohit G
· 5,093 Views · 1 Like
article thumbnail
Add Observability to Your React Native Application in 5 Minutes
A five-minute walkthrough for adding logs, traces, and error monitoring to a React Native iOS app using LaunchDarkly's Observability SDK, shown on a simple counter app.
July 6, 2026
by Alexis Roberson
· 1,560 Views · 5 Likes
article thumbnail
Dead Letter Queue Patterns in Apache Flink: Handling Poison Messages Without Stopping Your Stream
A poison message can trap a Flink job in a restart loop. Use side outputs, retries, tiered DLQs, durable sinks, and replay jobs to keep the stream running.
July 2, 2026
by Rohit Muthyala
· 3,230 Views
article thumbnail
AI-Augmented React Development: How I Rebuilt My Workflow Without Losing Control of the Code
AI accelerates React 18 workflows but breaks down in large enterprise codebases. Here’s where it helps, where it fails, and the guardrails your team needs.
July 1, 2026
by Sathwik Nagulapati
· 2,771 Views · 1 Like
article thumbnail
Fix the Target, Precompute Once: A Backend-Free Word-Ladder Solver With a BFS Distance Field
Every word ladder ends at the same word. One offline BFS precomputes a distance field, making par and shortest-path queries O(1) lookups, no backend.
June 22, 2026
by horus he
· 1,408 Views · 1 Like
article thumbnail
The Real-Time Revolution: Why Blockchain Needs Data Stream Processing
Blockchain and data streaming are bringing unprecedented levels of security, transparency, and real-time mechanisms to move data across the digital world.
June 17, 2026
by Gautam Goswami DZone Core CORE
· 1,954 Views · 1 Like
article thumbnail
Migrate a Hardcoded LangGraph Agent to LaunchDarkly AI Configs in 20 Minutes
Moving a hardcoded LangGraph React agent into LaunchDarkly AI Configs so prompts, models, tools, tracking, and rollout testing can be changed without redeploying.
June 2, 2026
by Scarlett Attensil
· 2,635 Views
article thumbnail
Alternative Structured Concurrency
My goal here is to experiment with an alternative approach leveraging Java's tried-and-tested, robust functionalities that have been available since JDK 1.5.
June 2, 2026
by Valery Silaev
· 2,625 Views
article thumbnail
Building Enterprise-Grade Real-Time IoT Dashboards with Vue 3, MQTT, and Kafka
Event-driven architecture using MQTT (device communication) → Kafka (durable streams) → WebSocket (browser push) → Vue 3 (reactive UI).
May 26, 2026
by Venkata Sandeep Dhullipalla
· 3,054 Views
article thumbnail
Zone-Free Angular: Unlocking High-Performance Change Detection With Signals and Modern Reactivity
Angular v21 ditches Zone.js signals trigger rendering directly, toSignal() bridges Observables, markForCheck() fills the gaps.
May 20, 2026
by Bhanu Sekhar Guttikonda DZone Core CORE
· 2,593 Views · 2 Likes
article thumbnail
Stop Writing Dialect-Specific SQL: A Unified Query Builder for Node.js
Learn how sql-flex-query lets you write once, run anywhere — with full TypeScript support and zero runtime overhead. Support SQL queries for multiple databases
May 20, 2026
by Ashish Lohia
· 2,357 Views
article thumbnail
Lambda-Driven API Design: Building Composable Node.js Endpoints With Functional Primitives
Lambda handlers are just functions that normalize once, wrap cross-cutting concerns as higher-order functions, and keep business logic clean.
May 19, 2026
by Bhanu Sekhar Guttikonda DZone Core CORE
· 3,692 Views · 4 Likes
article thumbnail
When Angular APIs Return 200 but the Frontend Is Already Failing Users
HTTP 200 can lie, validate payloads in your RxJS pipe, convert failures to real errors, and never let shareReplay cache bad data permanently.
May 8, 2026
by Bhanu Sekhar Guttikonda DZone Core CORE
· 2,526 Views · 2 Likes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • ...
  • Next
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

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

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

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

Let's be friends:

  • RSS
  • X
  • Facebook
×