Integration refers to the process of combining software parts (or subsystems) into one system. An integration framework is a lightweight utility that provides libraries and standardized methods to coordinate messaging among different technologies. As software connects the world in increasingly more complex ways, integration makes it all possible facilitating app-to-app communication. Learn more about this necessity for modern software development by keeping a pulse on the industry topics such as integrated development environments, API best practices, service-oriented architecture, enterprise service buses, communication architectures, integration testing, and more.
How to Marry MDC With Spring Integration
Integrating Selenium With Amazon S3 for Test Artifact Management
The rapid growth of generative AI is shifting the user’s expectations for next-gen applications. In this context, OpenAI/GPT models are gaining prominence. Being one of the booming customer apps of the decade, OpenAI/GPT has crossed the 100 million mark of having weekly active users by the year 2024. Behind the scenes, businesses are eager to integrate the same GPT functionalities into their own products by utilizing the OpenAI API. Improving business workflows through automation, transforming chatbots, and optimizing user interactions through human-like abilities. Leading Use Cases of OpenAI/GPT Models into your Mobile/Web Apps Struggling with different opinions, we are still in a dilemma about how OpenAI integration plays a critical role in streamlining our workflow and saving time. Here are the top use cases of OpenAI integration into your legacy apps. Chatbots and Customer Support It is evident that OpenAI integration into iOS, Android, and web apps is meant to empower advanced, conversational chatbots that do not confine themselves to simple rule-based systems. With the capability to comprehend natural language, GPT can: Process intricate customer queries with human-like fluency.Offer round-the-clock support across different platforms, including web, mobile, or messaging channels.Offer multilingual support, minimizing the need for a dedicated translation team.Remember to optimize responses over time, leveraging prompt engineering or context chaining. Example: An e-commerce application unifies GPT to build a chatbot that enables users to keep tracking their orders, return products, and even receive design recommendations. Dynamic Content Generation and Personalization GPT is an organic match for creating content that is dynamic and personalized. You can easily utilize it for many purposes: Render blog posts, ad copy, or product descriptions automatically.Provide tailored suggestions, emails, or onboarding communications/messages depending on user behavior.Draft content blocks, reports, or summaries within your app interface. Example: A mobile fitness app uses GPT to generate weekly workout plans and motivational messages based on the user's past activity and goals. Context-Aware Search and Recommendations Conventional search engines entirely depend on keywords — GPT enables you to develop semantic search and recommendation systems that help to analyze intent, context, and variation. Traditional search engines rely on keywords — GPT allows you to build semantic search and recommendation systems that understand intent, context, and nuance. GPT can interpret and answer natural language queries, such as “Suggest travel destinations I can visit in neighboring nations.”It can condense documents, process product reviews, or suggest content based on user context and preferences.You can blend GPT and vector databases. For example, Pinecone or FAISS for high-precision semantic search. Example: A learning application enables students to seek answers to complex questions. “How does OpenAI integration help boost your app’s intelligence?” OpenAI integration GPT models provide a clear, structured explanation. Integrating OpenAI GPT Into Your App: A Comprehensive Guide The OpenAI integration of GPT modules into your mobile or web apps is undoubtedly becoming a must-have element. The outstanding growth of AI-enabled apps pushes the need for conversational assistants and content automation. From the beginning to the implementation of GPT in the basic app, let us explore in detail in this section. Getting Started With the OpenAI API Here are the simple steps you need to follow to begin using GPT in your application. Step 1: Create an OpenAI Account Kickstart by opening your account. Visit https://platform.openai.com/ to sign up and get instant access to the API dashboard. Step 2: Generate an API Key Proceed forward by logging in using the credentials (username, password). Go to the section “API Keys” and build a new key. Maintain privacy and do not disclose this key in client-side code. Step 3: Understand the Core Endpoints Use the chat/completions endpoint (for GPT-3.5/4) for most apps. It accepts a huge series of messages and responds with a message in natural language. Step 4: Install HTTP Client Use libraries in your backend or client, for example: axios or fetch for JavaScripthttp for Dart/Flutterrequests for Pythoncurl or Postman for testing Make a sample request, for instance (Node.js): Another instance is GPT in a React Web App or Flutter Mobile App. React Web App Kindly, prevent calling the OpenAI API straight from the client for security purposes. You can use some other ways, like Create a backend to communicate (such as Node.js/Express or Next.js API routes).Consider using React for UI (for example, a chat input and display window).Invoke your backend route, ensuring security using user input. React front-end snippet: Flutter Mobile App Prefer to use the http package to initiate backend interactions.Never integrate the OpenAI API key into your app. Instead, leverage a cloud function or a serverless backend to relay requests. Back-end API call — Flutter Code Explore more: Top AI Development Companies Secure and Scalable Integration: Key Tips to Proceed To guarantee that your application performs well, is highly secure, and is ready for production, here the the best practices to follow: Security Tips Prefer not to disclose your OpenAI API key in front-end code.Choose to utilize a secure backend to manage API requests and API tokens.Enforce input/output filtering to escape from harmful prompts or misuse. Scalability Tips For long or vibrant responses, use streaming mode to optimize perceived performance.Enforce rate limiting and token utilization monitoring.Prefer caching frequent responses to cut off API load. Cost Optimization Begin with GPT-3.5-turbo and access additional features by upgrading to GPT-4 only as and when needed.Restrict max_tokens and message size depending on your app's use case.If available, utilize function calling or tools (such as GPT-4-turbo) to optimize workflows. Testing and Monitoring Prefer using tools such as Postman or Insomnia for testing API endpoints.Track delays, errors, and token utilization using tools such as Datadog, Sentry, custom logging, or OpenAI's usage dashboard. Wrapping Up OpenAI integration transforms your web and mobile apps in multiple ways, like intelligent automation, human-like communications, and dynamic content. As a key part of AI software development, it empowers businesses to offer advanced user experiences, from intelligent chatbots to tailored recommendations. OpenAI integration ensures the security, responsiveness, and future-centricity of your app through the right backend installation and cost-effective scaling. With the rise of AI adoption, it has become more essential to integrate these models and gain a competitive edge in the current digital landscape.
Did you ever feel like working with Redshift is like being stuck in a never-ending queue at Starbucks during morning rush hour? Everyone’s waiting, getting cranky, and you’re hoping that your simplest request get processed before you’re next birthday! After spending years working with various data warehousing solutions, I recently made a rather unconventional choice that I am excited to share. I switched from Amazon Redshift to DuckDB for my real-time GenAI application, and the results were eye-opening. The Problem With Redshift The Concurrency Nightmare The concurrency limitation became a significant nightmare, and here’s why: - Default concurrency limit is 50 connections (500 with large nodes)Each user session requires a dedicated connection.Connection pooling helps but adds complexity.Timeout issues when connections weren’t properly released. Python # Common scenario with Redshift import psycopg2 from psycopg2 import pool try: # Creating connection pool connection_pool = pool.SimpleConnectionPool( 1, 50, # min and max connections host="myhost", database="mydb", user="myuser", password="mypassword" ) except psycopg2.Error as e: print("Error: Could not connect to Redshift") print(e) # Even with connection pooling, we'd still hit limits during peak usage Query Performance Deep Dive The performance difference between Redshift and DuckDB was stark. Here is a real example. Python -- This query in Redshift took ~35 seconds WITH user_features AS ( SELECT user_id, COUNT(DISTINCT session_id) as session_count, AVG(session_duration) as avg_duration, SUM(interaction_count) as total_interactions FROM user_sessions WHERE date_trunc('day', timestamp) >= CURRENT_DATE - 30 GROUP BY user_id ), recent_interactions AS ( SELECT user_id, ARRAY_AGG(item_id ORDER BY timestamp DESC LIMIT 10) as recent_items FROM interactions WHERE timestamp >= CURRENT_TIMESTAMP - INTERVAL '24 hours' GROUP BY user_id ) SELECT uf.*, ri.recent_items FROM user_features uf LEFT JOIN recent_interactions ri ON uf.user_id = ri.user_id; The same query in DuckDB: Python # DuckDB completed in ~4 seconds import duckdb conn = duckdb.connect('my_database.db') result = conn.execute(""" WITH user_features AS ( SELECT user_id, COUNT(DISTINCT session_id) as session_count, AVG(session_duration) as avg_duration, SUM(interaction_count) as total_interactions FROM user_sessions WHERE timestamp >= CURRENT_DATE - 30 GROUP BY user_id ), recent_interactions AS ( SELECT user_id, LIST(item_id ORDER BY timestamp DESC LIMIT 10) as recent_items FROM interactions WHERE timestamp >= CURRENT_TIMESTAMP - INTERVAL '24 hours' GROUP BY user_id ) SELECT uf.*, ri.recent_items FROM user_features uf LEFT JOIN recent_interactions ri ON uf.user_id = ri.user_id; """).fetchdf() Why Such A Dramatic Difference? Architecture Redshift: Distributed system with network overhead.DuckDB: In-process database with direct memory access. Query Execution Redshift: Requires data distribution across nodes.DuckDB: Vectorized execution on local data. Memory Management Redshift: Manages shared memory across multiple connections.DuckDB: Direct access to process memory. Why DuckDB Won My Heart Embedded Analytics Excellence You could literally use any python data library along with DuckDB which makes life so much simpler. Check out this example below: - import duckdb import pandas as pd import pyarrow as pa # Create an in-memory database # Look how simple it is to setup duckdb conn = duckdb.connect(':memory:') # Load data from multiple sources conn.execute(""" CREATE TABLE user_data AS SELECT * FROM read_parquet('users/*.parquet') """) # Complex analytics with zero network overhead result = conn.execute(""" SELECT DATE_TRUNC('hour', timestamp) as hour, COUNT(*) as events, COUNT(DISTINCT user_id) as unique_users, AVG(response_time) as avg_response FROM user_data WHERE timestamp >= CURRENT_TIMESTAMP - INTERVAL '24 hours' GROUP BY 1 ORDER BY 1 """).fetchdf() # Direct integration with pandas/arrow arrow_table = pa.Table.from_pandas(result) Memory Efficiency and Data Processing It’s memory management is actually impressive. For my application, I had the data files stored in parquet format at S3, and boy, the speed at which it was able to eat them all! Here is a general example. # Efficient handling of large CSV files conn.execute(""" CREATE TABLE large_dataset AS SELECT * FROM read_csv_auto('large_file.csv', SAMPLE_SIZE=1000000, ALL_VARCHAR=0) """) # Automatic type inference and compression conn.execute(""" SELECT typeof(column_name), approx_count_distinct(column_name) as cardinality, pg_size_pretty(sum(length(column_name))) as size FROM large_dataset GROUP BY 1 """) Advanced Analytics Integration Duckdb comes with all the analytics functions that we are so dependant on while working with conventional databases. # Complex window functions conn.execute(""" WITH user_metrics AS ( SELECT user_id, timestamp, metric_value, AVG(metric_value) OVER ( PARTITION BY user_id ORDER BY timestamp ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) as moving_avg FROM metrics ) SELECT * FROM user_metrics WHERE moving_avg > 100 """) # Machine learning feature generation features = conn.execute(""" SELECT user_id, COUNT(*) as event_count, COUNT(DISTINCT session_id) as session_count, MIN(timestamp) as first_seen, MAX(timestamp) as last_seen, AVG(duration) as avg_duration, ARRAY_AGG(DISTINCT category) as categories FROM events GROUP BY user_id """).df() When To Stick With Redshift (Because Let’s Be Real) It’s perfect for: Enterprise-scale data warehousing Handles petabytes-scale data with ease.Efficient for complex JOIN operations across large tables.Better for data spanning multiple regions/zones. Complex ETL Workflows Native integration with AWS services.Scheduled maintenance and backups.Automated scaling and optimization.Supports complex ETL patterns. When you’re security team won’t let you breathe without audit logs Fine-grained access control.Row-level security.Audit logging and compliance reporting.Integration with AWS security services. Power-User Tips: Supercharging DuckDB Essential Extensions and Configurations Before we wrap-up, let me share some power-user configurations that took my DuckDB implementation from great to incredible. Python import duckdb # Basic connection with some tuned settings conn = duckdb.connect('my_db.db', config={ 'temp_directory': '/path/to/temp', # Custom temp directory 'memory_limit': '8GB', # Adjust based on your needs 'threads': 12 # Parallel processing threads }) # Load essential extensions conn.execute("INSTALL httpfs;") # HTTP file system support conn.execute("LOAD httpfs;") conn.execute("INSTALL json;") # Enhanced JSON support conn.execute("LOAD json;") conn.execute("INSTALL sqlite;") # SQLite scanner conn.execute("LOAD sqlite;") Working With Remote Data One of the coolest features is direct access to remote files. Here’s how I handle it. Python # Reading directly from S3 conn.execute(""" SET s3_region='us-east-1'; SET s3_access_key_id='your_access_key'; SET s3_secret_access_key='your_secret_key'; """) # Now you can query S3 data directly! conn.execute(""" SELECT * FROM read_parquet('s3://my-bucket/data/*.parquet') WHERE date_column >= '2024-01-01' """) # Or even better, create a view conn.execute(""" CREATE VIEW recent_data AS SELECT * FROM read_parquet('s3://my-bucket/data/*.parquet') WHERE date_column >= CURRENT_DATE - INTERVAL '30 days' """) Memory Management Like A Pro Python # Configure adaptive memory management def configure_memory_settings(conn, total_gb=8): settings = { 'memory_limit': f'{total_gb}GB', 'threads': max(4, os.cpu_count() - 2), # Leave some CPU for other processes 'temp_directory': '/path/to/fast/ssd', # Use fast storage for spilling } for key, value in settings.items(): conn.execute(f"SET {key}='{value}'") # Example usage with streaming results def process_large_dataset(conn): # Use chunks to handle large results chunk_size = 100000 result = conn.execute(""" SELECT * FROM huge_table WHERE complex_condition """).fetch_df_chunk(chunk_size=chunk_size) for chunk in result: process_chunk(chunk) Troubleshooting Tips When things don’t go as planned. Python # Memory debugging conn.execute("SELECT * FROM duckdb_memory();") # Query profiling conn.execute(""" EXPLAIN ANALYZE SELECT complex_query_here; """) # Check extension status conn.execute("SELECT * FROM duckdb_extensions();") The Verdict For my GenAI application, DuckDB proved to be a perfect fit. The perfect combination of: Lightening-fast query performance.Zero concurrency issues.Excellent Python integration.Simplified infrastructure. But, would I use it for everything? Absolutely not. Only for the applications that requires Real-Time processing, has moderate data size, requires direct integration with Python/pandas and have low-latency requirements. Choosing the right tool isn’t about following the trends. It’s about understanding your specific requirements and constraints. Sometimes, simpler is better. What’s your experience with DuckDB and Redshift. Drop your thoughts in the comments below.
Overview A web dashboard serves as the “front panel” for an embedded product — whether that product is a rack-mounted industrial controller, a bike-mounted GPS tracker, or a battery-powered soil-moisture sensor buried in a greenhouse bed. Because the dashboard is delivered over plain HTTP(S) and rendered in any modern browser, users do not have to download a native app, install drivers, or worry about operating-system compatibility; the interface is as portable as a URL. Typical tasks include: Toggling outputs (relays, MOSFETs, LEDs)Inspecting live data such as temperature, humidity, current draw, or RSSIAdjusting parameters like Wi-Fi credentials, alarm set-points, sampling ratesCollecting diagnostics like log files or memory statistics for field support staff Implementation Approaches Embed an HTTP server — Mongoose, lwIP-HTTPD, MicroPython’s uHTTPD, or a hand-rolled socket handler - inside the firmware. Then choose, or mix, the patterns below. Each technique sits at a distinct point on the scale of resource cost versus user-experience richness. 1. CGI (Common Gateway Interface) Classic CGI ties a URL such as /led.cgi to a firmware function that executes and returns HTML: C int cgi_led(struct http_request *r){ bool on = gpio_toggle(LED); http_printf(r,"<h1>LED %s</h1>", on ? "ON":"OFF"); return 200; } Pros: Footprint under 4 kB flash and a few hundred bytes of RAM.Cons: Every interaction forces a full page refresh, so UX feels clunky. Validation is manual. 2. RESTful API (Representational State Transfer) REST treats device features as resources: HTTP GET /api/led → read state POST /api/led {"on":1} → set state GET /api/adc/3 → read channel 3 The dashboard becomes a single-page app delivered from /index.html; JavaScript fetch() calls the API and patches the DOM. Pros: Clear separation of data and presentation; easy to reuse endpoints in a mobile app or cloud shim.Tips: Version your URIs (/v1/...) and compress payloads. 3. Server-Side Includes (SSI) SSI injects runtime values into static HTML. Tokens like the following are swapped out by a callback before the file is sent. HTML <h2>Temp: <!--#getTemp-->°C</h2> Pros: dead-simple way to inject a few dynamic numbers.Cons: limited once you need richer interactivity.When to use: Read-heavy dashboards that refresh every few seconds.Limits: Tokens only inject text; they cannot change style or handle clicks. 4. WebSockets WebSockets upgrade the HTTP connection to full duplex, letting both sides push frames whenever they like - ideal for streaming vibration data or ticker-style logs. Typical flow: the dashboard loads, JavaScript calls new WebSocket("wss://device-ip/ws"), firmware keeps the socket handle, and an ISR queues sensor frames. Pros: Latency < 10 ms, supports binary frames for efficiency.Cons: Each open socket eats buffers; TLS roughly doubles RAM cost. 5. MQTT Over WebSockets If the firmware already speaks MQTT, bridge the broker over wss://device-ip/mqtt. JavaScript clients such as Eclipse Paho can then subscribe and publish. Example topic map: Plain Text data/temperature → periodically published by device cmd/led → written by dashboard; MCU subscribes Pros: Reuses existing infrastructure; offline mode works because the broker is onboard.Cons: Extra protocol headers inflate packets; QoS handshakes add chatter. Comparing Footprints and Performance ApproachFlashRAMLatencyUX CGI 3 - 8 kB <1 kB >200 ms * SSI 4 - 10 kB ~1 kB 150 ms ** REST 8 - 25 kB 2 - 4 kB 80 ms *** WS 18 - 40 kB 4 - 8 kB <10 ms ***** MQTT/WS 25 - 60 kB 6 - 12 kB 15 ms **** Securing the Dashboard Transport security: Serve everything over TLS; many MCUs include hardware AES, keeping overhead modest.Authentication: Use a signed, time-limited token instead of basic auth.Validation: Treat query strings and JSON bodies as hostile until parsed and bounds-checked.Rate limiting: Guard against runaway polling that can starve the CPU. Many defences can be compiled out for development builds to save RAM, then re-enabled for production firmware. Blending Techniques Real products rarely stick to a single pattern. A popular recipe involves delivering the SPA shell, exposing low-frequency configuration through REST, streaming high-rate metrics over WebSockets, and maintaining an SSI “fallback” page for legacy browsers. This hybrid keeps memory use modest while giving power users a slick, low-latency UI. Selecting a Strategy A quick rule: If you need millisecond-level telemetry, pick WebSockets; if you want integration with phone apps or cloud dashboards, use REST; and if you are squeezed for flash, fall back to CGI or SSI. Transitioning later is painless because the same embedded server can host multiple schemes side by side. Conclusion Implementing a device dashboard using an embedded web server offers a scalable, browser-based control panel for embedded systems. Techniques such as CGI, SSI, REST APIs, and WebSockets cater to different device capabilities and complexity requirements. For modern applications, REST APIs strike a balance between simplicity and power, making them ideal for implementing features like an embedded web dashboard. By choosing the right implementation approach and utilizing a lightweight embedded web server, developers can craft efficient, interactive, and user-friendly dashboards to control and monitor embedded devices. For a hands-on walkthrough, see a step-by-step simple REST API implementation (LED Toggle Example). This walks through a minimal example of building an embedded web dashboard using a REST API served by an embedded web server. You can also try Mongoose Wizard to create your WebUI dashboard in minutes. It is a no-code visual tool that enables developers to effortlessly build a professionally looking device dashboard (WebUI) and REST API without writing any frontend code. Transforming the MCU board into a browser-accessible web dashboard for control, monitoring, and updates. Whether for prototyping or building production devices, integrating a web dashboard into firmware gives end users intuitive and powerful control.
Development on Salesforce has seen major changes in the last few years. SDD has made it possible for teams to match their Salesforce processes to the best modern DevOps approaches. Fundamentally, SDD depends on version control, automated deployments, and coding your data’s metadata. With benefits like consistency, traceability, and automation, such changes introduce new challenges about how versions and metadata should be managed throughout the project. Here, we’ll look at SDD principles and guide you through properly managing metadata and API versions in a Salesforce environment. Understanding Source-Driven Development (SDD) Using Source-Driven Development, configuration, code, and metadata are handled entirely in a version control system, mainly Git. As a result, the repository is the only true source of data, not Salesforce, which stores individual copies. Changes made to the software should start in the repository and happen using a predefined process. Doing things this way means teams can collaborate better, updates are safer, and problems can easily be fixed by rolling back if there is a mistake. Salesforce DX includes the tools needed to make SDD succeed. With this, developers can design their jobs in small parts, utilize scratch orgs for safe development and set up metadata using source code. Using SDD, developers can establish CI/CD processes just as is done in traditional software development. Managing Metadata in Salesforce Projects All configuration changes and add-ons made in Salesforce such as custom objects, fields, page layouts and flows, are known as metadata. When using SDD, metadata is put into a neat structure of files and stored versioned in Git. Salesforce DX supports using source format instead of the former metadata API format. Small, logical pieces of metadata are created by using the source format. Instead of putting every field of an object in one location, each individual field is tracked separately, so you can follow changes, resolve merging issues and focus on code reviews. This arrangement of metadata both increases transparency and simplifies how deployments are done. Tools such as the Salesforce CLI (sfdx) or plugins like sfdx-git-delta enable developers to retrieve and deploy only the metadata that has changed. This selective deployment approach leads to faster deployments and reduced risk in release pipelines. Tracking changes in scratch orgs is straightforward using source:tracking commands, while in persistent sandboxes, external diff tools are often used to compare the org’s current state to the repository. The Importance of API Version Management Every metadata file in Salesforce allows you to select the API version. These correspond to the platform releases (58.0, 59.0) and determine the functioning of the components. Proper control of API versions ensures everything works the same everywhere and prevents surprises once an API is deployed or running. The sourceApiVersion field in the sfdx-project.json file determines the default API version used when retrieving or deploying metadata. It’s best practice to align all metadata components with this version. Inconsistent API versions across metadata files can cause deployment errors, compatibility issues, or subtle bugs that are hard to trace. Many organizations are drawn to update all their metadata right when a new Salesforce version is available. However, we need to be careful when doing this. Teams ought to test the new API version in sandboxes or scratch orgs before updating. Many changes in the versions may have a strong impact on things such as Apex classes, Lightning Web Components (LWC), flows and permission sets. Regularly reviewing the metadata will keep software versions the same. CI tools let developers use API scripts to check if files and API instances are aligned, preparing them for use in the build. Tools and Practices to Support SDD The correct collection of tools and processes plays a crucial role in the success of an SDD solution. Most of what you do in Salesforce, from getting metadata to changing features, relies on the Salesforce CLI. Most developers work with Visual Studio Code (VS Code) and Salesforce extensions to manage and write metadata. To keep things up to date, it’s common to use GitHub Actions, Azure DevOps, or Bitbucket Pipelines for automated testing and deployments. For organizations managing large-scale Salesforce implementations, tools like sfdx-git-delta or metadata comparison utilities help streamline change tracking and promote selective deployments. Additionally, adopting unlocked packages can help modularize the org’s metadata, making it easier to manage independently developed components and accelerate delivery. Common Pitfalls to Avoid Even with all the pros, SDD comes with a few typical errors teams should avoid. The most typical problem is called API version drift which can happen when metadata does not agree on the same API version. There could be unexpected actions or issues in deployments. Make sure to keep version usage consistent and regularly look at how team members use the versions. Using manual changes to configuration files is yet another concern that isn’t tracked in version control. It’s important to sync any changes you make in a sandbox or in production back to the repository so there is no drift. Also, having one big metadata package greatly increases the danger of errors occurring. As an alternative, development teams should only roll out what has been updated or take advantage of modular packaging. Conclusion Modern software engineering techniques are now available for Salesforce development thanks to Source-Driven Development. When teams organize their metadata in the source file and agree on specific API versions, collaboration gets better, the solution becomes more stable, and it’s delivered faster. Moving to SDD needs careful planning regarding data and versions, but the more reliable and scalable platform it brings is an investment any Salesforce organization will value. Regardless of your experience or goals in DevOps, start by setting up solid structures — keep version checks automated, ensure your metadata is well structured and blend your version control system fully into all your release processes. By following these tips, you’ll make your Salesforce development process stronger and better suited for the future.
Forget the idea that modernization has to mean rewriting everything. The real work happens in the in-between, where REST meets SOAP, where sidecars live beside WAR files, and where code changes are political before they're technical. Especially in high-stakes, compliance-bound environments like healthcare, government, and labor systems, modernization doesn’t look like a revolution. It looks like a careful negotiation. When Modernization Isn't Optional (Yet Also Isn't a Rewrite) Enterprise Java applications aren’t always a clean slate. Many developers work with monoliths that began over a decade ago, coded with Hibernate DAOs, JSP-driven UIs, and SOAP-based services stitched together with barely documented business logic. These aren’t museum pieces. They still power critical systems. The real tension arises when modern demands enter the mix: JSON interfaces, cloud triggers, and client-side rendering. Teams are often asked to retrofit REST endpoints into XML-heavy ecosystems or to integrate AWS services without disturbing a decades-old session flow. Modernization, in this context, means layering, not leaping. You’re not rewriting the system; you’re weaving new threads into a fabric that’s still holding the enterprise together. In one modernization project for a public-sector system, even a minor update to session tracking caused a chain reaction that disabled inter-agency authentication. This underscored the principle that modernization efforts must be staged, tested in sandboxed environments, and introduced gradually through abstraction layers, not pushed as big-bang releases. In a real-world healthcare analytics project for a government client, we had to preserve decades-old XML-based report generation logic while enabling modern REST-based data access for third-party dashboards. The solution wasn't to replace, but to extend: we introduced Spring Boot services that intercepted requests, repackaged outputs into JSON, and served them in parallel without touching the legacy data model. It worked, not because we transformed everything, but because we layered just enough to adapt what mattered. The Balancing Act: Spring Boot Meets Legacy WAR Files One common strategy is selective decomposition: peeling off slices of the monolith into Spring Boot services. But this isn’t a clean cut. Existing WAR deployments require coexistence strategies, especially when the primary app is deployed on traditional servlet containers like WebLogic or Tomcat. Spring Boot’s embedded Tomcat can conflict with the parent app server’s lifecycle, and developers often have to carefully configure ports, context paths, and threading models to avoid resource contention. Session management is another complication. Many legacy systems rely on stateful sessions, and introducing token-based authentication in Spring Boot may create a mismatch in how user identity is tracked across services. Teams must decide whether to retain centralized session management or introduce statelessness at the edge and deal with session bridging internally. Configuration strategies also diverge; legacy apps may rely on property files and hardcoded XML, whereas Spring Boot encourages externalized, environment-driven configs. The architectural result often looks transitional. A legacy monolith continues to function as the core engine, but adjacent REST services are spun off using Spring Boot. These services are fronted by an API gateway or reverse proxy, and internal SOAP calls may be wrapped in REST adapters. Anti-corruption layers come into play to keep new services loosely coupled. Done right, this architecture buys time and stability while enabling incremental modernization. A similar approach was applied in a case involving a pension management platform, where we deployed Spring Boot services alongside a WAR-deployed core app still running on WebLogic. The goal wasn’t to replace workflows overnight, but to carefully intercept and extend specific endpoints, such as payment triggers and beneficiary lookups, through newer RESTful interfaces. This allowed independent deployments and cloud-based enhancements without risking the system’s compliance-grade uptime. RESTful Interfaces at War With XML Contracts The move to REST isn’t just about switching verbs or moving from POST to PUT. Older systems may still speak WSDL, and dropping XML isn’t always viable due to integrations with legacy clients, vendors, or compliance protocols. Some government-facing systems still require signed XML payloads validated against DTDs or XSDs. To maintain compatibility, many teams dual-maintain both REST and SOAP formats. JAXB plays a key role in binding Java objects to XML. At the same time, libraries like XStream allow flexible serialization control for hybrid payloads that must meet backward compatibility while providing modern interfaces. Developers often annotate classes with JAXB and then use custom message converters or serialization logic to support both XML and JSON APIs. Here’s an example of a JAXB-bound class with a fallback handler that captures unknown elements: Java @XmlRootElement(name = "User") public class User { private String name; private String email; @XmlElement public String getName() { return name; } @XmlElement public String getEmail() { return email; } @XmlAnyElement public List<Object> getUnknowns() { return unknownFields; } } This setup allows parallel API exposure without breaking legacy consumers, offering a pathway to decouple the front-facing API strategy from backend contracts. From Server Rooms to the Cloud: Incremental AWS Integration You don’t forklift a Java app to the cloud. You sneak it in, one event, one interface, one file upload at a time. One practical method is integrating AWS services alongside existing workflows, without replacing core components. A common pattern involves leveraging S3 buckets for file storage. A legacy Java backend can upload user files or reports to a bucket, which then triggers a Lambda function through an S3 event notification. The Lambda function might perform transformations, enrich data, or send alerts to other services using SNS or SQS. The Spring Cloud AWS toolkit simplifies this integration. Developers use the spring-cloud-starter-aws library to configure S3 clients, IAM roles, and event subscriptions. IAM roles can be applied at the EC2 instance level or dynamically assumed using STS tokens. Within the codebase, these roles provide controlled access to buckets and queues. Diagrammatically, this looks like: This is exactly what played out in a logistics project where we needed to track shipment uploads in real-time. Rather than rewrite the core Java backend, we allowed it to upload files to an S3 bucket. An AWS Lambda picked up the event, transformed the metadata, and forwarded it to a tracking dashboard, all without changing a single servlet in the monolith. It was cloud-native augmentation, not cloud-for-cloud’s-sake. This approach decouples cloud adoption from full-stack reengineering, allowing legacy systems to coexist with modern event-driven flows. It also acts as a proving ground for broader cloud-native transitions without disrupting production workloads. A future extension could involve migrating only certain data processing steps to ECS or EKS, based on what proves stable during these early Lambda integrations. CI/CD and Monitoring for a Codebase You Didn’t Write Legacy code often resists modernization, especially when coverage is low and configuration is arcane. CI/CD pipelines offer a lifeline by enforcing baseline quality checks and deployment discipline. A typical setup involves Jenkins pipelines that trigger on Git push events. The pipeline stages include Maven builds, code linting using PMD and Checkstyle, and static analysis using SonarQube. In large codebases, teams also use Jacoco to track incremental coverage improvements, even if full coverage is impractical. Jenkins shared libraries help enforce common pipeline stages across services. Here’s a Jenkinsfile snippet from one such setup: Groovy pipeline { agent any stages { stage('Build') { steps { sh 'mvn clean install -DskipTests' } } stage('Code Quality') { steps { sh 'mvn pmd:check checkstyle:check sonar:sonar' } } } } Monitoring requires similar compromise. Tools like AppDynamics or New Relic must be configured with custom JVM arguments during startup, often injected through shell scripts or container entrypoints. With EJBs or SOAP layers, automatic instrumentation may fail, requiring explicit trace annotations or manual config. Even if the system is too fragile for full APM adoption, lightweight metrics and error tracking can surface regressions early, making the system more observable without requiring invasive rewrites. Modernization Isn’t Technical First, It’s Strategic Rushing modernity is the fastest way to break an enterprise system. What makes modernization sustainable isn’t tooling; it’s judgment. Whether we’re layering Spring Boot next to a WAR-deployed monolith, adapting REST interfaces over legacy XML, or routing data through S3 buckets instead of adding endpoints, the same principle applies: progress happens at the edges first. Modernization doesn’t begin with code. It begins with the courage to leave working systems untouched, and the clarity to change only what will move the system forward. That’s not just modernization. That’s stewardship.
Graph databases are increasingly popular in modern applications because they can model complex relationships natively. Graphs provide a more natural representation of connected data from recommendation systems to fraud detection. Our previous articles explored graph databases broadly and delved into Neo4j. In this third part, we focus on JanusGraph, a scalable and distributed graph database. Unlike Neo4j, JanusGraph supports multiple backends and leverages Apache TinkerPop, a graph computing framework that introduces a standard API and query language (Gremlin) for various databases. This abstraction makes JanusGraph a flexible choice for enterprise applications. Understanding JanusGraph and TinkerPop JanusGraph is an open-source, distributed graph database that handles huge volumes of transactional and analytical data. It supports different storage and indexing backends, including Cassandra, HBase, BerkeleyDB, and Elasticsearch. It implements the TinkerPop framework, which provides two main components: Gremlin: A graph traversal language (both declarative and imperative).TinkerPop API: A set of interfaces for working with graph databases across different engines. This allows developers to write database-agnostic code on any compliant TinkerPop-compatible engine. Gremlin is a functional, step-based language for querying graph structures. It focuses on traversals: the act of walking through a graph. Gremlin supports OLTP (real-time) and OLAP (analytics) use cases across more than 30 graph database vendors. FeatureSQLGremlinEntity RetrievalSELECT * FROM Bookg.V().hasLabel('Book')FilteringWHERE name = 'Java'has('name','Java')Join/RelationshipJOIN Book_Category ON ...g.V().hasLabel('Book').out('is').hasLabel('Category')Grouping & CountGROUP BY category_idgroup().by('category').by(count())Schema FlexibilityFixed schemaDynamic properties, schema-optional JanusGraph supports both embedded and external configurations. To get started quickly using Cassandra and Elasticsearch, run this docker-compose file: YAML version: '3.8' services: cassandra: image: cassandra:3.11 ports: - "9042:9042" elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2 environment: - discovery.type=single-node - xpack.security.enabled=false ports: - "9200:9200" janusgraph: image: janusgraph/janusgraph:latest depends_on: - cassandra - elasticsearch ports: - "8182:8182" environment: - gremlin.graph=org.janusgraph.core.ConfiguredGraphFactory - storage.backend=cql - storage.hostname=cassandra - index.search.backend=elasticsearch - index.search.hostname=elasticsearch Alternatively, you can avoid external dependencies entirely for local development or embedded environments. JanusGraph supports embedded mode using BerkeleyDB Java Edition (berkeleyje) for local graph storage, and Lucene as the indexing engine. This is especially useful for quick prototyping or running unit tests without setting up infrastructure. BerkeleyJE is a fast, embeddable key-value store written in Java, which stores your graph data directly on the local filesystem. Here’s an example configuration for embedded mode: Properties files storage.backend=berkeleyje storage.directory=../target/jnosql/berkeleyje index.search.backend=lucene index.search.directory=../target/jnosql/lucene In case you want to execute with Casandra, please update the properties in this mode: Properties files jnosql.graph.database=janusgraph storage.backend=cql storage.hostname=localhost index.search.backend=elasticsearch index.search.hostname=localhost Before modeling the domain, we need to define the structure of the entities that will represent our graph. In this case, we use two vertex types: Book and Category. Each entity is annotated using Jakarta NoSQL annotations and contains a unique ID and a name. These entities will form the foundation of our graph, allowing us to define relationships between books and their associated categories. Java @Entity public class Book { @Id private Long id; @Column private String name; } @Entity public class Category { @Id private Long id; @Column private String name; } Once the entities are defined, the next step is to persist and retrieve them from the database. For this purpose, Eclipse JNoSQL provides the TinkerpopTemplate, which is a specialization of the generic Template interface specifically designed for graph operations using Apache TinkerPop. The service layer encapsulates the logic of querying the database for existing books or categories and inserting new ones if they don't exist. This pattern helps maintain idempotency when saving data, ensuring duplicates aren't created. Java @ApplicationScoped public class BookService { @Inject private TinkerpopTemplate template; public Book save(Book book) { return template.select(Book.class).where("name").eq(book.getName()).<Book>singleResult() .orElseGet(() -> template.insert(book)); } public Category save(Category category) { return template.select(Category.class).where("name").eq(category.getName()).<Category>singleResult() .orElseGet(() -> template.insert(category)); } } The BookApp class shows full execution: inserting entities, creating relationships (edges), and executing Gremlin queries: Java var architectureBooks = template.gremlin("g.V().hasLabel('Category').has('name','Architecture').in('is')").toList(); var highRelevanceBooks = template.gremlin("g.E().hasLabel('is').has('relevance', gte(9)).outV().hasLabel('Book').dedup()").toList(); You can also chain traversals with .traversalVertex() For more fluent pipelines: Java List<String> softwareBooks = template.traversalVertex().hasLabel("Category") .has("name", "Software") .in("is").hasLabel("Book").<Book>result() .map(Book::getName).toList(); The BookApp introduces the TinkerpopTemplate capability, where we have the bridge between Java and Janus database: Java public final class BookApp { private BookApp() { } public static void main(String[] args) { try (SeContainer container = SeContainerInitializer.newInstance().initialize()) { var template = container.select(TinkerpopTemplate.class).get(); var service = container.select(BookService.class).get(); var software = service.save(Category.of("Software")); var java = service.save(Category.of("Java")); var architecture = service.save(Category.of("Architecture")); var performance = service.save(Category.of("Performance")); var effectiveJava = service.save(Book.of("Effective Java")); var cleanArchitecture = service.save(Book.of("Clean Architecture")); var systemDesign = service.save(Book.of("System Design Interview")); var javaPerformance = service.save(Book.of("Java Performance")); template.edge(Edge.source(effectiveJava).label("is").target(java).property("relevance", 10).build()); template.edge(Edge.source(effectiveJava).label("is").target(software).property("relevance", 9).build()); template.edge(Edge.source(cleanArchitecture).label("is").target(software).property("relevance", 8).build()); template.edge(Edge.source(cleanArchitecture).label("is").target(architecture).property("relevance", 10).build()); template.edge(Edge.source(systemDesign).label("is").target(architecture).property("relevance", 9).build()); template.edge(Edge.source(systemDesign).label("is").target(software).property("relevance", 7).build()); template.edge(Edge.source(javaPerformance).label("is").target(performance).property("relevance", 8).build()); template.edge(Edge.source(javaPerformance).label("is").target(java).property("relevance", 9).build()); List<String> softwareCategories = template.traversalVertex().hasLabel("Category") .has("name", "Software") .in("is").hasLabel("Category").<Category>result() .map(Category::getName) .toList(); List<String> softwareBooks = template.traversalVertex().hasLabel("Category") .has("name", "Software") .in("is").hasLabel("Book").<Book>result() .map(Book::getName) .toList(); List<String> sofwareNoSQLBooks = template.traversalVertex().hasLabel("Category") .has("name", "Software") .in("is") .has("name", "NoSQL") .in("is").<Book>result() .map(Book::getName) .toList(); System.out.println("The software categories: " + softwareCategories); System.out.println("The software books: " + softwareBooks); System.out.println("The software and NoSQL books: " + sofwareNoSQLBooks); System.out.println("\Books in 'Architecture' category:"); var architectureBooks = template.gremlin("g.V().hasLabel('Category').has('name','Architecture').in('is')").toList(); architectureBooks.forEach(doc -> System.out.println(" - " + doc)); System.out.println("Categories with more than one book:"); var commonCategories = template.gremlin("g.V().hasLabel('Category').where(__.in('is').count().is(gt(1)))" ).toList(); commonCategories.forEach(doc -> System.out.println(" - " + doc)); var highRelevanceBooks = template.gremlin( "g.E().hasLabel('is').has('relevance', gte(9))" + ".outV().hasLabel('Book').dedup()").toList(); System.out.println("Books with high relevance:"); highRelevanceBooks.forEach(doc -> System.out.println(" - " + doc)); System.out.println("\Books with name: 'Effective Java':"); var effectiveJavaBooks = template.gremlin("g.V().hasLabel('Book').has('name', @name)", Collections.singletonMap("name", "Effective Java")).toList(); effectiveJavaBooks.forEach(doc -> System.out.println(" - " + doc)); } } } To complement the use of TinkerpopTemplate, Eclipse JNoSQL supports the Jakarta Data specification by enabling repository-based data access. This approach allows developers to define interfaces, like BookRepository and CategoryRepository — that automatically provide CRUD operations and support custom graph traversals through the @Gremlin annotation. By combining standard method name queries (e.g., findByName) with expressive Gremlin scripts, we gain both convenience and fine-grained control over graph traversal logic. These repositories are ideal for clean, testable, and declarative access patterns in graph-based applications. Java @Repository public interface BookRepository extends TinkerPopRepository<Book, Long> { Optional<Book> findByName(String name); @Gremlin("g.V().hasLabel('Book').out('is').hasLabel('Category').has('name','Architecture').in('is').dedup()") List<Book> findArchitectureBooks(); @Gremlin("g.E().hasLabel('is').has('relevance', gte(9)).outV().hasLabel('Book').dedup()") List<Book> highRelevanceBooks(); } @Repository public interface CategoryRepository extends TinkerPopRepository<Category, Long> { Optional<Category> findByName(String name); @Gremlin("g.V().hasLabel('Category').where(__.in('is').count().is(gt(1)))") List<Category> commonCategories(); } After defining the repositories, we can build a full application that leverages them to manage data and run queries. The BookApp2 class illustrates this repository-driven execution flow. It uses the repositories to create or fetch vertices (Book and Category) and falls back to GraphTemplate only when inserting edges, since Jakarta Data currently supports querying vertices but not edge creation. This hybrid model provides a clean separation of concerns and reduces boilerplate, making it easier to read, test, and maintain. Java public final class BookApp2 { private BookApp2() { } public static void main(String[] args) { try (SeContainer container = SeContainerInitializer.newInstance().initialize()) { var template = container.select(GraphTemplate.class).get(); var bookRepository = container.select(BookRepository.class).get(); var repository = container.select(CategoryRepository.class).get(); var software = repository.findByName("Software").orElseGet(() -> repository.save(Category.of("Software"))); var java = repository.findByName("Java").orElseGet(() -> repository.save(Category.of("Java"))); var architecture = repository.findByName("Architecture").orElseGet(() -> repository.save(Category.of("Architecture"))); var performance = repository.findByName("Performance").orElseGet(() -> repository.save(Category.of("Performance"))); var effectiveJava = bookRepository.findByName("Effective Java").orElseGet(() -> bookRepository.save(Book.of("Effective Java"))); var cleanArchitecture = bookRepository.findByName("Clean Architecture").orElseGet(() -> bookRepository.save(Book.of("Clean Architecture"))); var systemDesign = bookRepository.findByName("System Design Interview").orElseGet(() -> bookRepository.save(Book.of("System Design Interview"))); var javaPerformance = bookRepository.findByName("Java Performance").orElseGet(() -> bookRepository.save(Book.of("Java Performance"))); template.edge(Edge.source(effectiveJava).label("is").target(java).property("relevance", 10).build()); template.edge(Edge.source(effectiveJava).label("is").target(software).property("relevance", 9).build()); template.edge(Edge.source(cleanArchitecture).label("is").target(software).property("relevance", 8).build()); template.edge(Edge.source(cleanArchitecture).label("is").target(architecture).property("relevance", 10).build()); template.edge(Edge.source(systemDesign).label("is").target(architecture).property("relevance", 9).build()); template.edge(Edge.source(systemDesign).label("is").target(software).property("relevance", 7).build()); template.edge(Edge.source(javaPerformance).label("is").target(performance).property("relevance", 8).build()); template.edge(Edge.source(javaPerformance).label("is").target(java).property("relevance", 9).build()); System.out.println("Books in 'Architecture' category:"); var architectureBooks = bookRepository.findArchitectureBooks(); architectureBooks.forEach(doc -> System.out.println(" - " + doc)); System.out.println("Categories with more than one book:"); var commonCategories = repository.commonCategories(); commonCategories.forEach(doc -> System.out.println(" - " + doc)); var highRelevanceBooks = bookRepository.highRelevanceBooks(); System.out.println("Books with high relevance:"); highRelevanceBooks.forEach(doc -> System.out.println(" - " + doc)); var bookByName = bookRepository.queryByName("Effective Java"); System.out.println("Book by name: " + bookByName); } } } JanusGraph, backed by Apache TinkerPop and Gremlin, offers a highly scalable and portable way to model and traverse complex graphs. With Eclipse JNoSQL and Jakarta Data, Java developers can harness powerful graph capabilities while enjoying a clean and modular API. Janus adapts to your architecture, whether embedded or distributed, while keeping your queries expressive and concise. References JanusGraph ProjectApache TinkerPopGremlin TutorialEclipse JNoSQLJakarta DataSample Repository
In the first two articles of this series (part 1 and part 2), I demonstrated how quickly an idea can become a reality using Spring Boot, the framework I have used for over 10 years to establish new services. I stepped out of my comfort zone in the last article (part 3) when I used Quarkus for the first time, which offered a really nice CLI to assist with the development process. I would like to close out this short series with another framework that’s new (to me), called Micronaut. Micronaut is an open-source JDK-based framework focused on lightweight microservices. Under the hood, Micronaut does not rely on reflective programming, but emphasizes an inversion of control (IoC) design, which results in less memory usage and a much faster start time. A robust CLI exists, too. While the service should start fast, the biggest question I have is: “How quickly can I transform my motivation quotes idea to a reality using Micronaut?” Getting Started With Micronaut As noted above, just like with Spring Boot and Quarkus, Micronaut also has a CLI (mn). There is also an initializer (called a launcher), which is very similar to what Spring Boot offers. For this article, we’ll use the CLI, and I’ll use Homebrew to perform the install: Shell $ brew install --cask micronaut-projects/tap/micronaut Other installation options can be found here. To get started, we’ll issue the mn command: Shell $ mn For macOS users, you will likely need to visit the Privacy & Security section in System Settings to allow the use of the mn command. We’ll simply use create to interactively initialize a new service: Shell mn> create Apr 01, 2025 2:52:20 PM org.jline.utils.Log logr WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information) What type of application do you want to create? (enter for default) *1) Micronaut Application 2) Micronaut CLI Application 3) Micronaut Serverless Function 4) Micronaut gRPC Application 5) Micronaut Messaging Application We’ll select the default option to create a new application. Shell Choose your preferred language. (enter for default) *1) Java 2) Groovy 3) Kotlin We’ll use Java for this exercise. Shell Choose your preferred test framework. (enter for default) *1) JUnit 2) Spock 3) Kotest We’ll use JUnit for our test framework. Shell Choose your preferred build tool. (enter for default) *1) Gradle (Groovy) 2) Gradle (Kotlin) 3) Maven We’ll plan to use Gradle (Groovy) this time. Shell Choose the target JDK. (enter for default) *1) 17 2) 21 We’ll stick with Java 17, since it matches what we’ve used for other articles in this series. Shell Enter any features to apply. Use tab for autocomplete and separate by a space. For now, we’ll add support for OpenAPI and Swagger and press the return key: Shell openapi swagger-ui Shell Enter a name for the project. We’ll use the name quotes-micronaut for our project. Now let’s exit out of mn using Control-C and check out the base directory structure: Shell $ cd quotes-micronaut && ls -la total 88 drwxr-xr-x@ 13 johnvester 416 Apr 1 14:58 . drwxrwxrwx 93 root 2976 Apr 1 14:58 .. -rw-r--r--@ 1 johnvester 127 Apr 1 14:58 .gitignore -rw-r--r--@ 1 johnvester 1380 Apr 1 14:58 README.md -rw-r--r--@ 1 johnvester 1590 Apr 1 14:58 build.gradle drwxr-xr-x@ 3 johnvester 96 Apr 1 14:58 gradle -rw-r--r--@ 1 johnvester 23 Apr 1 14:58 gradle.properties -rwxr--r--@ 1 johnvester 8762 Apr 1 14:58 gradlew -rw-r--r--@ 1 johnvester 2966 Apr 1 14:58 gradlew.bat -rw-r--r--@ 1 johnvester 367 Apr 1 14:58 micronaut-cli.yml -rw-r--r--@ 1 johnvester 182 Apr 1 14:58 openapi.properties -rw-r--r--@ 1 johnvester 39 Apr 1 14:58 settings.gradle drwxr-xr-x@ 4 johnvester 128 Apr 1 14:58 src We can open the project in IntelliJ and run the Application class: Shell __ __ _ _ | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_ | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __| | | | | | (__| | | (_) | | | | (_| | |_| | |_ |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__| 15:01:56.083 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 648ms. Server Running: http://localhost:8080 We can validate that the RESTful API is working, too, by calling the URI configured in the QuotesMicronautController class. Java @Controller("/quotes-micronaut") public class QuotesMicronautController { @Get(uri="/", produces="text/plain") public String index() { return "Example Response"; } } We’ll use the following cURL command: Shell curl --location 'http://localhost:8080/quotes-micronaut' This returns the following text: Shell Example Response The only difference I saw with using the CLI over the launcher is that I wasn’t able to specify a base package. That’s okay, because we can use the CLI to establish the rest of our classes: Shell $ mn create-bean quotes.micronaut.repositories.QuotesRepository $ mn create-bean quotes.micronaut.services.QuotesService $ mn create-bean quotes.micronaut.controllers.QuotesController You’ll notice we are creating our own controller class. This is simply to follow the same approach I’ve used in my earlier articles. The auto-generated QuotesMicronautController would have worked just fine. To make things less complicated for the injection side of things, we can use Lombok, adding the following dependencies to the build.gradle file: Groovy compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") Minor Roadblock With OpenAPI Generation When I looked up the server-side OpenAPI generators, I was excited to see java-micronaut-server as an option. Upon further review, the documentation indicated this has a stability level of “BETA.” The beta version was able to generate the expected Quote model object, but I ran into issues trying to generate an interface or abstract class that my controllers could extend. So I decided to create an issue in the openapi-generator library (link for those who are interested in the details) and pivot toward another approach. Using Cursor AI to Convert My Quarkus Service While sharing this experience with Alvin Lee (a longtime colleague), he had an idea. Alvin asked, “What if we use Cursor AI to analyze your Quarkus repo and port it to Micronaut for us?” I told Alvin to “go for it” and became excited, because this article really had not required the use of AI like the other articles in the series. I gave Alvin access to fork my Quarkus repo and within a matter of minutes, the service was completely ported to Micronaut. Let’s quickly review the classes that were created automatically. First, let’s review the repository layer: Java @Singleton public class QuotesRepository { private static final List<Quote> QUOTES = List.of( Quote.builder() .id(1) .quote("The greatest glory in living lies not in never falling, but in rising every time we fall.") .build(), Quote.builder() .id(2) .quote("The way to get started is to quit talking and begin doing.") .build(), Quote.builder() .id(3) .quote("Your time is limited, so don't waste it living someone else's life.") .build(), Quote.builder() .id(4) .quote("If life were predictable it would cease to be life, and be without flavor.") .build(), Quote.builder() .id(5) .quote("If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success.") .build() ); public List<Quote> getAllQuotes() { return QUOTES; } public Optional<Quote> getQuoteById(Integer id) { return QUOTES.stream().filter(quote -> quote.getId().equals(id)).findFirst(); } } Next, let’s take a look at the service layer: Java @Singleton public class QuotesService { private final QuotesRepository quotesRepository; @Inject public QuotesService(QuotesRepository quotesRepository) { this.quotesRepository = quotesRepository; } public List<Quote> getAllQuotes() { return quotesRepository.getAllQuotes(); } public Optional<Quote> getQuoteById(Integer id) { return quotesRepository.getQuoteById(id); } public Quote getRandomQuote() { List<Quote> quotes = quotesRepository.getAllQuotes(); return quotes.get(ThreadLocalRandom.current().nextInt(quotes.size())); } } Finally, we can examine the controller that will be the consumer-facing interface to our API: Java @Controller("/quotes") public class QuotesController { @Inject private QuotesService quotesService; @Get public List<Quote> getAllQuotes() { return quotesService.getAllQuotes(); } @Get("/{id}") public Optional<Quote> getQuoteById(@PathVariable Integer id) { return quotesService.getQuoteById(id); } @Get("/random") public Quote getRandomQuote() { return quotesService.getRandomQuote(); } } Validating Our Service Locally To validate the service locally, we’ll start the service from IntelliJ like we did before: Shell __ __ _ _ | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_ | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __| | | | | | (__| | | (_) | | | | (_| | |_| | |_ |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__| 21:18:54.740 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 284ms. Server Running: http://localhost:8080 With all the code in place, the Micronaut service started in just 284 milliseconds. We’ll use the following cURL command to retrieve a random motivational quote: Shell curl --location 'http://localhost:8080/quotes/random' I received a 200 OK HTTP response and the following JSON payload: JSON { "id": 3, "quote": "Your time is limited, so don't waste it living someone else's life." } Looks like the port was quick and successful! Now it is time to deploy. Leveraging Heroku to Deploy the Service Since I used Heroku for my prior articles, I wondered if support existed for Micronaut services. Although Heroku recently published some updated information regarding Micronaut support in its Java buildpack, I felt like I could use my existing experience to get things going. Selecting Heroku helps me deploy my services quickly, and I don’t lose time dealing with infrastructure concerns. The first thing I needed to do was add the following line to the application.properties file to make sure the server’s port could be overridden by Heroku: Properties files micronaut.server.port=${PORT:8080} Next, I created a system.properties file to specify we are using Java version 17: Properties files java.runtime.version = 17 Knowing that Heroku runs the “stage” Gradle task as part of the deployment, we can specify this in the manifest file we plan to use by adding the following lines to the build.gradle file: Groovy jar { manifest { attributes( 'Main-Class': 'quotes.micronaut.Application' ) } } task stage(dependsOn: ['build', 'clean']) build.mustRunAfter clean Ordinarily, for running apps on Heroku, we would need to add a Procfile, where we specify the Heroku environment, reference the path to where the stage-based JARs will reside, and use the port set by Heroku. That file might look like this: Shell web: java $JAVA_OPTS -Dmicronaut.environments=heroku -Dserver.port=$PORT -jar build/libs/*.jar However, on detecting a Micronaut app, the Heroku buildpack defaults to this: Shell java -Dmicronaut.server.port=$PORT $JAVA_OPTS -jar build/libs/*.jar … and that's exactly what I need. No need for a Procfile after all! That's pretty slick. Now we just need to log in to Heroku and create a new application: Shell $ heroku login $ heroku create The CLI responds with the following response: Shell Creating app... done, aqueous-reef-26810 https://aqueous-reef-26810-4db1994daff4.herokuapp.com/ | https://git.heroku.com/aqueous-reef-26810.git The Heroku app instance is named aqueous-reef-26810-4db1994daff4, so my service will run at https://aqueous-reef-26810-4db1994daff4.herokuapp.com/. One last thing to do… push the code to Heroku, which deploys the service: Shell $ git push heroku main Switching to the Heroku dashboard, we see our service has deployed successfully: We are now ready to give our service a try on the internet. Motivational Quotes in Action Using the Heroku app URL, https://aqueous-reef-26810-4db1994daff4.herokuapp.com/, we can now test our Motivational Quotes API using curl commands. First, we retrieve the list of quotes: Shell curl --location 'https://aqueous-reef-26810-4db1994daff4.herokuapp.com/quotes' JSON [ { "id": 1, "quote": "The greatest glory in living lies not in never falling, but in rising every time we fall." }, { "id": 2, "quote": "The way to get started is to quit talking and begin doing." }, { "id": 3, "quote": "Your time is limited, so don't waste it living someone else's life." }, { "id": 4, "quote": "If life were predictable it would cease to be life, and be without flavor." }, { "id": 5, "quote": "If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success." } ] We can retrieve a single quote by its ID: Shell curl --location 'https://aqueous-reef-26810-4db1994daff4.herokuapp.com/quotes/4' JSON { "id": 4, "quote": "If life were predictable it would cease to be life, and be without flavor." } We can retrieve a random motivational quote: Shell curl --location 'https://aqueous-reef-26810-4db1994daff4.herokuapp.com/quotes/random' JSON { "id": 3, "quote": "Your time is limited, so don't waste it living someone else's life." } We can even view the auto-generated Swagger UI via Heroku: Conclusion In this article, I had to step outside my comfort zone again — this time working with Micronaut for the very first time. Along the way, I ran into an unexpected issue which caused me to pivot my approach to leverage AI — more specifically, Cursor. Once ready, I was able to use my existing knowledge to deploy the service to Heroku and validate everything was working as expected. My readers may recall my personal mission statement, which I feel can apply to any IT professional: “Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.” — J. Vester Micronaut offered a fully functional CLI and a service that started the fastest of all the frameworks in the series. Cursor helped us stay on track by porting our Quarkus service to Micronaut. Like before, Heroku provided a fast and easy way to deploy the service without having to worry about any infrastructure concerns. All three key solutions fully adhered to my mission statement, allowing me to not only be able to focus on my idea, but to make it an internet-accessible reality… quickly! If you are interested in the source code for this article, it is available on GitLab. Have a really great day!
OpenAI launched two new Speech to Text models gpt-4o-mini-transcribe and gpt-4o-transcribe in March 2025. These models support streaming transcription for both completed and ongoing audio. Audio transcription refers to converting the audio input to text output (output format would be text or json).The transcription of already completed audio is much simpler using the transcription API provided by OpenAI. The Realtime transcription is useful in application that require immediate feedback such as Voice assistants, Live captioning, Interactive voice applications, Meeting transcription and Accessibility tools. OpenAI has provided Realtime Transcription API (currently in beta) which allows you to stream audio data and receive transcription results in real-time. The realtime transcription API should be invoked using WebSocket or webRTC. This article focuses on invoking Realtime API using Java WebSocket implementation. This image has been designed using resources from Flaticon.com What Are WebSockets WebSockets are a bidirectional communication protocol useful for an ongoing communication between client and server. This differs from HTTP, which follows request response format and client needs to submit a request in-order to get a response from service. WebSockets create a single, long lived TCP connection that allows for a 2 way communication between client and server. In real-time audio transcription, the client sends audio chunks as they become available; the OpenAI API returns transcription results upon processing. WebSocket Methods OnOpen(): This method is invoked when WebSocket connection is established.onMessage(): This method is invoked when the client receives any message from the server. We should add the core logic to detect errors and process transcription response here.onClose(): This method is invoked when the WebSocket client is closed. WebSocket connection could be closed by both client and server.onError(): This method is invoked when WebSocket encounters an error. WebSocket always closes the connection after an error. Implementation In your Java project and add the Java-WebSocket dependency to your pom.xml file. Pick the latest stable version from MVN Repository. XML <!--WebSocket client --> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.6.0</version> </dependency> Create two classes: SimpleOpenAITranscription and TranscriptionWebSocketClient. TranscriptionWebSocketClient will contain the WebSocket Client implementation. SimpleOpenAITranscription will contain the main method that co-ordinates audio streaming and transcription. In the code examples below, I have specified the class they belong to. Establish a WebSocket connection to OpenAI's API The OpenAI API uses API keys for authentication. To create API keys, login to your OpenAI account and go to organization settings. The connection is established by invoking client.connect() from the main method. The WebSocket url is wss://api.openai.com/v1/realtime?intent=transcription Java //TranscriptionWebSocketClient // Set the request headers private static class TranscriptionWebSocketClient extends WebSocketClient { public TranscriptionWebSocketClient(URI serverUri) { super(serverUri, createHeaders()); } private static Map<String, String> createHeaders() { Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "Bearer " + API_KEY); headers.put("openai-beta", "realtime=v1"); return headers; } } Java //SimpleOpenAITranscription TranscriptionWebSocketClient client = new TranscriptionWebSocketClient(new URI(WEBSOCKET_URL)); client.connect(); // Wait until websocket connection is established while (!client.isOpen()) { System.out.println("Websocket is not open."); Thread.sleep(100); } Set Up Transcription Session A config message is required while creating the transcription session. The model name in config could be either gpt-4o-transcribe or gpt-4o-mini-transcribe based on your applications requirement. The language (eg: "en", "fr", "ko") is optional field, but when specified improves accuracy. The turn_detection field is used to setup Voice Activity Detection (VAD). When VAD is enabled, OpenAI will automatically detect any silence in audio and commit the audio message. Once committed, the server responds with transcription result. I have disabled VAD for simplicity. Java // SimpleOpenAITranscription client.sendInitialConfig(); Java //TranscriptionWebSocketClient public void sendInitialConfig() { JSONObject config = new JSONObject() .put("type", "transcription_session.update") .put("session", new JSONObject() .put("input_audio_format", "pcm16") .put("input_audio_transcription", new JSONObject() .put("model", "gpt-4o-transcribe") .put("language", "en")) .put("turn_detection", JSONObject.NULL) .put("input_audio_noise_reduction", new JSONObject() .put("type", "near_field"))); send(config.toString()); } Stream Audio Data in Chunks The audio chunks are sent using “input_audio_buffer.append" message type. When VAD is disabled, we must commit the messages manually to receive transcription result. Java //SimpleOpenAITranscription File audioFile = new File(args[0]); AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(audioFile); byte[] buffer = new byte[CHUNK_SIZE]; int bytesRead; while ((bytesRead = audioInputStream.read(buffer)) != -1) { client.sendAudioChunk(buffer, bytesRead); } client.commitAndClearBuffer(); Java //TranscriptionWebSocketClient public void sendAudioChunk(byte[] buffer, int bytesRead) { // Create a new byte array with only the read bytes byte[] audioData = Arrays.copyOf(buffer, bytesRead); String base64Audio = Base64.getEncoder().encodeToString(audioData); JSONObject audioMessage = new JSONObject() .put("type", "input_audio_buffer.append") .put("audio", base64Audio); send(audioMessage.toString()); } public void commitAndClearBuffer() { send(new JSONObject().put("type", "input_audio_buffer.commit").toString()); send(new JSONObject().put("type", "input_audio_buffer.clear").toString()); } Receive and Process the Responses in Real-Time The client receives messages from server using onMessage() method of WebSocket. The transcript is present in conversation.item.input_audio_transcription.completed message type. Application should listen these events and display the transcription response to users. Java //TranscriptionWebSocketClient @Override public void onMessage(String message) { System.out.println("message: " + message); JSONObject response = new JSONObject(message); if ("conversation.item.input_audio_transcription.completed".equals(response.get("type"))) { System.out.println("Transcription: " + response.getString("transcript")); this.close(); } } These are few other response types that are helpful to track transcription_session.created: Session is created.transcription_session.updated: Session is updated based on the config payload.input_audio_buffer.committed: The audio was committed by the client.conversation.item.input_audio_transcription.delta: Partial transcriptions received from server. Tips to improve your application For better tracking the transcription session, create a sessionCreatedLatch and streamLatch variables of type CountDownLatch. Use sessionCreatedLatch to wait sending audio until “transcription_session.created” event is received. Use streamLatch to keep the audio stream open until client has sent all events and has received all transcriptions back.The Voice Activity detection(VAD) feature automatically detects start and end of speech turns. When using server_vad mode of VAD, you might need to adjust silence_duration_ms if silences in your audio are incorrectly identified.Never hardcode your API key in your application. You could use AWS secrets manager to store the secrets.When VAD is disabled, commit and clear audio buffer at periodic intervals to avoid exceeding the input buffer limit.Adding a small delay between events prevent overwhelming the connection. OpenAI response contains gibberish text when the connection is overwhelmed.Close connections properly during errors to free up resources. The WebSocket connection times out at 30 minutes.As OpenAI's realtime audio transcription feature is actively developed, regularly check the official official documentation for updates By following this guide, you should now be able to integrate OpenAI's Realtime Transcription API into your Java applications.
In modern distributed systems, calling other services from an application is a common task for developers. The use of WebClient is ideally suitable for such tasks owing to its non-blocking nature. The WebClient is a reactive HTTP client in Spring WebFlux. Its main advantage is asynchronic, non-blocking communication between microservices or external APIs. Everything in WebClient is built around events and reactive streams of data, enabling you to focus on business logic and forget about efficient management of threads, locks, or state. WebClient Key Features Non-blocking: Provides a non-blocking operation mode that greatly scales up the performance of an application.Reactive REST client: Interact with Spring’s reactive paradigm, using all the benefits for which it is known.Wrapper to Reactor Netty: Since it’s built on top of Reactor Netty, it is a powerful means to work with HTTP at low-level network operations.Immutable, thread-safe: It is safe for multithreaded use. How WebClient Works At the core of WebClient is reactive programming, which stands in huge contrast to the classic usage of blocking operations. The main idea behind reactive programming is processing data and events as they appear, without blocking threads. The main mechanism to ensure asynchrony or non-blocking execution of tasks in WebClient is the Event Loop. Now, imagine a system that has an incoming request queue — inbound queue — and one thread per CPU core, to keep things simple. This thread constantly polls the queue for tasks. In case there are no tasks in the queue, it stays in an IDLE state. Once a request comes into the queue, this thread picks it up for execution but does not wait until the task is finished. Right away, it starts processing another request. If it is completed, then it fetches the response and puts it into an outbound queue for further processing. Key Aspects of Working With WebClient Creating a WebClient A simple example of creating a WebClient: Java @Bean(name = "webClient") public WebClient commonWebClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .build(); } Path Variables In WebClient you have several ways to work with URI variables are supported, allowing you to flexibly build request URLs. String concatenation: The most evident and, hence, the easiest way is to use string concatenation. In real programs, it is seldom applied. Java public Mono<Product> commonOption(Long id) { return webClient.get() .uri("api/products/" + id) .retrieve() .bodyToMono(Product.class); } Using varargs: This is a variable-length method that lets you dynamically replace values in the URI template using variables, with positional substitution, meaning it’s order-dependent. You must pass the variables in the same order they appear in the URI template. Java public Mono<Product> varargOption(Long id) { return webClient.get() .uri("api/products/{id}", id) .retrieve() .bodyToMono(Product.class); } public Mono<Product> varargsOption(String version, Long id) { return webClient.get() .uri("api/{version}/products/{id}", version, id) .retrieve() .bodyToMono(Product.class); } Using a map: In case your URL contains many variables or their order may change, using a Map becomes the most convenient way to work with URI variables. Java public Mono<Product> mapOption(String version, Long id) { var map = Map.ofEntries( Map.entry("version", version), Map.entry("id", id) ); return webClient.get() .uri("api/{version}/products/{id}", map) .retrieve() .bodyToMono(Product.class); } Query Parameters When you’re dealing with HTTP requests, in real-life situations like using WebClient to interact with a server, you often need to include query parameters in the URL, such as filters for data retrieval and search criteria for results. These parameters play a role in informing the server on how to handle your request. In our exploration of working with WebClient, we will delve into methods of incorporating query parameters into a URL, which include using UriBuilder passing parameters through a Map structure and understanding how it manages encoding and value processing nuances. Using UriBuilder The UriBuilder method provides a safe and flexible way to create URLs with query parameters. It allows you to dynamically append paths, parameters, and variables, without needing to worry about encoding or formatting issues. Java Flux<Product> products = webClient.get() .uri(uriBuilder -> uriBuilder .path("/api/products") .queryParam("page", 0) .queryParam("size", 20) .build()) .retrieve() .bodyToFlux(Product.class); Named Parameters If you want to work with named parameters, you can use the build(Map<String, ?> variables) method. Java Map<String, Object> uriVariables = Map.of("f1", 0, "f2", 20); Flux<Product> products = webClient.get() .uri(uriBuilder -> uriBuilder .path("/api/products") .queryParam("page", "{f1}") .queryParam("size", "{f2}") .build(uriVariables)) .retrieve() .bodyToFlux(Product.class); Sending Parameters via Map Another approach is passing query parameters using a MultiValueMap. Java MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); queryParams.add("page", "10"); queryParams.add("size", "20"); queryParams.add("filter", "active"); Flux<Product> products = webClient.get() .uri(uriBuilder -> uriBuilder .path("/api/products") .queryParams(queryParams) .build()) .retrieve() .bodyToFlux(Product.class); Sending Multiple Values Java queryParams.add("category", "electronics"); queryParams.add("category", "books"); This will generate the parameters category=electronics&category=books. Other Useful UriBuilder Methods 1. Conditional parameter addition You can choose to include certain parameters only when specific conditions are met. Java String filter = getFilter(); // May return null Flux<Product> products = webClient.get() .uri(uriBuilder -> { var builder = uriBuilder.path("/api/products"); if (filter != null) { builder.queryParam("filter", filter); } return builder.build(); }) .retrieve() .bodyToFlux(Product.class); 2. Passing collections You can pass multiple values for a single parameter by using a list. Java var categories = List.of("electronics", "books", "clothing"); Flux<Product> products = webClient.get() .uri(uriBuilder -> uriBuilder .path("/api/products") .queryParam("category", categories) .build()) .retrieve() .bodyToFlux(Product.class); This will generate the URL query parameters like category=electronics&category=books&category=clothing. Streaming Response (Flux) In certain situations, you may need to start processing data as soon as it begins arriving, rather than waiting for all of it to load at once. This can be especially useful when working with large datasets or continuous data streams. WebClient has built-in support for streaming responses using reactive types like Flux Java public Flux<Product> streamingProduct() { return webClient.get() .uri("api/products") .retrieve() .bodyToFlux(Product.class); } Body Publisher vs. Body Value When working with WebClient, you’ll frequently need to send POST requests that include a body. Depending on the type of data you’re handling, you’ll want to choose between the bodyValue() and body() methods. Understanding when to use each will help you make the most of WebClient and ensure your data gets transmitted properly. bodyValue() When you use the bodyValue() method, it’s best for situations where you have an object or value stored in memory that you want to send as the request body. This could be as straightforward as a string or a number, or it could involve an intricate object like a DTO that can be smoothly converted into formats like JSON. Java public Mono<Product> bodyValue() { var product = new Product(1L, "product_1"); return webClient .post() .uri("api/products") .bodyValue(product) // <-- this is for objects .retrieve() .bodyToMono(Product.class); } When Should You Use bodyValue() Data is ready: Use this method when your data is already available in memory and doesn’t need to be fetched asynchronously.Simple data types: It’s perfect for sending basic data types like strings, numbers, or DTOs.Synchronous scenarios: When there’s no need for asynchronous or streaming data, bodyValue() keeps things straightforward. body() The body() function, on the other hand, is intended for more complicated scenarios. When your data originates from a reactive stream or any other source that uses the Publisher interface, such as Flux or Mono, this technique can be helpful. It comes in particularly useful when you’re retrieving data from a database or other service, for example, and the data you’re transmitting isn’t immediately available. Java public Mono<Product> bodyMono(Mono<Product> productMono) { return webClient .post() .uri("api/products") .body(productMono, Product.class) // <-- Use this for Mono/Flux data streams .retrieve() .bodyToMono(Product.class); } When dealing with this scenario, we are providing a Mono<Product> to the body() function. The WebClient will wait for the Mono to complete before sending the resolved data in the request body. Default Headers During development work, it’s often necessary to use HTTP headers for all requests — whether for authentication purposes, to specify content type, or to send custom metadata. Instead of including these headers in every single request separately, WebClient offers the option to define default headers at the client level. When you first create your WebClient instance make sure to set up these headers so they are automatically included in all requests, saving time and reducing the need, for code. Using defaultHeader If you only need to set a few headers that will apply to all requests made by your WebClient, the defaultHeader method is a straightforward solution. Java public WebClient commonWebClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .defaultHeader("Custom-Header", "Custom-Value") .build(); } Using defaultHeaders For situations where you need to set multiple headers at once, you can use the defaultHeaders method, which accepts a Consumer<HttpHeaders>. This approach lets you add several headers in a more organized way. Java public WebClient commonWebClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .defaultHeaders(headers -> { headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.add("Custom-Header", "Custom-Value"); }) .build(); } You can also use a map with headers and the setAll method to set them all at once. Java public WebClient commonWebClient() { var headersMap = Map.of( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE, "Custom-Header", "Custom-Value" ); return WebClient.builder() .baseUrl("http://localhost:8080") .defaultHeaders(headers -> headers.setAll(headersMap)) .build(); } Overriding Headers for a Specific Request Sometimes, there could be situations where you have to change or add headers for a request. In scenarios when dealing with a single request level usage of header() or headers() directly. Java public Flux<Product> headers() { return webClient.get() .uri("api/products") .header("Custom-Header", "New-Value") //change header .headers(httpHeaders -> { httpHeaders.add("Additional-Header", "Header-Value"); //add new header }) .retrieve() .bodyToFlux(Product.class); } Setting Authentication Headers Authentication headers are essential when dealing with secured endpoints. Whether you’re using Basic Authentication or a Bearer token, WebClient makes it easy to set these headers. Basic Authentication For Basic Authentication, where credentials are encoded in Base64, you can use the setBasicAuth() method: Java public WebClient commonWebClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .defaultHeaders(headers -> headers.setBasicAuth("username", "password")) .build(); } Bearer Token Authentication If you are using Bearer tokens, such as JWTs, the setBearerAuth() method simplifies the process: Java public WebClient commonWebClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .defaultHeaders(headers -> headers.setBearerAuth("your_jwt_token")) .build(); } Dynamically Updating Authentication Tokens In cases where your authentication token may change frequently (e.g., OAuth2 tokens), it’s efficient to dynamically set these headers for each request. This can be accomplished using an ExchangeFilterFunction. Here's an example of setting up a dynamic Bearer token. Java @Bean(name = "webClientDynamicAuth") public WebClient webClientDynamicAuth() { ExchangeFilterFunction authFilter = ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { ClientRequest authorizedRequest = ClientRequest.from(clientRequest) .headers(headers -> headers.setBearerAuth(getCurrentToken())) .build(); return Mono.just(authorizedRequest); }); return WebClient.builder() .baseUrl("http://localhost:8080") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .filter(authFilter) .build(); } In this case, the getCurrentToken() method fetches the latest token each time a request is initiated to guarantee that the authentication token remains current at all times. Error Handling When you use WebClient to connect to remote services, errors such as network problems, service outages, and server or client-side failures can occur. It’s important to handle these errors so that your application remains stable and resilient. In this part, we will look at ways to handle errors in WebClient and focus on operators such as onErrorReturn, onErrorResume, doOnError , and onErrorMap. Key Error-Handling Methods These are special operators in reactive programming that help in controlling and handling errors (exceptions) in a thread, which is a clean and efficient way of handling exceptions. onErrorReturn The onErrorReturn method replaces the error in the thread with the default value, and makes sure that the thread terminates without throwing an exception. Return a default value on any error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorReturn(new Product()); // Returns an empty product on any error In this scenario, if an error occurs, a default Product object is returned, preventing an exception from propagating. Handling a specific type of error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorReturn(WebClientResponseException.NotFound.class, new Product(-1, "NOT_FOUND")); Here, a 404 Not Found error results in a custom Product object, while other errors will continue to be passed through the stream. Filter errors using a predicate: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorReturn( e -> e instanceof WebClientResponseException && ((WebClientResponseException) e).getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE, new Product(-1, "UNAVAILABLE") ); In this example, we use a predicate to check if the error is a 503 Service Unavailable status, and in that case, return an object. onErrorResume The onErrorResume method allows you to switch the stream to an alternative Publisher in case of an error. This gives you the opportunity to perform additional actions or return another data stream when an exception occurs. Switch to an alternative stream on any error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorResume(e -> { // Log the error logger.error("Error retrieving product", e); // Return an alternative Mono return Mono.just(new Product()); }); Handling a specific type of error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorResume(WebClientResponseException.NotFound.class, e -> { // Additional handling of 404 error logger.warn("Product not found: {}", id); // Return an empty Mono return Mono.empty(); }); Using a predicate: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorResume(e -> e instanceof TimeoutException, e -> { // Handle timeout return getProductFromCache(id); // Method to get product from cache }); Here, if a timeout occurs, the error is handled by falling back to a cached version of the product. doOnError The doOnError method allows you to perform side effects (such as logging) when an error occurs, without changing the error stream itself. Logging any error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .doOnError(e -> logger.error("Error retrieving product", e)); Logging a specific error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .doOnError(WebClientResponseException.NotFound.class, e -> logger.warn("Product not found: {}", id)); Collecting error metrics: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .doOnError(e -> errorCounter.increment()); onErrorMap This method transforms one type of exception into another. It is helpful when you need to mask internal details or convert exceptions into custom ones. Transform all errors into a custom exception: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorMap(e -> new ProductServiceException("Error retrieving product", e)); Transform a specific error: Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .onErrorMap(WebClientResponseException.NotFound.class, e -> new ProductNotFoundException("Product not found: " + id)); Combining Error Handling Methods You can combine various methods for more flexible error handling. Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class) .doOnError(e -> logger.error("Error retrieving product", e)) .onErrorResume(WebClientResponseException.NotFound.class, e -> { logger.warn("Product not found: {}", id); return Mono.empty(); }) .onErrorReturn(new Product(-99, "DEFAULT")); In this case: All errors are logged.A 404 Not Found error returns an empty Mono.For any other error, a default Product object is returned to ensure the stream completes without propagating the error. retrieve() vs. exchange() When you work with WebClient to send HTTP requests. You may come across two approaches, for managing responses: retrieve() and exchange(). retrieve() The approach makes it easier to deal with replies by concentrating on getting the response content and managing HTTP error statuses automatically. Characteristics of retrieve() Automatic error handling: In case the server encounters an error (status codes 4xx or 5xx), retrieve() will automatically throws a WebClientResponseException.Ease of use: When you just need only the body of a response and do not even care about formats like headers or status codes. Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .retrieve() .bodyToMono(Product.class); exchange()/exchangeToMono() Returns ClientResponse that provides access to the underlying status code and response from the server. With that, you are able to get into details like status codes, headers, cookies, etc. Note: As of Spring WebFlux 5.2, the exchange() method is deprecated; use the safer and preferred exchangeToMono() or exchangeToFlux() Characteristics of exchangeToMono() and exchangeToFlux Full control: You can manipulate the ClientResponse object, which allows you to deal with headers, status codes, cookies, and everything else.Flexibility in error handling: Implement your own response status and exceptions management strategy with the same interface. Java Mono<Product> productMono = webClient.get() .uri("/api/products/{id}", id) .exchangeToMono(response -> { HttpStatus status = response.statusCode(); HttpHeaders headers = response.headers().asHttpHeaders(); if (status.is2xxSuccessful()) { // Extract custom header String requestId = headers.getFirst("X-Request-ID"); return response.bodyToMono(Product.class) .doOnNext(product -> { // Use information from headers product.setRequestId(requestId); }); } else { // Handle errors return response.createException().flatMap(Mono::error); } }); In this example: The status code and headers are retrieved from the ClientResponse.If the response is successful, we extract the X-Request-ID custom header and add it to the Product object.If an error occurs, we handle it by creating and propagating an exception. Detailed Comparison of retrieve() and exchangeToMono() ExchangeFilterFunction ExchangeFilterFunction is a functional interface in Spring WebFlux, that gives you the ability to intercept and modify requests and responses. For example, features like logging, authentication, error handling, metrics, and caching can be plugged into your HTTP calls with the help of this ability without altering the core application logic. Serving as a filter, ExchangeFilterFunction represents a web filter that runs on every request and response. You can chain multiple filters together, offering a modular and flexible way to enhance the functionality of your HTTP client. Java @FunctionalInterface public interface ExchangeFilterFunction { Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next); } It accepts a ClientRequest and an ExchangeFunction, and returns a Mono<ClientResponse>. In simple terms, it is a function that enables you to: Modify the Request before it’s sent.Modify the Response after it’s received.Perform Additional Actions before or after sending the request and receiving the response. How to Use ExchangeFilterFunction Adding Filters When Creating WebClient When constructing a WebClient instance with the builder, you can tack one or more filters onto it like so. Java public WebClient commonWebClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .filter(filter1) .filter(filter2) .build(); Note: Filters are applied in the order they are added. Creating an ExchangeFilterFunction You can use static methods exposed by ExchangeFilterFunction: ofRequestProcessor(): To process or modify the ClientRequestofResponseProcessor(): To process or modify the ClientResponse Example 1: Logging requests and responses Java public WebClient commonWebClient() { ExchangeFilterFunction logRequest = ExchangeFilterFunction.ofRequestProcessor(request -> { logger.info("Request: {} {}", request.method(), request.url()); return Mono.just(request); }); ExchangeFilterFunction logResponse = ExchangeFilterFunction.ofResponseProcessor(response -> { logger.info("Response status: {}", response.statusCode()); return Mono.just(response); }); return WebClient.builder() .baseUrl("http://localhost:8080") .filter(logRequest) .filter(logResponse) .build(); } Example 2: Error handling and retrying Java public WebClient commonWebClient() { ExchangeFilterFunction retryFilter = (request, next) -> next.exchange(request) .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1)) .filter(throwable -> throwable instanceof IOException)) .onErrorResume(throwable -> { logger.error("Error executing request: {}", throwable.getMessage()); return Mono.error(throwable); }); return WebClient.builder() .baseUrl("http://localhost:8080") .filter(retryFilter) .build(); } The filter intercepts the response and, if an IOException occurs, tries to repeat the request up to three times with a delay of 1 second.On final failure, it logs the error and propagates the exception. Conclusion During this article, we saw some of the features of the Spring WebClient and how this is a better approach provided by Spring to do HTTP client requests in a reactive and non-blocking way. We deeply reviewed the powerful features regarding setup and configurations. We have reviewed some of the benefits that it offers at the time of composing requests and handling responses. In the same vein, we likewise discussed handling responses coming from a service, handling error scenarios, and how to handle an empty response. You can also read an article about functional endpoints.
Artificial intelligence (AI) offers great potential for software applications by providing options like natural language processing, image recognition, and predictive analysis, which can be integrated within software. This tutorial aims to empower developers to unlock advanced functionalities by providing a blend of theoretical insights and practical, code-centric examples, allowing for seamless integration of AI in their Java applications. AI Foundations for Java Developers AI offers a host of techniques that enable software to mimic human intelligence, such as reasoning, pattern recognition, and natural language understanding. At its very core, machine learning (ML) uses exposure to data to enhance the performance of its algorithms. Deep learning, on the other hand, is a specialized subset of ML that uses neural networks to identify intricate patterns. This makes Deep Learning an ideal choice for tasks like image classification and language generation. Java developers have a choice of various frameworks and APIs, such as TensorFlow, Deeplearning4j, and OpenAI, to harness the power of AI. These libraries and frameworks will not only offer cutting-edge AI capabilities but also ensure to do so gracefully by ensuring scalability and efficiency. Essential Tools and Frameworks Some of the options to integrate AI in Java applications are, Deeplearning4j – A Java-native deep learning library that can be used for seamless integration into a Java applicationTensorFlow Java API – Google's AI framework for machine learning, deep learning, etc.OpenAI API – A cloud-based service that can be used for advanced language tasks, allowing developers to add smart language features using prompt engineering This tutorial focuses on the TensorFlow Java API and OpenAI API due to their powerful features, extensive documentation, and developer-friendly interface, which makes them ideal for Java-based AI projects. Environment Setup To begin, add the following dependencies to the Maven project to enable TensorFlow and OpenAI API integrations: XML <dependencies> <dependency> <groupId>org.tensorflow</groupId> <artifactId>tensorflow</artifactId> <version>0.6.0</version> </dependency> <dependency> <groupId>com.theokanning.openai-gpt3-java</groupId> <artifactId>service</artifactId> <version>0.18.2</version> </dependency> </dependencies> Ensure Java version 8 or later is being used. For using TensorFlow, you may also need to install native libraries like libtensorflow_jni. More details can be found at the following links: TensorFlow Java documentationFor using OpenAI, an API key is needed, which can be obtained from OpenAI’s website Practical AI Implementations in Java Let's dive into practical examples showing the integration of AI with Java. 1. Image Classification With TensorFlow This example shows how a pre-trained TensorFlow model can be loaded to classify images, demonstrating the power of deep learning in visual recognition. Java import org.tensorflow.*; import java.nio.file.Files; import java.nio.file.Paths; public class ImageClassifier { public static void main(String[] args) throws Exception { byte[] graphDef = Files.readAllBytes(Paths.get("model.pb")); try (Graph graph = new Graph()) { graph.importGraphDef(graphDef); try (Session session = new Session(graph)) { Tensor<Float> input = preprocessImage("image.jpg"); Tensor<?> result = session.runner() .feed("input_tensor", input) .fetch("output_tensor") .run().get(0); System.out.println(processResults(result)); } } } private static Tensor<Float> preprocessImage(String imagePath) { // Implement preprocessing (resize, normalization) } private static String processResults(Tensor<?> tensor) { // Interpret tensor output to obtain readable labels } } 2. Natural Language Processing With OpenAI This example uses the OpenAI API to generate human-like responses. The temperature can be adjusted to change the type of response. For a more deterministic and focused response, use temperature < 0.7, for a more creative use > 0.7. Java import com.theokanning.openai.OpenAiService; import com.theokanning.openai.completion.CompletionRequest; public class NLPExample { public static void main(String[] args) { String apiKey = "API_KEY"; OpenAiService service = new OpenAiService(apiKey); CompletionRequest request = CompletionRequest.builder() .model("text-davinci-003") .prompt("Describe the benefits of integrating AI in Java.") .maxTokens(150) .temperature(0.7) .build(); service.createCompletion(request) .getChoices() .forEach(choice -> System.out.println(choice.getText())); } } 3. Sentiment Analysis Using TensorFlow This example uses a pre-trained sentiment model to analyze sentiments. The output of the score will be between 0 and 1, where 0 represents a fully negative sentiment, while 1 represents a fully positive sentiment, and values in between will represent a sentiment leaning to negative or positive based on the value. Java import org.tensorflow.*; import java.nio.file.Files; import java.nio.file.Paths; public class SentimentAnalysis { public static void main(String[] args) throws Exception { byte[] graphDef = Files.readAllBytes(Paths.get("sentiment_model.pb")); try (Graph graph = new Graph()) { graph.importGraphDef(graphDef); try (Session session = new Session(graph)) { Tensor<String> input = Tensor.create("This is an amazing tutorial!"); Tensor<?> result = session.runner() .feed("input_tensor", input) .fetch("output_tensor") .run().get(0); System.out.println("Sentiment Score: " + processSentiment(result)); } } } private static float processSentiment(Tensor<?> tensor) { // Convert tensor output into sentiment score } } 4. Building a Chatbot With OpenAI The following shows an example of using OpenAI to build a chatbot. Java import com.theokanning.openai.OpenAiService; import com.theokanning.openai.completion.chat.ChatCompletionRequest; import com.theokanning.openai.completion.chat.ChatMessage; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Chatbot { public static void main(String[] args) { String apiKey = System.getenv("API_KEY"); OpenAiService service = new OpenAiService(apiKey); Scanner scanner = new Scanner(System.in); List<ChatMessage> conversation = new ArrayList<>(); conversation.add(new ChatMessage("system", "A helpful assistant!")); while (true) { System.out.print("User: "); String userInput = scanner.nextLine(); if (userInput.equalsIgnoreCase("exit")) break; conversation.add(new ChatMessage("user", userInput)); ChatCompletionRequest request = ChatCompletionRequest.builder() .model("gpt-3.5-turbo") .messages(conversation) .maxTokens(100) .build(); String response = service.createChatCompletion(request) .getChoices().get(0).getMessage().getContent(); System.out.println("Bot: " + response); conversation.add(new ChatMessage("assistant", response)); } scanner.close(); } } Conclusion As we see, integrating AI into Java applications can be as simple as integrating a library. Tools like TensorFlow and OpenAI API make it super easy for Java developers to harness the power of deep learning and natural language processing in their applications. This tutorial covered the essential steps to get started, like setting up the environment, getting an API key, with some real-life examples, image classifications, generating human-like responses, sentiment analysis, and building a simple chatbot. Apart from being extremely simple to integrate, these frameworks are highly flexible and scalable. Whether you are enhancing an existing project or building a new application from the ground up, the code and concept provided here will build a good foundation on which to build your features. Feel free to experiment with different models and adjust parameters, such as temperature or classification methods, to suit your specific use case. Also, explore the vast and useful TensorFlow Java documentation and OpenAI documentation to get more information if your needs vary. As AI keeps on evolving, there are always exciting opportunities to supercharge your applications with new features and better intelligence. So, what's next? Integrate a pre-trained model in your application and start feeling the power of AI. Drop your comments on how you see AI helping your application!
John Vester
Senior Staff Engineer,
Marqeta
Thomas Jardinet
IT Architect,
Rhapsodies Conseil